线段树
如何快速求出一个序列的区间和?可以使用前缀和。如何快速求出一个序列的最值?可以使用 ST 表。这两种数据结构在建立的时候颇费功夫,但使用的时候效率很高。如果再增加一个需求:需要时不时修改序列的值,那么这两种数据结构就无法高效完成了。线段树可以用来解决这类问题。
线段树是一种特殊的二叉树,它可以将一个线性的序列组织成一个树状的结构,从而可以在对数时间复杂度下访问序列上的任意一个区间并进行维护。
线段树的建立与操作
例题:P3372【模板】线段树 1
已知一个数列
,需要支持两种操作:
1.将区间内每一个数加上 ;
2.求出某区间中每一个数的和。
数的个数和操作次数不超过, 和变化后的数列数字的绝对值不超过 。
分析:线段树的思想在于将序列中若干个区间在树上用节点表示,其中
对长度为
- 对于线段树上的任意一个结点,它要么没有子结点,要么有两个子结点,不存在只有一个子结点的情况。
- 对于一个长度为
的序列,它所建立的线段树只有 个结点。 - 对于一个长度为
的序列,它所建立的线段树高为 。
对于第二条性质,考虑首先线段树有且仅有
对于第三条性质,考虑对于任意一个表示
线段树中一个结点上可以维护若干个所需要的信息,在访问时,将若干个结点的信息合并,就能得到任意所需区间的信息。例如,在上图中,如果希望获得区间
例如,如果要求区间和,区间
将根结点定义为
1. 建立线段树
树是递归定义的,因此可以用递归的方式建立线段树:如果这个区间左端点等于右端点,说明是叶子结点,其数据的值赋值为对应数列元素的值;否则将这个区间分为左右两部分,分别递归建立线段树,然后将左右两个区间的数据进行汇总(pushup)处理。
假设初始数列是
建立线段树的代码如下:
#define LC (cur*2) #define RC (cur*2+1) typedef long long LL; const int MAXN = 500005; struct Node { int l, r; // 某个结点所代表的区间 LL value; // value存储结点对应的区间和 }; LL a[MAXN]; Node tree[MAXN*4]; void pushup(int cur) { // 2*cur是左子结点,2*cur+1是右子结点 tree[cur].value = tree[LC].value + tree[RC].value; } void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; if (l == r) { // 到达叶子结点 tree[cur].value = a[l]; return; } int mid = (l + r) / 2; // 将区间分成[l,mid]和[mid+1,r] build(LC, l, mid); build(RC, mid+1, r); // 递归构建子树 pushup(cur); // 由子区间的区间和更新当前区间的和 }
在上面的代码中,cur
表示当前线段树结点的编号,成员变量 value
是结点维护的信息,也就是区间和。如果已经达到了叶结点,那么区间和显然就是对应位置的和,直接赋值即可;否则递归构建左右子树,然后通过 pushup
函数,将左右子树所维护的区间和进行合并。不难发现,每调用一次 build
,就新建了一个线段树结点,因此 build
函数的时间复杂度为
2. 单点查询与修改
如何精确定位到叶子结点呢?假设需要定位到 pushup
,来保证线段树信息的正确性。
例如将数列第
单点查询和单点修改的代码如下:
LL query1(int cur, int p) { if (tree[cur].l == tree[cur].r) // 到达叶结点即可返回 return tree[cur].value; int mid = (tree[cur].l + tree[cur].r) / 2; if (p <= mid) return query1(LC, p); // 如果查询的位置在左子树内,就递归查询左子树 else return query1(RC, p); // 反之查询右子树 // 因为查询没有对区间和进行修改,因此不需要pushup } void update1(int cur, int p, LL x) { // 假设这里的更新操作是单点+x if (tree[cur].l == tree[cur].r) { // 到达叶结点则直接更新 tree[cur].value += x; return; } int mid = (tree[cur].l + tree[cur].r) / 2; if (p <= mid) update1(LC, p, x); // 若修改的位置p在左子树内,递归修改左子树 else update1(RC, p, x); // 反之修改右子树 pushup(cur); // 别忘记更新以后需要修改当前结点的区间和 }
在上面的代码中,可以发现每递归调用一次函数,都会在线段树上向下移动一层。因为线段树的树高是
3. 区间查询
只能支持单点操作的线段树是没什么意义的,这里我们需要用线段树快速维护区间信息,即给定区间
从根开始递归,如果当前结点所代表的区间被所询问的区间
例如,查询
区间查询的代码如下:
LL query(int cur, int l, int r) { // 区间查询 if (tree[cur].l >= l && tree[cur].r <= r) { // 如果完全包含则直接返回区间和信息 return tree[cur].value; } int mid = (tree[cur].l + tree[cur].r) / 2; LL res = 0; if (mid >= l) res += query(LC, l, r); // 查询区间与左子树区间相交 if (mid < r) res += query(RC, l, r); // 查询区间与右子树区间相交 return res; }
在 query
函数里,并不是每层只会向下延伸一个结点,而是对左右子结点分别递归。那么如何分析其复杂度呢?在线段树每层的递归中,最多只有两个结点会向下继续递归,也就是被查询区间两端点所在的结点。而剩下的结点要么是被完全包含,要么是与查询区间不相交。因此,每一层只会新建
4. 区间修改
在区间修改时,显然不能暴力地修改每个叶子,那样效率很低。为此,引入延迟标记(又称为懒标记或者 lazy-tag),记录一些区间修改的信息。当递归至一个被完全包含的区间时,在这个区间上打一个延迟标记,记录这个区间中的每个数都需要被加上某个数,然后直接修改该结点的区间和并返回,不再向下递归。当新访问到一个结点时,先将延迟标记下放到子结点,然后再进行递归。
可以发现,这样做可以保证与根相连的某个连通块的信息总是正确的,并且在调用时总能得到正确的信息。同时,因为被完全包含和不相交的情况都不会再递归,所以其时间复杂度为
struct Node { int l, r; // 某个结点所代表的区间 LL value, tag; // value存储结点对应的区间和 // tag是区间加的延迟标记 };
假设初始数列是
此时如果查询
可以看到,对于打了延迟标记的结点,其维护的区间和是已经修改完成的信息,其子结点的值还没有被修改。也就是说,延迟标记起到的作用是记录子结点的每个数应该加上多少,而不是该结点本身的信息。代码如下,注意在查询时也要包含下放标记的过程:
void work(int cur, LL delta) { int len = tree[cur].r - tree[cur].l + 1; tree[cur].value += delta * len; // 修改当前结点的区间和 tree[cur].tag += delta; // 修改当前结点的延迟标记 } void pushdown(int cur) { if (tree[cur].tag != 0) { work(LC, tree[cur].tag); // 下放标记给左子树 work(RC, tree[cur].tag); // 下放标记给右子树 tree[cur].tag = 0; // 因为标记信息已经传到下一层结点了,当前层清空标记 } } void update(int cur, int l, int r, LL delta) { if (tree[cur].l >= l && tree[cur].r <= r) { work(cur, delta); // 完全包含则直接打标记即可 return; } pushdown(cur); // 注意必须先将当前结点的标记下传,才能递归修改下面的结点 int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= l) update(LC, l, r, delta); if (mid < r) update(RC, l, r, delta); pushup(cur); } LL query(int cur, int l, int r) { // 区间查询 if (tree[cur].l >= l && tree[cur].r <= r) { // 如果完全包含则直接返回区间和 return tree[cur].value; } pushdown(cur); // 查询的时候也需要将结点标记下传 int mid = (tree[cur].l + tree[cur].r) / 2; LL res = 0; // 若与左/右子结点区间有相交,则需递归处理 if (mid >= l) res += query(LC, l, r); if (mid < r) res += query(RC, l, r); return res; }
上面的代码中,pushdown
函数是将延迟标记下传的过程,work
函数是更新结点信息的过程。成员变量 tag
记录的是当前结点应该加的值的大小,那么该区间的区间和需要增加的值就是长度乘上增加量。需要注意的是,在将标记下传后,应该清空当前结点的延迟标记。并且必须要先判断区间之间的完全包含关系,这样就会保证叶结点不会再 pushdown
,否则一旦在叶结点 pushdown
,可能会造成数组越界。
本题的完整代码如下:
#include <cstdio> #define LC (cur*2) #define RC (cur*2+1) typedef long long LL; const int MAXN = 100005; struct Node { int l, r; // 某个结点所代表的区间 LL value, tag; // value存储结点对应的区间和 // tag是区间加的延迟标记 }; LL a[MAXN]; Node tree[MAXN*4]; void pushup(int cur) { // 2*cur是左子结点,2*cur+1是右子结点 tree[cur].value = tree[LC].value + tree[RC].value; } void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; if (l == r) { // 到达叶子结点 tree[cur].value = a[l]; return; } int mid = (l + r) / 2; // 将区间分成[l,mid]和[mid+1,r] build(LC, l, mid); build(RC, mid+1, r); // 递归构建子树 pushup(cur); // 由子区间的区间和更新当前区间的和 } void work(int cur, LL delta) { int len = tree[cur].r - tree[cur].l + 1; tree[cur].value += delta * len; // 修改当前结点的区间和 tree[cur].tag += delta; // 修改当前结点的延迟标记 } void pushdown(int cur) { if (tree[cur].tag != 0) { work(LC, tree[cur].tag); // 下放标记给左子树 work(RC, tree[cur].tag); // 下放标记给右子树 tree[cur].tag = 0; // 因为标记信息已经传到下一层结点了,当前层清空标记 } } void update(int cur, int l, int r, LL delta) { if (tree[cur].l >= l && tree[cur].r <= r) { work(cur, delta); // 完全包含则直接打标记即可 return; } pushdown(cur); // 注意必须先将当前结点的标记下传,才能递归修改下面的结点 int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= l) update(LC, l, r, delta); if (mid < r) update(RC, l, r, delta); pushup(cur); } LL query(int cur, int l, int r) { // 区间查询 if (tree[cur].l >= l && tree[cur].r <= r) { // 如果完全包含则直接返回区间和 return tree[cur].value; } pushdown(cur); // 查询的时候也需要将结点标记下传 int mid = (tree[cur].l + tree[cur].r) / 2; LL res = 0; // 若与左/右子结点区间有相交,则需递归处理 if (mid >= l) res += query(LC, l, r); if (mid < r) res += query(RC, l, r); return res; } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%lld", &a[i]); build(1, 1, n); while (m--) { int op; scanf("%d", &op); if (op == 1) { int x, y; LL k; scanf("%d%d%lld", &x, &y, &k); update(1, x, y, k); } else { int x, y; scanf("%d%d", &x, &y); printf("%lld\n", query(1, x, y)); } } return 0; }
线段树的应用
例题:P3870 [TJOI2009] 开关
给定一个初始为
的长度为 的数列,进行 次操作,要求支持两种操作:
1.给区间的所有数字对 取异或。
2.求区间内 的个数。
数据范围:
分析:不难发现,要求的“区间内
#include <cstdio> #define LC (2 * cur) #define RC (2 * cur + 1) const int N = 1e5 + 5; struct Node { int l, r, cnt, tag; }; Node tree[N * 4]; void pushup(int cur) { tree[cur].cnt = tree[LC].cnt + tree[RC].cnt; } void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; if (l == r) return; int mid = (l + r) / 2; build(LC, l, mid); build(RC, mid + 1, r); } void work(int cur) { tree[cur].cnt = tree[cur].r - tree[cur].l + 1 - tree[cur].cnt; tree[cur].tag ^= 1; } void pushdown(int cur) { if (tree[cur].tag) { work(LC); work(RC); } tree[cur].tag = 0; } void update(int cur, int l, int r) { if (tree[cur].l >= l && tree[cur].r <= r) { work(cur); return; } pushdown(cur); int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= l) update(LC, l, r); if (mid + 1 <= r) update(RC, l, r); pushup(cur); } int query(int cur, int l, int r) { if (tree[cur].l >= l && tree[cur].r <= r) return tree[cur].cnt; pushdown(cur); int mid = (tree[cur].l + tree[cur].r) / 2; int res = 0; if (mid >= l) res += query(LC, l, r); if (mid + 1 <= r) res += query(RC, l, r); return res; } int main() { int n, m; scanf("%d%d", &n, &m); build(1, 1, n); while (m--) { int c, a, b; scanf("%d%d%d", &c, &a, &b); if (c == 0) update(1, a, b); else printf("%d\n", query(1, a, b)); } return 0; }
例题:P1438 无聊的数列
维护一个长度为
的数列 。要求支持 此操作,操作有两种类型:
1.1 l r k d
:给出一个长度等于的等差数列,首项为 ,公差为 ,并将它对应加到 范围中的每一个数上。即:令 。
2.2 p
:询问序列的第个数的值 。
数据范围:
分析:这是一个区间加等差数列的问题。考虑等差数列有两个要素:首项
例如,将数列
于是使用两个延迟标记,分别表示首项和公差即可。
#include <cstdio> #define LC (2 * cur) #define RC (2 * cur + 1) typedef long long LL; const int N = 1e5 + 5; int a[N]; struct Node { int l, r; LL val, k, d; }; Node tree[N * 4]; void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; if (l == r) { tree[cur].val = a[l]; return; } int mid = (l + r) / 2; build(LC, l, mid); build(RC, mid + 1, r); } void work(int cur, LL k, LL d) { tree[cur].k += k; tree[cur].d += d; tree[cur].val += k; } void pushdown(int cur) { if (tree[cur].k != 0 || tree[cur].d != 0) { int mid = (tree[cur].l + tree[cur].r) / 2; work(LC, tree[cur].k, tree[cur].d); work(RC, tree[cur].k + (mid + 1 - tree[cur].l) * tree[cur].d, tree[cur].d); tree[cur].k = tree[cur].d = 0; } } void update(int cur, int l, int r, LL k, LL d) { if (tree[cur].l >= l && tree[cur].r <= r) { work(cur, k + d * (tree[cur].l - l), d); return; } pushdown(cur); int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= l) update(LC, l, r, k, d); if (mid + 1 <= r) update(RC, l, r, k, d); } LL query(int cur, int p) { if (tree[cur].l == tree[cur].r) return tree[cur].val; pushdown(cur); int mid = (tree[cur].l + tree[cur].r) / 2; if (p <= mid) return query(LC, p); else return query(RC, p); } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); build(1, 1, n); while (m--) { int opt; scanf("%d", &opt); if (opt == 1) { int l, r, k, d; scanf("%d%d%d%d", &l, &r, &k, &d); update(1, l, r, k, d); } else { int p; scanf("%d", &p); printf("%lld\n", query(1, p)); } } return 0; }
例题:P1253 扶苏的问题
给定一个长度为
的序列 ,要求支持 次操作,共有三种类型的操作:
1.给定区间,将区间内每个数都修改为 ;
2.给定区间,将区间内每个数都加上 ;
3.给定区间,求区间内的最大值。
数据范围:。
分析:本题中所求的“区间最大值”也可以使用线段树维护:父结点的区间最大值就是它两个子结点的区间最大值中较大的一个。
对于修改操作,可以使用两个延迟标记,一个表示区间赋值为
需要注意的是,因为操作时可能赋值为
#include <cstdio> #include <algorithm> #define LC (cur*2) #define RC (cur*2+1) using namespace std; typedef long long LL; const int MAXN = 1000005; const LL INF = 1e16; struct Node { int l, r; // value为区间最大值,add为区间加法标记,cover为区间赋值标记 LL value, add, cover; }; LL a[MAXN]; Node tree[MAXN*4]; void pushup(int cur) { tree[cur].value = max(tree[LC].value, tree[RC].value); } void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; tree[cur].cover = INF; // 注意cover标记的初始化 if (l == r) { tree[cur].value = a[l]; return; } int mid = (l + r) / 2; build(LC, l, mid); build(RC, mid+1, r); pushup(cur); } void work(int cur, LL x, int op) { // op表示操作类型 if (op == 1) { // 区间赋值 tree[cur].value = tree[cur].cover = x; tree[cur].add = 0; } else { // 区间加法 tree[cur].value += x; if (tree[cur].cover != INF) tree[cur].cover += x; else tree[cur].add += x; } } void pushdown(int cur) { if (tree[cur].cover != INF) { work(LC, tree[cur].cover, 1); work(RC, tree[cur].cover, 1); tree[cur].cover = INF; // 清空cover标记 } if (tree[cur].add != 0) { work(LC, tree[cur].add, 2); work(RC, tree[cur].add, 2); tree[cur].add = 0; // 清空add标记 } } void update(int cur, int l, int r, LL delta, int op) { if (tree[cur].l >= l && tree[cur].r <= r) { work(cur, delta, op); return; } pushdown(cur); int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= l) update(LC, l, r, delta, op); if (mid < r) update(RC, l, r, delta, op); pushup(cur); } LL query(int cur, int l, int r) { // 区间查询 // 全包含 if (tree[cur].l >= l && tree[cur].r <= r) { return tree[cur].value; } pushdown(cur); LL res = -INF; int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= l) res = max(res, query(LC, l, r)); if (mid < r) res = max(res, query(RC, l, r)); return res; } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%lld", &a[i]); build(1, 1, n); while (m--) { int op; scanf("%d", &op); if (op < 3) { int x, y; LL k; scanf("%d%d%lld", &x, &y, &k); update(1, x, y, k, op); } else { int x, y; scanf("%d%d", &x, &y); printf("%lld\n", query(1, x, y)); } } return 0; }
P3373【模板】线段树 2
给定一个长度为
的数列,需要进行下面三种操作:
1.将区间内每个数乘上;
2.将区间内每个数加上;
3.求数列的区间和,答案对一个大数取模。
分析:这是一个多标记线段树的题。本题中一共出现了两种修改,分别为加法和乘法,考虑用两个标记分别维护它们。
设
因此,标记的下传顺序十分关键:在 pushdown 时,必须先下传乘法标记,再下传加法标记。因为乘法标记在下传时,需要让子结点的加法标记也乘上当前结点的乘法标记值,如果先下传加法标记,会让下传的那部分加法标记再乘上乘法标记,而事实上这部分标记已经在原结点乘过了,因此会计算错误。
#include <cstdio> #define LC (cur*2) #define RC (cur*2+1) typedef long long LL; const int MAXN = 500005; struct Node { int l, r; LL value, add, mul; }; LL a[MAXN], m; Node tree[MAXN*4]; void pushup(int cur) { tree[cur].value = (tree[LC].value + tree[RC].value) % m; } void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; tree[cur].mul = 1; if (l == r) { tree[cur].value = a[l] % m; return; } int mid = (l + r) / 2; build(LC, l, mid); build(RC, mid+1, r); pushup(cur); } void work(int cur, LL x, int op) { //对cur这个节点进行具体的更新操作 if (op == 1) { tree[cur].value *= x; tree[cur].value %= m; tree[cur].mul *= x; tree[cur].mul %= m; tree[cur].add *= x; tree[cur].add %= m; } else { tree[cur].value += x * (tree[cur].r-tree[cur].l+1) % m; tree[cur].value %= m; tree[cur].add += x; tree[cur].add %= m; } } void pushdown(int cur) { if (tree[cur].mul != 1) { work(LC, tree[cur].mul, 1); work(RC, tree[cur].mul, 1); tree[cur].mul = 1; } if (tree[cur].add != 0) { work(LC, tree[cur].add, 2); work(RC, tree[cur].add, 2); tree[cur].add = 0; } } void update(int cur, int l, int r, LL x, int op) { if (tree[cur].l >= l && tree[cur].r <= r) { work(cur, x, op); return; } pushdown(cur); int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= l) update(LC, l, r, x, op); if (mid < r) update(RC, l, r, x, op); pushup(cur); } LL query(int cur, int l, int r) { // 区间查询 if (tree[cur].l >= l && tree[cur].r <= r) { // 完全包含 return tree[cur].value; } pushdown(cur); int mid = (tree[cur].l + tree[cur].r) / 2; LL res = 0; if (mid >= l) { res += query(LC, l, r); res %= m; } if (mid < r) { res += query(RC, l, r); res %= m; } return res; } int main() { int n, q; scanf("%d%d%lld", &n, &q, &m); for (int i = 1; i <= n; i++) scanf("%lld", &a[i]); build(1, 1, n); while (q--) { int op; scanf("%d", &op); if (op < 3) { int x, y; LL k; scanf("%d%d%lld", &x, &y, &k); update(1, x, y, k, op); } else { int x, y; scanf("%d%d", &x, &y); printf("%lld\n", query(1, x, y)); } } return 0; }
习题:CF558E A Simple Task
解题思路
使用计数排序的思想。对于每一个询问,查询每种字母的在区间内的个数,使用计数排序的方式来更新区间信息。
构建
时间复杂度为
参考代码
#include <cstdio> #define LC (2 * cur) #define RC (2 * cur + 1) const int N = 1e5 + 5; char s[N], ans[N]; int cnt[26]; struct Node { int l, r, cnt, cover; }; Node tree[26][N * 4]; void pushup(int idx, int cur) { tree[idx][cur].cnt = tree[idx][LC].cnt + tree[idx][RC].cnt; } void build(int idx, int cur, int l, int r) { tree[idx][cur].l = l; tree[idx][cur].r = r; tree[idx][cur].cover = -1; if (l == r) return; int mid = (l + r) / 2; build(idx, LC, l, mid); build(idx, RC, mid + 1, r); pushup(idx, cur); } void work(int idx, int cur, int val) { tree[idx][cur].cover = val; if (tree[idx][cur].cover == 1) tree[idx][cur].cnt = tree[idx][cur].r - tree[idx][cur].l + 1; else tree[idx][cur].cnt = 0; } void pushdown(int idx, int cur) { if (tree[idx][cur].cover != -1) { work(idx, LC, tree[idx][cur].cover); work(idx, RC, tree[idx][cur].cover); tree[idx][cur].cover = -1; } } void update(int idx, int cur, int l, int r, int val) { if (tree[idx][cur].l >= l && tree[idx][cur].r <= r) { work(idx, cur, val); return; } pushdown(idx, cur); int mid = (tree[idx][cur].l + tree[idx][cur].r) / 2; if (mid >= l) update(idx, LC, l, r, val); if (mid + 1 <= r) update(idx, RC, l, r, val); pushup(idx, cur); } int query(int idx, int cur, int l, int r) { if (tree[idx][cur].l >= l && tree[idx][cur].r <= r) return tree[idx][cur].cnt; pushdown(idx, cur); int mid = (tree[idx][cur].l + tree[idx][cur].r) / 2; int res = 0; if (mid >= l) res += query(idx, LC, l, r); if (mid + 1 <= r) res += query(idx, RC, l, r); return res; } int main() { int n, q; scanf("%d%d%s", &n, &q, s + 1); for (int i = 0; i < 26; i++) build(i, 1, 1, n); for (int i = 1; i <= n; i++) { update(s[i] - 'a', 1, i, i, 1); } while (q--) { int l, r, k; scanf("%d%d%d", &l, &r, &k); for (int i = 0; i < 26; i++) { cnt[i] = query(i, 1, l, r); update(i, 1, l, r, 0); } int cur = k == 1 ? l : r; for (int i = 0; i < 26; i++) { if (cnt[i] == 0) continue; if (k == 1) { update(i, 1, cur, cur + cnt[i] - 1, 1); cur += cnt[i]; } else { update(i, 1, cur - cnt[i] + 1, cur, 1); cur -= cnt[i]; } } } for (int i = 0; i < 26; i++) { for (int j = 1; j <= n; j++) if (query(i, 1, j, j) == 1) ans[j] = 'a' + i; } for (int i = 1; i <= n; i++) printf("%c", ans[i]); printf("\n"); return 0; }
对于要求支持区间查询的线段树,其结点上所维护的信息必须具有可合并性。也就是说,从某个结点的两个子结点的信息通过汇总操作可以得出该结点的信息。但有时所求的信息如果直接维护并不具有可合并性,这时可能需要维护一些额外的信息,从而使得子结点信息可以合并推出父结点信息。
例题:P4513 小白逛公园
给定一个长度为
的数列 ,有 次操作,每次操作要么对 进行单点修改,要么查询数列 的最大子段和是多少。区间 的连续和是指 。最大子段和指的是所有的区间连续和中最大的值。
数据范围:。
分析: 考虑对序列
进一步地,考虑父结点的最大子段和只可能存在三种情况:是左子结点的最大子段和,是右子结点的最大子段和,是左子结点和右子结点的两段相邻的和拼起来。对于前两种情况很容易转移,现在考虑第三种情况。
在这种情况下,左子结点被拼起来的那一段区间必须包含左子结点的右端点,换句话说,它是以左子结点右端点为起点向左找的最大连续和,称之为最大后缀和;同理,右子结点被拼起来的那一段区间是以右子结点左端点为起点向右找的最大连续和,称为最大前缀和。这样只需维护结点的最大前缀和、最大后缀和以及最大子段和,就可以合并出父结点的最大子段和了。
进一步考虑如何合并出父结点的最大前缀和以及最大后缀和:对于父结点的最大前缀和,要么直接就是左子结点的最大前缀和,要么是左子结点的全体拼上右子结点的最大前缀和;最大后缀和的维护同理。
因此,只需要再维护一个区间和,就可以完成对最大前缀和、最大后缀和的维护了。显然区间和的维护直接合并两个子结点的区间和即可。
#include <cstdio> #include <algorithm> #define LC (cur * 2) #define RC (cur * 2 + 1) using namespace std; const int N = 500005; struct Node { // sum是区间和,res是最大子段和 // lsum是最大前缀和,rsum是最大后缀和 int l, r, sum, res, lsum, rsum; }; Node tree[N * 4]; int a[N]; void pushup(Node & cur, const Node & lc, const Node & rc) { cur.sum = lc.sum + rc.sum; cur.res = max(lc.rsum + rc.lsum, max(lc.res, rc.res)); cur.lsum = max(lc.lsum, lc.sum + rc.lsum); cur.rsum = max(rc.rsum, lc.rsum + rc.sum); } void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; if (l == r) { tree[cur].sum = tree[cur].res = tree[cur].lsum = tree[cur].rsum = a[l]; return; } int mid = (l + r) / 2; build(LC, l, mid); build(RC, mid + 1, r); pushup(tree[cur], tree[LC], tree[RC]); } Node query(int cur, int l, int r) { if (tree[cur].l >= l && tree[cur].r <= r) return tree[cur]; int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= r) return query(LC, l, r); else if (mid < l) return query(RC, l, r); else { Node res, resl = query(LC, l, r), resr = query(RC, l, r); pushup(res, resl, resr); return res; } } void update(int cur, int p, int s) { if (tree[cur].l == tree[cur].r && tree[cur].l == p) { tree[cur].sum = tree[cur].res = tree[cur].lsum = tree[cur].rsum = s; return; } int mid = (tree[cur].l + tree[cur].r) / 2; if (mid >= p) update(LC, p, s); else update(RC, p, s); pushup(tree[cur], tree[LC], tree[RC]); } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); } build(1, 1, n); while (m--) { int k, x, y; scanf("%d%d%d", &k, &x, &y); if (k == 1) { if (x > y) swap(x, y); printf("%d\n", query(1, x, y).res); } else { update(1, x, y); } } return 0; }
习题:P2894 [USACO08FEB] Hotel G
解题思路
维护每一段区间内的最大连续空房数,但是只维护这一个值是不够的,因为光从两个子区间的最大连续空房数中取最大值是不够的,也不能将两者直接相加,因为两个子区间里的最长连续空房不一定是挨着的。实际上除了从两个子区间的最大连续空房数中取最大值外,也有可能整个区间中的最长连续空房是横跨左右两个区间的,因此还需要维护区间内的前缀、后缀连续空房数量。因为还涉及区间更新操作,所以还需要一个延迟标记。
参考代码
#include <cstdio> #include <algorithm> #define LC (2 * u) #define RC (2 * u + 1) using std::max; const int N = 50005; struct Node { int l, r, len; int rest; // 区间内最长连续空房数 int pre, suf; // 前缀/后缀连续空房数 int flag; // 延迟标记 }; Node tree[N * 4]; void pushup(int u) { tree[u].rest = max(tree[LC].suf + tree[RC].pre, max(tree[LC].rest, tree[RC].rest)); tree[u].pre = tree[LC].pre + (tree[LC].pre == tree[LC].len ? tree[RC].pre : 0); tree[u].suf = tree[RC].suf + (tree[RC].suf == tree[RC].len ? tree[LC].suf : 0); } void work(int u, int flag) { tree[u].flag = flag; if (flag == 1) { // 入住 tree[u].rest = tree[u].pre = tree[u].suf = 0; } else { // 退房 tree[u].rest = tree[u].pre = tree[u].suf = tree[u].len; } } void pushdown(int u) { if (tree[u].flag != 0) { work(LC, tree[u].flag); work(RC, tree[u].flag); tree[u].flag = 0; } } void build(int u, int l, int r) { tree[u].l = l; tree[u].r = r; tree[u].len = r - l + 1; if (l == r) { tree[u].rest = tree[u].pre = tree[u].suf = 1; // 初始均为空房 return; } int mid = (l + r) / 2; build(LC, l, mid); build(RC, mid + 1, r); pushup(u); } int query(int u, int x) { if (tree[u].rest < x) return 0; if (tree[u].len == 1) return tree[u].l; pushdown(u); // 如果左区间有足够的入住房间,只需在左区间内查询 if (tree[LC].rest >= x) return query(LC, x); // 如果横跨左右区间能够提供足够的入住房间,则答案就是左子树区间后缀部分的起始位置 if (tree[LC].suf + tree[RC].pre >= x) return tree[LC].r - tree[LC].suf + 1; return query(RC, x); // 否则只能考虑右区间 } void update(int u, int l, int r, int val) { if (tree[u].l >= l && tree[u].r <= r) { work(u, val); return; } pushdown(u); int mid = (tree[u].l + tree[u].r) / 2; if (mid >= l) update(LC, l, r, val); if (mid + 1 <= r) update(RC, l, r, val); pushup(u); } int main() { int n, m; scanf("%d%d", &n, &m); build(1, 1, n); while (m--) { int i; scanf("%d", &i); if (i == 1) { int x; scanf("%d", &x); int q = query(1, x); printf("%d\n", q); if (q != 0) update(1, q, q + x - 1, 1); } else { int x, y; scanf("%d%d", &x, &y); y = x + y - 1; update(1, x, y, -1); } } return 0; }
习题:P6477 [NOI Online #2 提高组] 子序列问题
解题思路
对于这类区间信息求和的问题,我们往往可以枚举一个端点(比如右端点),在一个数据结构上维护另一个端点取每个值时,该区间的答案。
这里我们考虑枚举右端点
如果问的是
这样一来,问题就转化成了对一个序列支持两种操作:区间加
区间加
注意因为
参考代码
#include <cstdio> #include <vector> #include <algorithm> #define LC (u * 2) #define RC (u * 2 + 1) using std::vector; using std::lower_bound; using std::sort; using std::unique; const int MOD = 1000000007; const int N = 1e6 + 5; int a[N], last[N]; vector<int> data; int discretization(int x) { return lower_bound(data.begin(), data.end(), x) - data.begin() + 1; } struct Node { int l, r, len; int sqr, add, sum; }; Node tree[N * 4]; void pushup(int u) { tree[u].sqr = (tree[LC].sqr + tree[RC].sqr) % MOD; tree[u].sum = (tree[LC].sum + tree[RC].sum) % MOD; } void build(int u, int l, int r) { tree[u].l = l; tree[u].r = r; tree[u].len = r - l + 1; if (l == r) return; int mid = (l + r) / 2; build(LC, l, mid); build(RC, mid + 1, r); } void work(int u, int add) { tree[u].sqr += 1ll * add * add % MOD * tree[u].len % MOD; tree[u].sqr %= MOD; tree[u].sqr += 2ll * add * tree[u].sum % MOD; tree[u].sqr %= MOD; tree[u].sum += 1ll * tree[u].len * add % MOD; tree[u].sum %= MOD; tree[u].add += add; } void pushdown(int u) { if (tree[u].add != 0) { work(LC, tree[u].add); work(RC, tree[u].add); tree[u].add = 0; } } void update(int u, int l, int r) { if (tree[u].l >= l && tree[u].r <= r) { work(u, 1); return; } pushdown(u); int mid = (tree[u].l + tree[u].r) / 2; if (mid >= l) update(LC, l, r); if (mid + 1 <= r) update(RC, l, r); pushup(u); } int main() { int n; scanf("%d", &n); build(1, 1, n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); data.push_back(a[i]); } sort(data.begin(), data.end()); data.erase(unique(data.begin(), data.end()), data.end()); for (int i = 1; i <= n; i++) a[i] = discretization(a[i]); int ans = 0; for (int i = 1; i <= n; i++) { int pre = last[a[i]]; update(1, pre + 1, i); ans += tree[1].sqr; ans %= MOD; last[a[i]] = i; } printf("%d\n", ans); return 0; }
扫描线
例:P5490 【模板】扫描线
参考代码
#include <cstdio> #include <algorithm> using namespace std; typedef long long LL; const int MAXN = 100005; int y[MAXN * 2], ylen; struct Node { int l, r, len, cnt; }; Node tree[MAXN * 8]; struct Line { int x, y1, y2, flag; bool operator<(const Line& other) const { return x < other.x; } }; Line line[MAXN * 2]; void build(int cur, int l, int r) { tree[cur].l = l; tree[cur].r = r; if (l + 1 == r) return; int mid = (l + r) / 2; build(cur * 2, l, mid); build(cur * 2 + 1, mid, r); } void pushup(int cur) { if (tree[cur].cnt) tree[cur].len = y[tree[cur].r] - y[tree[cur].l]; else if (tree[cur].l + 1 == tree[cur].r) tree[cur].len = 0; else tree[cur].len = tree[cur * 2].len + tree[cur * 2 + 1].len; } void update(int cur, int l, int r, int d) { if (tree[cur].r <= l || tree[cur].l >= r) return; if (tree[cur].l >= l && tree[cur].r <= r) { tree[cur].cnt += d; pushup(cur); return; } update(cur * 2, l, r, d); update(cur * 2 + 1, l, r, d); pushup(cur); } int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); line[i] = {x1, y1, y2, 1}; line[n + i] = {x2, y1, y2, -1}; y[i] = y1; y[n + i] = y2; } n *= 2; sort(line + 1, line + n + 1); sort(y + 1, y + n + 1); ylen = unique(y + 1, y + n + 1) - y - 1; build(1, 1, ylen); LL ans = 0; for (int i = 1; i < n; i++) { int y1 = lower_bound(y + 1, y + ylen + 1, line[i].y1) - y; int y2 = lower_bound(y + 1, y + ylen + 1, line[i].y2) - y; update(1, y1, y2, line[i].flag); ans += 1ll * (line[i + 1].x - line[i].x) * tree[1].len; } printf("%lld\n", ans); return 0; }
例:P3875 [TJOI2010] 被污染的河流
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int MAXN = 10005; struct Line { int x, y1, y2, flag; bool operator<(const Line& other) const { return x < other.x; } } line[MAXN * 2]; int y[MAXN * 2], cnt; struct Node { int l, r, len, cnt; } tree[MAXN * 8]; void pushup(int cur) { if (tree[cur].cnt) tree[cur].len = y[tree[cur].r] - y[tree[cur].l]; else if (tree[cur].l + 1 == tree[cur].r) tree[cur].len = 0; else tree[cur].len = tree[cur * 2].len + tree[cur * 2 + 1].len; } void build(int cur, int l, int r) { tree[cur] = {l, r, 0, 0}; if (l + 1 == r) return; int mid = (l + r) / 2; build(cur * 2, l, mid); build(cur * 2 + 1, mid, r); } void update(int cur, int l, int r, int d) { if (tree[cur].l >= r || tree[cur].r <= l) return; if (tree[cur].l >= l && tree[cur].r <= r) { tree[cur].cnt += d; pushup(cur); return; } update(cur * 2, l, r, d); update(cur * 2 + 1, l, r, d); pushup(cur); } int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); if (x1 == x2) { line[i] = {x1 - 1, min(y1, y2), max(y1, y2), 1}; line[i + n] = {x1 + 1, min(y1, y2), max(y1, y2), -1}; y[i] = min(y1, y2); y[i + n] = max(y1, y2); } else { line[i] = {min(x1, x2), y1 - 1, y1 + 1, 1}; line[i + n] = {max(x1, x2), y1 - 1, y1 + 1, -1}; y[i] = y1 - 1; y[i + n] = y1 + 1; } } sort(line + 1, line + 2 * n + 1); sort(y + 1, y + 2 * n + 1); cnt = unique(y + 1, y + 2 * n + 1) - y - 1; build(1, 1, cnt); int ans = 0; for (int i = 1; i < n * 2; i++) { int y1 = lower_bound(y + 1, y + cnt + 1, line[i].y1) - y; int y2 = lower_bound(y + 1, y + cnt + 1, line[i].y2) - y; update(1, y1, y2, line[i].flag); ans += (line[i + 1].x - line[i].x) * tree[1].len; } printf("%d\n", ans); return 0; }
例:P1502 窗口的星星
参考代码
#include <cstdio> #include <algorithm> using namespace std; typedef long long LL; const int MAXN = 20005; struct Line { LL x, y1, y2, d, flag; bool operator<(const Line& other) const { // 注意对于两条x相等的扫描线,应先处理引入星星的扫描线 return x != other.x ? x < other.x : flag > other.flag; } }; Line line[MAXN]; LL c[MAXN]; struct Node { int l, r; LL res, add; }; Node tree[MAXN * 4]; void pushup(int cur) { tree[cur].res = max(tree[cur * 2].res, tree[cur * 2 + 1].res); } void pushdown(int cur) { if (tree[cur].l != tree[cur].r) { tree[cur * 2].res += tree[cur].add; tree[cur * 2 + 1].res += tree[cur].add; tree[cur * 2].add += tree[cur].add; tree[cur * 2 + 1].add += tree[cur].add; tree[cur].add = 0; } } void build(int cur, int l, int r) { tree[cur] = {l, r, 0, 0}; if (l == r) return; int mid = (l + r) / 2; build(cur * 2, l, mid); build(cur * 2 + 1, mid + 1, r); pushup(cur); } void update(int cur, int l, int r, LL d) { if (tree[cur].l > r || tree[cur].r < l) return; if (tree[cur].l >= l && tree[cur].r <= r) { tree[cur].res += d; tree[cur].add += d; return; } pushdown(cur); update(cur * 2, l, r, d); update(cur * 2 + 1, l, r, d); pushup(cur); } int main() { int t; scanf("%d", &t); while (t--) { int n, w, h; scanf("%d%d%d", &n, &w, &h); for (int i = 1; i <= n; i++) { LL x, y, l; scanf("%lld%lld%lld", &x, &y, &l); line[i] = {x, y, y + h - 1, l, 1}; line[n + i] = {x + w - 1, y, y + h - 1, l, -1}; c[i] = y; c[n + i] = y + h - 1; } sort(line + 1, line + 2 * n + 1); sort(c + 1, c + 2 * n + 1); int len = unique(c + 1, c + 2 * n + 1) - c - 1; build(1, 1, len); LL ans = 0; for (int i = 1; i < 2 * n; i++) { int y1 = lower_bound(c + 1, c + len + 1, line[i].y1) - c; int y2 = lower_bound(c + 1, c + len + 1, line[i].y2) - c; update(1, y1, y2, line[i].d * line[i].flag); ans = max(ans, tree[1].res); } printf("%lld\n", ans); } return 0; }
例:P1856 [IOI1998] [USACO5.5] 矩形周长Picture
参考代码
#include <cstdio> #include <algorithm> using namespace std; const int MAXN = 10005; struct Line { int x, y1, y2, flag; bool operator<(const Line& other) const { return x != other.x ? x < other.x : flag > other.flag; } }; Line lx[MAXN], ly[MAXN]; int x[MAXN], y[MAXN], xlen, ylen; struct Node { int l, r, cnt, len; }; Node tree[MAXN * 4]; void pushup(int cur, int a[]) { if (tree[cur].cnt) tree[cur].len = a[tree[cur].r] - a[tree[cur].l]; else if (tree[cur].l + 1 == tree[cur].r) tree[cur].len = 0; else tree[cur].len = tree[cur * 2].len + tree[cur * 2 + 1].len; } void build(int cur, int l, int r, int a[]) { tree[cur].l = l; tree[cur].r = r; if (l + 1 == r) return; int mid = (l + r) / 2; build(cur * 2, l, mid, a); build(cur * 2 + 1, mid, r, a); } void update(int cur, int l, int r, int d, int a[]) { if (tree[cur].l >= r || tree[cur].r <= l) return; if (tree[cur].l >= l && tree[cur].r <= r) { tree[cur].cnt += d; pushup(cur, a); return; } update(cur * 2, l, r, d, a); update(cur * 2 + 1, l, r, d, a); pushup(cur, a); } int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); lx[i] = {x1, y1, y2, 1}; lx[n + i] = {x2, y1, y2, -1}; ly[i] = {y1, x1, x2, 1}; ly[n + i] = {y2, x1, x2, -1}; x[i] = x1; x[n + i] = x2; y[i] = y1; y[n + i] = y2; } n *= 2; sort(x + 1, x + n + 1); xlen = unique(x + 1, x + n + 1) - x - 1; sort(y + 1, y + n + 1); ylen = unique(y + 1, y + n + 1) - y - 1; sort(lx + 1, lx + n + 1); sort(ly + 1, ly + n + 1); build(1, 1, ylen, y); int ans = 0; for (int i = 1; i <= n; i++) { int y1 = lower_bound(y + 1, y + ylen + 1, lx[i].y1) - y; int y2 = lower_bound(y + 1, y + ylen + 1, lx[i].y2) - y; int pre = tree[1].len; update(1, y1, y2, lx[i].flag, y); ans += abs(tree[1].len - pre); } build(1, 1, xlen, x); for (int i = 1; i <= n; i++) { int x1 = lower_bound(x + 1, x + xlen + 1, ly[i].y1) - x; int x2 = lower_bound(x + 1, x + xlen + 1, ly[i].y2) - x; int pre = tree[1].len; update(1, x1, x2, ly[i].flag, x); ans += abs(tree[1].len - pre); } printf("%d\n", ans); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?