线段树

如何快速求出一个序列的区间和?可以使用前缀和。如何快速求出一个序列的最值?可以使用 ST 表。这两种数据结构在建立的时候颇费功夫,但使用的时候效率很高。如果再增加一个需求:需要时不时修改序列的值,那么这两种数据结构就无法高效完成了。线段树可以用来解决这类问题。

线段树是一种特殊的二叉树,它可以将一个线性的序列组织成一个树状的结构,从而可以在对数时间复杂度下访问序列上的任意一个区间并进行维护。

线段树的建立与操作

例题:P3372【模板】线段树 1

已知一个数列 \(a_i\),需要支持两种操作:
1.将区间 \([x,y]\) 内每一个数加上 \(k\)
2.求出某区间 \([x,y]\) 中每一个数的和。
数的个数和操作次数不超过 \(10^5\)\(a_i, k\) 和变化后的数列数字的绝对值不超过 \(2^{63}-1\)

分析:线段树的思想在于将序列中若干个区间在树上用节点表示,其中 \([1,n]\) 区间(\(n\) 表示序列长度)是树的根。而对一个表示区间 \([l,r]\) 的节点(\(l \ne r\)),设 \(mid=\lfloor \frac{l+r}{2} \rfloor\),将 \([l,mid]\)\([mid+1,r]\) 作为该节点的左子结点和右子结点。

image

对长度为 \(10\) 的序列构建线段树,将结点 \([1,10]\) 作为根结点,设 \(mid=\lfloor \frac{1+10}{2} \rfloor = 5\),将 \([1,5]\) 作为根结点的左子结点,\([6,10]\) 作为根结点的右子结点。这两个结点的子树构建方法类似。

  1. 对于线段树上的任意一个结点,它要么没有子结点,要么有两个子结点,不存在只有一个子结点的情况。
  2. 对于一个长度为 \(n\) 的序列,它所建立的线段树只有 \(2n-1\) 个结点。
  3. 对于一个长度为 \(n\) 的序列,它所建立的线段树高为 \(\log n\)

对于第二条性质,考虑首先线段树有且仅有 \(n\) 个叶结点,初始时它们没有父结点,然后将没有父结点的结点进行两两合并,每次合并会新建一个结点,共给 \(2\) 个结点建立了父结点,新增了一个没有父结点的点。也就是每次合并会新增一个结点,没有父结点的点数减一。最终线段树有且仅有一个没有父结点的结点,因此总共新建了一个 \(n-1\) 个结点,加上初始的 \(n\) 个叶结点,总共 \(2n-1\) 个结点。

对于第三条性质,考虑对于任意一个表示 \([l,r]\) 的结点,设 \(len=r-l+1\),若 \(len\) 为偶数,则显然它的两个子结点的长度均为 \(\frac{len}{2}\),若 \(len\) 为奇数,则它的一个子结点长度为 \(\frac{len+1}{2}\),另一个为 \(\frac{len-1}{2}\)。也就是说,子结点的长度至多为父结点长度加一后的一半。设树高为 \(h\),则有 \(\frac{\frac{\frac{n+1}{2}+1}{2}+1}{2} \dots\),迭代 \(h\) 次后为 \(1\)。这个式子不大于 \(\frac{n+h}{2^h}\),于是 \(\frac{n+h}{2^h} \approx 1\),解方程得到 \(h=O(\log n)\)

线段树中一个结点上可以维护若干个所需要的信息,在访问时,将若干个结点的信息合并,就能得到任意所需区间的信息。例如,在上图中,如果希望获得区间 \([1,4]\) 的信息,只需要将结点 \([1,3]\) 和结点 \([4,4]\) 的信息合并即可。在这里需要注意的是,使用线段树所维护的信息必须具有可合并性

