动态开点线段树
1. 105386J
抽象题面:带多重资源限制的最短路问题
在图上寻找最短路径,但每走一条边不仅看距离,还需要消耗特定种类的“资源”。且资源的使用有严格的顺序要求和单次容量上限。
核心思路:
- 状态隔离:因为资源种类很多,如果混在一起处理会很慢。所以用线段树森林,为每种资源单独开一棵动态开点线段树,维护该区间内资源的最大可用容量。
- 树上二分(加速转移):在跑 Dijkstra 寻路时,传统做法是挨个找下一个可用的资源,极其耗时。现在的做法是直接去对应资源的线段树上查,利用
findFirst(线段树上二分),以极快的速度(对数时间)跳过不合法的区间,精准定位到下一个容量达标的可用资源。
点击查看代码
struct Node {
int v;
int ticket;
int curlen; // 当前用了多少
bool operator<(const Node &t) const {
if (ticket != t.ticket) return ticket > t.ticket;
return curlen > t.curlen;
}
};
struct node {
int v;
int l;
int c;
};
const int N = 5e5 + 10;
vector<node> g[N];
template <class Info> struct DynamicSegmentTree {
struct Node {
int ls = 0; // 左孩子下标
int rs = 0; // 右孩子下标
Info info;
};
int n;
vector<Node> nodes;
DynamicSegmentTree(int n_) : n(n_) {
nodes.push_back(Node{});
}
int newNode() {
nodes.emplace_back();
return (int)nodes.size() - 1;
}
// 上拉更新
void pull(int p) {
const Info &lhs = nodes[p].ls ? nodes[nodes[p].ls].info : Info();
const Info &rhs = nodes[p].rs ? nodes[nodes[p].rs].info : Info();
nodes[p].info = lhs + rhs;
}
// 如果是新的树开一个点
// 如果是旧树不变
int modify(int p, int l, int r, int x, const Info &v) {
if (!p) p = newNode(); // 动态开点
if (r - l == 1) {
nodes[p].info = v;
return p;
}
int m = l + (r - l) / 2;
// 移除 push(p)
if (x < m) {
int new_ls = modify(nodes[p].ls, l, m, x, v);
nodes[p].ls = new_ls;
} else {
int new_rs = modify(nodes[p].rs, m, r, x, v);
nodes[p].rs = new_rs;
}
pull(p); // 更新当前节点信息
return p;
}
// 外部接口:单点修改
void modify(int &root, int x, const Info &v) {
root = modify(root, 0, n, x, v);
}
// 区间查询
Info rangeQuery(int p, int l, int r, int x, int y) {
// 如果节点不存在,或者区间无交集,返回单位元
if (!p || l >= y || r <= x) {
return Info();
}
// 完全包含
if (l >= x && r <= y) {
return nodes[p].info;
}
int m = l + (r - l) / 2;
return rangeQuery(nodes[p].ls, l, m, x, y) + rangeQuery(nodes[p].rs, m, r, x, y);
}
// 外部接口:区间查询
Info rangeQuery(int root, int l, int r) {
return rangeQuery(root, 0, n, l, r);
}
// 线段树二分:查找区间 [x, y) 内第一个满足 pred 的位置
template <class F> int findFirst(int p, int l, int r, int x, int y, F &&pred) {
if (!p || l >= y || r <= x) return -1;
// 剪枝:如果当前区间的信息都不满足条件,直接返回
if (l >= x && r <= y && !pred(nodes[p].info)) return -1;
if (r - l == 1) return l;
int m = l + (r - l) / 2;
// 移除 push(p)
int res = findFirst(nodes[p].ls, l, m, x, y, pred);
if (res == -1) res = findFirst(nodes[p].rs, m, r, x, y, pred);
return res;
}
template <class F> int findFirst(int root, int l, int r, F &&pred) {
return findFirst(root, 0, n, l, r, pred);
}
// --- 你要求的 findLast ---
template <class F> int findLast(int p, int l, int r, int x, int y, F &&pred) {
if (!p || l >= y || r <= x) return -1;
if (l >= x && r <= y && !pred(nodes[p].info)) return -1;
if (r - l == 1) return l;
int m = l + (r - l) / 2;
// 优先找右边
int res = findLast(nodes[p].rs, m, r, x, y, pred);
if (res == -1) res = findLast(nodes[p].ls, l, m, x, y, pred);
return res;
}
template <class F> int findLast(int root, int l, int r, F &&pred) {
return findLast(root, 0, n, l, r, pred);
}
};
struct Info {
int mx = -1; // 默认为 0 或 -INF,视具体题目而定
};
// 重载 + 运算符用于合并
Info operator+(const Info &a, const Info &b) {
Info res;
res.mx = max(a.mx, b.mx);
return res;
}
void solve() {
int n, m, k;
cin >> n >> m >> k;
for (int i = 1; i <= n; i++)
g[i].clear();
vector<array<int, 2>> a(k + 1);
for (int i = 1; i <= m; i++) {
int u, v, c, l;
cin >> u >> v >> c >> l;
g[u].push_back({v, l, c});
g[v].push_back({u, l, c});
}
DynamicSegmentTree<Info> seg(k + 1);
vector<int> root(m + 1);
for (int i = 1; i <= k; i++) {
cin >> a[i][0] >> a[i][1];
int id = a[i][0];
// root[id]=seg.newTree();
seg.modify(root[id], i, {a[i][1]});
}
auto dij = [&]() {
vector<bool> vis(n + 1, 0);
priority_queue<Node> q;
q.push({1, 1, 0});
while (q.size()) {
auto [u, ticket, curlen] = q.top();
q.pop();
if (vis[u]) continue;
vis[u] = 1;
if (ticket > k) continue;
for (auto [v, l, c] : g[u]) {
// v是下一个点 l是长度 c是哪一个公司
int nowG = a[ticket][0];
int nowLimit = a[ticket][1];
if (c == nowG && curlen + l <= nowLimit) {
// 走这个路
q.push({v, ticket, curlen + l});
}
auto cmp = [&](Info &info) { return info.mx >= l; };
int next = seg.findFirst(root[c], ticket + 1, k + 1, cmp);
if (next != -1) {
q.push({v, next, l});
}
}
}
for (int i = 1; i <= n; i++)
if (vis[i])
cout << 1;
else
cout << 0;
cout << endl;
};
dij();
}
2. CF 915 E (Physical Education Lessons)
抽象题面:超大区间的状态覆盖与统计
在一个极大的范围(比如 10^9 长度)内,所有元素初始都是“状态 A”。我们需要频繁地把某一段区间全部覆盖为“状态 B”,或者恢复为“状态 A”,并随时查询当前“状态 A”的总数量。
核心思路:
- 按需开点:因为范围太大,直接开满数组内存会爆,所以必须用动态开点,只有修改到的地方才分配内存。
- 默认状态剪枝(防内存爆炸的核心):因为初始全是“状态 A”,如果遇到一个操作是“把某区间设为状态 A”,且发现线段树上这个区间的节点还没被创建出来,那就说明这地方本来就是默认的“状态 A”。此时直接
return停止递归,绝对不要开新点,这样能省下大量无意义的内存消耗。
点击查看代码
struct Tag {
int val = -1; // -1:无操作, 0:设为默认, 1:设为特殊
// 是否有有效标记
bool active() const {
return val != -1;
}
// 标记合并:父节点的 Tag 覆盖/合并到子节点的 Tag
void apply(const Tag &t) {
if (t.active()) val = t.val;
}
// 【核心优化】判断是否为默认操作(即该操作不会改变默认状态的节点)
// 用于剪枝:如果节点不存在,且操作是默认操作,就不需要新建节点
bool is_default_modification() const {
return val == 0;
}
};
struct Info {
int cnt = 0;
// 将 Tag 应用到 Info 上(更新数值)
void apply(const Tag &t, int l, int r) {
if (t.active()) cnt = t.val * (r - l);
}
};
Info operator+(const Info &a, const Info &b) {
return {a.cnt + b.cnt};
}
template <class Info, class Tag> struct DynamicSegTree {
struct Node {
int ls = 0; // 左孩子下标
int rs = 0; // 右孩子下标
Info info;
Tag tag;
};
int n;
vector<Node> nodes;
DynamicSegTree(int _n) : n(_n) {
nodes.reserve(12000000); // 预分配防抖动
nodes.emplace_back(); // 0: dummy node
}
int newNode() {
nodes.emplace_back();
return (int)nodes.size() - 1;
}
// --- 核心辅助函数 ---
// 对节点 p 应用 Tag (更新 info 和 tag)
void apply(int p, int l, int r, const Tag &v) {
if (!p) return;
nodes[p].info.apply(v, l, r);
nodes[p].tag.apply(v);
}
// 下放标记 push
// 注意:在动态开点中,push 意味着如果子节点不存在,必须创建出来接盘
void push(int p, int l, int r) {
if (!nodes[p].tag.active()) return;
int mid = l + (r - l) / 2;
Tag &tag = nodes[p].tag; // 取引用,后续清空
// 处理左孩子
if (!nodes[p].ls) {
int id = newNode(); // 先获取id
nodes[p].ls = id; // 再赋值,防止 vector 扩容导致 nodes[p] 引用失效
}
apply(nodes[p].ls, l, mid, tag);
// 处理右孩子
if (!nodes[p].rs) {
int id = newNode();
nodes[p].rs = id;
}
apply(nodes[p].rs, mid, r, tag);
// 清空当前节点标记
tag = Tag();
}
// 上拉更新 pull
void pull(int p) {
nodes[p].info =
(nodes[p].ls ? nodes[nodes[p].ls].info : Info()) + (nodes[p].rs ? nodes[nodes[p].rs].info : Info());
}
// --- 递归实现 (返回 int 下标) ---
int rangeApply(int p, int l, int r, int x, int y, const Tag &v) {
// 区间无交集
if (l >= y || r <= x) return p;
// 剪枝 2: 【关键优化】节点不存在,且当前操作是“无效操作”
if (!p && v.is_default_modification()) return 0;
// 如果节点不存在,必须新建(因为有操作要发生)
if (!p) p = newNode();
// 情况 1: 完全覆盖
if (l >= x && r <= y) {
apply(p, l, r, v);
return p;
}
// 情况 2: 部分覆盖 -> 需要下放
push(p, l, r);
int mid = l + (r - l) / 2;
// 递归左右,并更新左右孩子指针
int new_ls = rangeApply(nodes[p].ls, l, mid, x, y, v);
nodes[p].ls = new_ls;
int new_rs = rangeApply(nodes[p].rs, mid, r, x, y, v);
nodes[p].rs = new_rs;
pull(p);
return p;
}
Info rangeQuery(int p, int l, int r, int x, int y) {
if (!p || l >= y || r <= x) return Info();
if (l >= x && r <= y) return nodes[p].info;
push(p, l, r);
int mid = l + (r - l) / 2;
return rangeQuery(nodes[p].ls, l, mid, x, y) + rangeQuery(nodes[p].rs, mid, r, x, y);
}
// Find First
template <class F> int findFirst(int p, int l, int r, int x, int y, F &&pred) {
if (!p || l >= y || r <= x) return -1;
if (l >= x && r <= y && !pred(nodes[p].info)) return -1;
if (r - l == 1) return l;
push(p, l, r);
int mid = l + (r - l) / 2;
int res = findFirst(nodes[p].ls, l, mid, x, y, pred);
if (res == -1) res = findFirst(nodes[p].rs, mid, r, x, y, pred);
return res;
}
// Find Last
template <class F> int findLast(int p, int l, int r, int x, int y, F &&pred) {
if (!p || l >= y || r <= x) return -1;
if (l >= x && r <= y && !pred(nodes[p].info)) return -1;
if (r - l == 1) return l;
push(p, l, r);
int mid = l + (r - l) / 2;
int res = findLast(nodes[p].rs, mid, r, x, y, pred);
if (res == -1) res = findLast(nodes[p].ls, l, mid, x, y, pred);
return res;
}
void rangeApply(int &root, int l, int r, const Tag &v) {
root = rangeApply(root, 0, n, l, r, v);
}
// 2. 单点修改 (封装为区间修改)
void modify(int &root, int p, const Tag &v) {
root = rangeApply(root, 0, n, p, p + 1, v);
}
// 3. 区间查询
Info rangeQuery(int root, int l, int r) {
return rangeQuery(root, 0, n, l, r);
}
// 4. Find First
template <class F> int findFirst(int root, int l, int r, F &&pred) {
return findFirst(root, 0, n, l, r, pred);
}
// 5. Find Last
template <class F> int findLast(int root, int l, int r, F &&pred) {
return findLast(root, 0, n, l, r, pred);
}
};
int main() {
// 关闭流同步,加速 cin/cout,防止 I/O 耗时过长
ios::sync_with_stdio(0);
cin.tie(0);
int n, q;
// 读入总天数 n 和操作数 q
if (!(cin >> n >> q)) return 0;
// 创建线段树实例,管理范围 [0, n)
DynamicSegTree<Info, Tag> seg(n + 1);
int root = 0;
while (q--) {
int l, r, k;
cin >> l >> r >> k;
// 构造操作 Tag
Tag t;
t.val = (k == 1 ? 1 : 0); // k=1设为非工作日(1), k=2设为工作日(0)
// 执行区间修改
// 题目输入是闭区间 [l, r],线段树一般用左闭右开 [l, r+1)
seg.rangeApply(root, l, r + 1, t);
// 输出剩余的工作日天数
// 总天数 n - 非工作日天数 (root节点的cnt)
cout << n - seg.nodes[root].info.cnt << "\n";
}
return 0;
3. Mr. Liang's Sequence Problem II
抽象题面:大范围区间的数值加法与求和
维护一个极大的数组(值域很大),支持两种操作:把某一段区间内的所有数字统一加上一个值,以及查询某一段区间的数字总和。
核心思路:
- 带长度的标记下放 (Push-down):这题虽然也是动态开点,但维护的是加法和求和。在下放“加法懒标记 (Lazy Tag)”时,新增的总和等于“加上的值 × 区间的长度”。所以在代码实现中,节点更新时必须显式地把区间长度传进去计算,不能像普通的单点覆盖那样直接赋值。
- 防内存引用的“坑”:在动态建树向下递归开新点时,底层的
vector数组可能会因为空间不够而自动扩容。为了防止扩容导致原来的指针/引用找错位置,必须老老实实地“先拿到新开点的编号,再把它赋值给父节点的左右儿子”,确保内存安全。
点击查看代码
using u64 = unsigned long long;
struct Tag {
u64 add = 0;
// 标记合并:数值叠加
void apply(const Tag &t) {
add += t.add;
}
// 判断标记是否为空(用于剪枝)
bool empty() const {
return add == 0;
}
};
struct Info {
u64 sum = 0;
// 【关键修正】:必须传入区间长度 len
void apply(const Tag &t, u64 len) {
sum += t.add * len;
}
};
// Info 合并
Info operator+(const Info &a, const Info &b) {
return {a.sum + b.sum};
}
template<class Info, class Tag>
struct DynamicLazySegmentTree {
struct Node {
int ls = 0;
int rs = 0;
Info info;
Tag tag;
};
u64 n;
std::vector<Node> nodes;
// 预估空间防止扩容慢
DynamicLazySegmentTree(u64 n_) : n(n_) {
nodes.push_back(Node{});
}
int newNode() {
nodes.emplace_back();
return (int)nodes.size() - 1;
}
void pull(int p) {
// 必须用 nodes[...] 访问,防止引用失效
const Info &lhs = nodes[p].ls ? nodes[nodes[p].ls].info : Info();
const Info &rhs = nodes[p].rs ? nodes[nodes[p].rs].info : Info();
nodes[p].info = lhs + rhs;
}
// 【关键修正】:apply 接收 l, r
void apply(int p, u64 l, u64 r, const Tag &v) {
nodes[p].info.apply(v, r - l); // 传入长度
nodes[p].tag.apply(v);
}
// 【关键修正】:push 接收 l, r
void push(int p, u64 l, u64 r) {
// 剪枝:如果没有标记,坚决不开点!
if (nodes[p].tag.empty()) return;
if (!nodes[p].ls) nodes[p].ls = newNode();
if (!nodes[p].rs) nodes[p].rs = newNode();
u64 mid = l + (r - l) / 2;
// 拷贝 tag 防止引用失效
Tag t = nodes[p].tag;
apply(nodes[p].ls, l, mid, t);
apply(nodes[p].rs, mid, r, t);
nodes[p].tag = Tag(); // 清空
}
// 区间修改
int rangeApply(int p, u64 l, u64 r, u64 x, u64 y, const Tag &v) {
if (l >= y || r <= x) return p;
if (!p) p = newNode();
if (l >= x && r <= y) {
apply(p, l, r, v);
return p;
}
u64 m = l + (r - l) / 2;
push(p, l, r);
int new_ls = rangeApply(nodes[p].ls, l, m, x, y, v);
nodes[p].ls = new_ls;
int new_rs = rangeApply(nodes[p].rs, m, r, x, y, v);
nodes[p].rs = new_rs;
pull(p);
return p;
}
// 区间查询
Info rangeQuery(int p, u64 l, u64 r, u64 x, u64 y) {
if (!p || l >= y || r <= x) return Info();
if (l >= x && r <= y) return nodes[p].info;
u64 m = l + (r - l) / 2;
push(p, l, r);
return rangeQuery(nodes[p].ls, l, m, x, y) + rangeQuery(nodes[p].rs, m, r, x, y);
}
// 外部接口
void rangeApply(int &root, u64 l, u64 r, const Tag &v) {
root = rangeApply(root, 0, n, l, r, v);
}
Info rangeQuery(int root, u64 l, u64 r) {
return rangeQuery(root, 0, n, l, r);
}
};
void solve() {
u64 n;
int q;
cin >> n >> q;
// 实例化线段树
DynamicLazySegmentTree<Info, Tag> seg(n + 1); // 这里的 n 只是范围上限
int root = 0;
while (q--) {
int op;
cin >> op;
if (op == 1) {
u64 l, r, v;
cin >> l >> r >> v;
// 题目输入是闭区间 [l, r],代码习惯用左闭右开 [l, r+1)
seg.rangeApply(root, l, r + 1, {v});
} else {
u64 l, r;
cin >> l >> r;
cout << seg.rangeQuery(root, l, r + 1).sum << "\n";
}
}
}

浙公网安备 33010602011771号