例如,如果要求区间和,区间 \([1,3]\) 的和加上 \([4,4]\) 的和显然就是区间 \([1,4]\) 的和;但是简单的线段树难以直接用于维护区间众数,因为区间 \([1,12]\)(假定是一个长度为 \(12\) 的序列)的众数不一定是区间 \([1,6]\) 的众数和 \([7,12]\) 的众数中出现次数较多的。比如考察数列(3,3,1,1,1,2,3,3,4,5,5,5),区间 \([1,6]\) 的众数是出现了 \(3\) 次的 \(1\),区间 \([7,12]\) 的众数是出现了 \(3\) 次的 \(5\),但区间 \([1,12]\) 的众数却是出现了 \(4\) 次的 \(3\)

将根结点定义为 \(1\) 号结点;对于编号为 \(i\) 的结点,它的左子结点编号为 \(2i\),右子结点编号为 \(2i+1\)。不难发现,这样每个结点都有且仅有唯一的编号与之对应,且在线段树上,结点的最大编号不超过 \(4n-1\),其中 \(n\) 是序列长度。

1. 建立线段树

树是递归定义的,因此可以用递归的方式建立线段树:如果这个区间左端点等于右端点,说明是叶子结点,其数据的值赋值为对应数列元素的值;否则将这个区间分为左右两部分,分别递归建立线段树,然后将左右两个区间的数据进行汇总(pushup)处理。

假设初始数列是 \(1,5,4,2,3\),那么建立好的线段树如下图所示,其中圆括号里的数字是这个区间的数字和。显然非叶子结点的区间数字和是其左右两子结点的区间数字和的和。

image

建立线段树的代码如下:

#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 函数的时间复杂度为 \(O(n)\)

2. 单点查询与修改

如何精确定位到叶子结点呢?假设需要定位到 \(p\) 这个结点,实际上是需要找到 \([p,p]\) 这个区间。初始时,该结点在根结点 \([1,n]\) 的子树中。根结点的左子结点为 \([1,mid]\),右子结点为 \([mid+1,n]\),其中 \(mid=(1+n)/2\),如果 \(p<=mid\),那么目标结点显然在左子树中,向左递归即可,否则目标结点在右子树中,需要向右递归。单点修改也是类似的过程,如果进行的是修改(更新操作),在返回时需要一路 pushup,来保证线段树信息的正确性。

例如将数列第 \(1\) 个数字加上 \(3\) 时,则先找到对应的叶子结点(也就是 \(8\) 号)更新它的数字,然后一直往这个结点的父结点更新区间和,直到根结点为止。

image

单点查询和单点修改的代码如下:

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); // 别忘记更新以后需要修改当前结点的区间和
}

在上面的代码中,可以发现每递归调用一次函数,都会在线段树上向下移动一层。因为线段树的树高是 \(O(\log n)\),所以递归函数只会调用 \(O(\log n)\) 次。也就是说,线段树的单点操作时间复杂度为 \(O(\log n)\)

3. 区间查询

只能支持单点操作的线段树是没什么意义的,这里我们需要用线段树快速维护区间信息,即给定区间 \([l,r]\),求这个区间的数字和。

从根开始递归,如果当前结点所代表的区间被所询问的区间 \([l,r]\) 所包含,那么直接返回当前区间的区间和;如果两个区间没有交集,应该返回 \(0\);如果没有被包含且两个区间有交,则递归左右子子结点处理即可。

image

例如,查询 \([2,5]\) 的区间时,相当于查询 \([2,2],[3,3,],[4,5]\) 这些区间的数据,然后进行汇总就是答案。

区间查询的代码如下:

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 函数里,并不是每层只会向下延伸一个结点,而是对左右子结点分别递归。那么如何分析其复杂度呢?在线段树每层的递归中,最多只有两个结点会向下继续递归,也就是被查询区间两端点所在的结点。而剩下的结点要么是被完全包含,要么是与查询区间不相交。因此,每一层只会新建 \(2\) 个递归函数调用。而因为树高是 \(O(\log n)\),所以总的时间复杂度还是 \(O(\log n)\)

4. 区间修改

在区间修改时,显然不能暴力地修改每个叶子,那样效率很低。为此,引入延迟标记(又称为懒标记或者 lazy-tag),记录一些区间修改的信息。当递归至一个被完全包含的区间时,在这个区间上打一个延迟标记,记录这个区间中的每个数都需要被加上某个数,然后直接修改该结点的区间和并返回,不再向下递归。当新访问到一个结点时,先将延迟标记下放到子结点,然后再进行递归。

可以发现,这样做可以保证与根相连的某个连通块的信息总是正确的,并且在调用时总能得到正确的信息。同时,因为被完全包含和不相交的情况都不会再递归,所以其时间复杂度为 \(O(\log n)\)

struct Node {
	int l, r; // 某个结点所代表的区间
	LL value, tag; // value存储结点对应的区间和
    // tag是区间加的延迟标记
};

假设初始数列是 \(1,5,4,2,3\)。对区间 \([1,4]\) 的每个数都加上 \(5\),该区间在线段树上被分成了 \([1,3]\)\([4,4]\) 两个结点,初始时 \([1,3]\) 的和为 \(10\),标记的值 \(tag\)\(0\)\([4,4]\) 的和为 \(2\),标记的值 \(tag\)\(0\)。修改时,将 \([1,3]\) 的区间和加上 \(3 \times 5 = 15\) 变成 \(25\)\([4,4]\) 的区间和加上 \(1 \times 5 = 5\) 变成 \(7\),两者的 \(tag\) 都加上 \(5\) 变成 \(5\)。完成递归后,将结点 \([1,4]\) 的区间和更新为 \(30\)

此时如果查询 \([3,5]\) 的区间和,访问到 \([1,3]\) 区间时,发现需要继续递归下去,这时将标记下放到这个结点的两个子结点。子结点的 \(tag\) 值要增加父节点的 \(tag\) 值,同时将区间和的值加上区间长度乘以父结点的 \(tag\) 值。下放后父结点的 \(tag\) 值要清空。

image

可以看到,对于打了延迟标记的结点,其维护的区间和是已经修改完成的信息,其子结点的值还没有被修改。也就是说,延迟标记起到的作用是记录子结点的每个数应该加上多少,而不是该结点本身的信息。代码如下,注意在查询时也要包含下放标记的过程:

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 = 500005;
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] 开关

给定一个初始为 \(0\) 的长度为 \(n\) 的数列,进行 \(m\) 次操作,要求支持两种操作:
1.给区间 \([a,b]\) 的所有数字对 \(1\) 取异或。
2.求区间 \([a,b]\)\(1\) 的个数。
数据范围:\(1 \le n,m \le 10^5\)

分析:不难发现,要求的“区间内 \(1\) 的个数”这一问题具有可合并性,并且“区间异或”这一操作很容易通过线段树的延迟标记实现,定义延迟标记的含义为区间内所有数字都异或上该值,修改时,将延迟标记也异或上 \(1\)。在每次异或 \(1\) 时,原有的 \(1\) 会变成 \(0\),原有的 \(0\) 会变成 \(1\),也即区间内 \(1\) 的个数会变成区间长度减去原来的个数。

#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 无聊的数列

维护一个长度为 \(n\) 的数列 \(a\)。要求支持 \(m\) 此操作,操作有两种类型:
1.1 l r k d:给出一个长度等于 \(r-l+1\) 的等差数列,首项为 \(k\),公差为 \(d\),并将它对应加到 \([l,r]\) 范围中的每一个数上。即:令 \(a_l = a_l + k, a_{l+1} = a_{l+1} + k + d, \dots, a_r = a_r + k + (r-l) \times d\)
2.2 p:询问序列的第 \(p\) 个数的值 \(a_p\)
数据范围:\(n,m \le 10^5\)

分析:这是一个区间加等差数列的问题。考虑等差数列有两个要素:首项 \(k\) 和公差 \(d\)。只要这两项确定了,等差数列就唯一确定了。而这两个要素具有“可加性”:将首项分别为 \(k_1, k_2\),公差分别为 \(d_1,d_2\) 的两个等差数列的每一项对应相加,得到的数列也是一个等差数列,且它的首项为 \(k_1+k_2\),公差为 \(d_1+d_2\)

例如,将数列 \(1,2,3\)(首项为 \(1\),公差为 \(1\))和 \(1,3,5\)(首项为 \(1\),公差为 \(2\))对应相加,得到数列 \(2,5,8\)。它的首项为 \(1+1=2\),公差为 \(1+2=3\)

于是使用两个延迟标记,分别表示首项和公差即可。

#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 扶苏的问题

给定一个长度为 \(n\) 的序列 \(a\),要求支持 \(q\) 次操作,共有三种类型的操作:
1.给定区间 \([l,r]\),将区间内每个数都修改为 \(x\)
2.给定区间 \([l,r]\),将区间内每个数都加上 \(x\)
3.给定区间 \([l,r]\),求区间内的最大值。
数据范围:\(1 \le n,q \le 10^6; -10^9 \le a_i,x \le 10^9\)

分析:本题中所求的“区间最大值”也可以使用线段树维护:父结点的区间最大值就是它两个子结点的区间最大值中较大的一个。

对于修改操作,可以使用两个延迟标记,一个表示区间赋值为 \(x\)(记为 \(cover\)),一个表示区间加上 \(x\)(记为 \(add\))。当对一个结点执行操作 \(1\) 时,直接将 \(cover\) 赋值为 \(x\)\(add\) 清空;执行操作 \(2\) 时,若 \(cover\) 存在,则将 \(cover\) 加上 \(x\),否则将 \(add\) 加上 \(x\)

需要注意的是,因为操作时可能赋值为 \(0\) 或负数,所以需要用一个在计算过程中永远不可能出现的数(例如 \(10^{16}\))来表示覆盖标记不存在。

#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

给定一个长度为 \(n (1 \le n \le 10^5)\) 的数列,需要进行下面三种操作:
1.将区间内每个数乘上 \(x\)
2.将区间内每个数加上 \(x\)
3.求数列的区间和,答案对一个大数取模。

分析:这是一个多标记线段树的题。本题中一共出现了两种修改,分别为加法和乘法,考虑用两个标记分别维护它们。

\(add\) 表示某个区间的加法标记,\(mul\) 表示某个区间的乘法标记。这里 \(mul\) 表示的是原区间和乘上的数,\(add\) 表示的是区间和加上的数,设原区间和为 \(s\),则最终计算的区间和为 \(s \times mul + add\),否则难以维护。值得注意的是,进行区间加操作时,对前面的区间乘操作没有影响,而进行区间乘操作时,相当于将之前区间要加的数也乘上了 \(x\)。也即:若设原区间和为 \(s\),新加了 \(a\),现在要乘上 \(b\),则新的区间和是 \((s+a) \times b\),也即 \(s \times b + a \times b\)

因此,标记的下传顺序十分关键:在 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

解题思路

使用计数排序的思想。对于每一个询问,查询每种字母的在区间内的个数,使用计数排序的方式来更新区间信息。

构建 \(26\) 棵线段树面向每一种字母。这样一来,结合计数排序的模式,问题就转化为了区间个数查询和区间个数更新,需要使用延迟标记技术来更新区间信息。

时间复杂度为 \(O(Aq \log n)\),其中 \(A\) 是符号的种类数,在这道题中相当于 \(26\)

参考代码
#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 小白逛公园

给定一个长度为 \(n\) 的数列 \(a\),有 \(m\) 次操作,每次操作要么对 \(a\) 进行单点修改,要么查询数列 \(a\) 的最大子段和是多少。区间 \([l,r]\) 的连续和是指 \(\sum \limits_{i=l}^{r} a_i = a_l + a_{l+1} + \cdots + a_r\)。最大子段和指的是所有的区间连续和中最大的值。
数据范围:\(1 \le n \le 5 \times 10^5, 1 \le m \le 10^5, -1000 \le a_i \le 1000\)

分析: 考虑对序列 \(a\) 建立线段树。如果只在结点上维护信息“当前区间内的最大子段和”,则无法汇总到父结点上。因为并不能通过“子结点的最大子段和”推出父结点的最大子段和。例如,对于序列 \(-1,1,1,-1\) 和序列 \(-1,1,-1,1\),两者的区间 \([1,2]\) 以及区间 \([3,4]\) 的最大子段和均为 \(1\),但是前者的区间 \([1,4]\) 的最大子段和为 \(2\),后者的为 \(1\)

进一步地,考虑父结点的最大子段和只可能存在三种情况:是左子结点的最大子段和,是右子结点的最大子段和,是左子结点和右子结点的两段相邻的和拼起来。对于前两种情况很容易转移,现在考虑第三种情况。

在这种情况下,左子结点被拼起来的那一段区间必须包含左子结点的右端点,换句话说,它是以左子结点右端点为起点向左找的最大连续和,称之为最大后缀和;同理,右子结点被拼起来的那一段区间是以右子结点左端点为起点向右找的最大连续和,称为最大前缀和。这样只需维护结点的最大前缀和、最大后缀和以及最大子段和,就可以合并出父结点的最大子段和了。

image

进一步考虑如何合并出父结点的最大前缀和以及最大后缀和:对于父结点的最大前缀和,要么直接就是左子结点的最大前缀和,要么是左子结点的全体拼上右子结点的最大前缀和;最大后缀和的维护同理。

image

因此,只需要再维护一个区间和,就可以完成对最大前缀和、最大后缀和的维护了。显然区间和的维护直接合并两个子结点的区间和即可。

#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 提高组] 子序列问题

解题思路

对于这类区间信息求和的问题,我们往往可以枚举一个端点(比如右端点),在一个数据结构上维护另一个端点取每个值时,该区间的答案。

这里我们考虑枚举右端点 \(r\),维护 \(l\) 取每个值的时候 \(f(l,r)\) 是多少。

如果问的是 \(f(l,r)\) 的和而不是平方和,则这个问题是很简单的(思考当枚举的右端点从 \(r-1\) 移动到 \(r\) 的时候,\(f\) 的值会怎么变化)。可以发现,这取决于上一个与 \(a_r\) 相等的数在哪个位置出现,如果 \(a_r\) 是第一次出现,则之前的每个 \(f\) 相当于都要加 \(1\),如果之前出现过 \(a_r\),则相当于前面部分的 \(f\) 不变(因为本来就有这个数),而后面部分的 \(f\) 都要加 \(1\)

这样一来,问题就转化成了对一个序列支持两种操作:区间加 \(1\) 以及求整个区间的 \(f\) 的平方和。这个问题考虑用线段树来维护 \(l\) 取每个值时 \(f(l,r)\) 的平方和。

区间加 \(1\) 对区间中的平方和有什么样的影响?如果区间加 \(1\),平方和会增加 \(2 \times sum + len\),其中 \(sum\) 是区间和,\(len\) 是区间长度。因此只维护区间平方和不够,还需要维护一下区间和 \(sum\)。推广到区间加 \(add\) 对平方和的影响:若区间加 \(add\),则平方和会增加 \(2 \times d \times sum + d^2 \times len\)

注意因为 \(a_i\) 很大,需要进行离散化。

参考代码
#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());
    int len = unique(data.begin(), data.end()) - data.begin();
    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;
}
posted @ 2023-08-31 17:26  RonChen  阅读(100)  评论(0编辑  收藏  举报