线段树 & 树状数组

我们之前已经学习过猫树了,今天来补一期线段树。

引入

何为区间问题(来自 OI wiki)?

假设有这样一道题:

已知一个数列 \(\{a_i\}\),你需要进行下面两种操作:

  • 给定 \(x\)\(val\),将 \(a[x]\) 增加 \(val\)

  • 给定 \(l\)\(r\),求解 \(a[l...r]\) 的和

其中第一种操作就是「单点修改」,第二种操作就是「区间查询」。

类似地,还有:「区间修改」、「单点查询」。

注意到,区间问题一般严格强于单点问题,因为对单点的操作相当于对一个长度为 \(1\) 的区间操作。

在处理数组的动态区间查询和更新问题时,线段树(Segment Tree)和树状数组(Fenwick Tree)是两种常用的数据结构。它们均基于分治思想,能将时间复杂度优化至 \(O(log n)\),但适用场景和实现方式各有特点。

我们先来看一看两者对比:

特性 线段树 树状数组
区间查询 支持任意区间查询(如和、最值、GCD) 仅支持前缀和查询,需通过差分数组扩展
区间修改 支持(需懒惰标记确保时间复杂度正确) 需通过差分数组实现
适用场景 更广 可看作线段树处理问题的子集
常数复杂度 常数较大 常数较小

树状数组

思路演示

树状数组是一种支持 单点修改区间查询 的,代码量小的数据结构。

需要注意的是,虽然树状数组解决的问题比较局限,但由于其极简的码量,在处理其可解决问题时,无疑他会成为线段树的更好选择(常数、空间复杂度均优于线段树)

很多选手对树状数组的了解仅停留在背诵层面,却不知道这短短的两行代码背后的实际含义。深入研究树状数组的结构有助于我们更好地理解和运用树状数组(但事实上,背会就好了,如果不会运用直接使用线段树就好,线段树较树状数组来说更利于理解)。

我们引入一颗树,规定其叶子节点为原数组,定义每一列的顶端节点为 \(c\) 数组。

树状数组

\(c\) 数组的计算方式就是子树的叶子节点的权值之和。

例如说在上述例子中就有:

\(c_i\) \(=\) \(\sum_{k = i - \operatorname{lowbit}(i) + 1}^{i} c_k\)
\(c_1\) \(=\) \(a_1\)
\(c_2\) \(=\) \(a_1 + a_2\)
\(c_3\) \(=\) \(a_3\)
\(c_4\) \(=\) \(a_1 + a_2 + a_3 + a_4\)
\(c_5\) \(=\) \(a_5\)
\(c_6\) \(=\) \(a_5 + a_6\)
\(c_7\) \(=\) \(a_7\)
\(c_8\) \(=\) \(a_1 + a_2 + a_3 + a_4 + a_5 + a_6 + a_7 + a_8\)

实现流程

区间查询 & 单点修改

  • 利用 \(\{c_i\}\) 数组,求 \(\{a_i\}\) 数组中前 \(i\) 项和。

    我们不妨来举个例子演示一下:

    比如说我们要求 \(i = 7\) 时的前 \(7\) 项和,结合刚才的表格不难发现:

    \[\begin{align*} \sum_{i=1}^{7} a_i & = a_1+a_2+a_3+a_4+a_5+a_6+a_7 \\ & = c_4 + c_6 + c_7 \end{align*} \]

    我们尝试将其下标写成二进制(不要问为什么想到二进制,你可以理解为树状结构尤其是二叉树的下标一般和二进制有关,这一思想我们在讲猫树的时候也用到了),可以得到:

    \(sum[(111)_2] = c[(100)_2] + c[(110)_2] + c[(111)_2]\)

    不难发现,他就是每次去掉最低位的 \(1\),然后累加求和。

  • 修改 \(\{a_i\}\) 数组中第 \(i\) 项,并同步到 \(\{c_i\}\) 数组。

    从刚刚的图中不难看出,当在 \(a_1\) 加上一个值,即更新 \(a_1\) 时,需要向上更新 \(c_1,c_2,c_4,c_8\),这个时候只需将这\(4\) 个节点每个节点的值更新即可。

    同样写出其二进制:\(c[(001)_2],c[(010)_2],c[(100)_2],c[(1000)_2]\)

    那这比较显然了,就是每次加上一个当前最低位的 \(1\) 以及它后面的 \(0\) 构成的数值就可以了。

单点查询 & 区间修改

我们的树状数组你如果看了上面的介绍就会发现,他从根本上是无法独立实现区间操作的。

所以我们需要去维护一个差分数组,将区间操作转化为单点操作。

也就是从 \(a[l..r] + val\) 转化为 \(c[l] + val\)\(c[r + 1] - val\) 两步操作,从而保证其时间复杂度正确性。

区间查询 & 区间修改

如果题目中出现这个操作,奉劝各位读者赶紧使用线段树就行了。

如果你硬要使用树状数组,倒也不是不可以,我们可以使用双差分技巧,思维难度会直线上升。

这部分不是很重要,我们改日再更新。

代码实现

刚才的思路讲解过程中均涉及到了当前二进制位的最后一个 \(1\),就比如 0010100 这个二进制最低位的 \(1\) 以及它后面的 \(0\) 构成的数值就是 100

所以我们首先需要实现一个 \(\operatorname{lowbit}(x)\) 函数来实现上述操作,那这个函数是什么原理呢?

\(\operatorname{lowbit} (x)\) 函数可以取出 \(x\) 在二进制表示下最低位的 \(1\) 以及它后面的 \(0\) 构成的数值,\(x - lowbit (x)\) 操作就可以去掉最低位的 \(1\)

  1. 假设 \(x\) 最低位的 \(1\) 在第 \(k\) 位上;
  2. 则按位取反 ~x 的二进制的第 \(k\) 位为 \(0\)\([0 , k-1]\) 位全部为 \(1\)\([k+1 , +\infty)\) 位也被取反;
  3. 于是 -x = ~x + 1 由于进位,\([0 , k-1]\) 位全部为 \(0\), 第 \(k\) 位为 \(1\)\([k+1 , +\infty)\) 位仍然和原来相反;
  4. 那么 x & (-x) 自然就只剩下最低位的 \(1\) 以及它后面的 \(0\) 构成的数值了

所以有:

inline ll lowbit(ll x) {
	return x & (-x);
}

知道了 \(\operatorname{lowbit} (x)\) 函数之后,我们就可以依照上面的逻辑写出其他的代码了:

区间查询,返回 \(\sum_{i = 1}^{x} a_i\)

inline ll check(ll x) {
	ll res = 0;
	for (ll i = x; i; i -= lowbit(i)) {
		res += tree[i];
	}
	return res;
}

单点修改,将 \(a_i\) 加上 \(val\)

inline void add(ll x, ll val) {
	for (ll i = x; i <= n; i += lowbit(i)) {
		tree[i] += val;
	}
}

综上所述,你就掌握了树状数组的基本操作了。

例题

由于博主有些老代码,当时并不清楚树状数组的空间复杂度,如果遇到 tree[N << 2] 请自动忽略。见谅

P3374 【模板】树状数组 1

板题。详见刚刚的区间查询单点修改部分,你就可以将查到的答案当作前缀和处理,然后用 \(sum_r - sum _{l - 1}\) 就可以,这应该不用我教吧?

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e6+86;
ll n, m, t;
ll a[N];
ll tree[N];

inline ll lowbit(ll x) {
	return x & (-x);
}

inline void add(ll x, ll val) {
	for (ll i = x; i <= n; i += lowbit(i)) {
		tree[i] += val;
	}
}
inline ll check(ll x) {
	ll res = 0;
	for (ll i = x; i; i -= lowbit(i)) {
		res += tree[i];
	}
	return res;
}


int main() {

	cin >> n >> m;
	for (ll i = 1; i <= n; i++) {
		cin >> t;
		add(i, t);
	}
	while (m--) {
		ll opt;
		cin >> opt;
		if (opt == 1) {
			ll x, k;
			cin >> x >> k;
			add(x, k);
		} else {
			ll x, y;
			cin >> x >> y;
			// 可以将check函数理解为前缀和
			cout << check(y) - check(x - 1) << endl;
		}
	}

	return ~~ (0 ^ 0);
}

P3368 【模板】树状数组 2

板题,使用差分数组。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 5e5 + 86;
ll tree[N], a[N];
ll n, m;
inline ll lowbit(ll x) {
	return x & (-x);
}

inline void add(ll x, ll val) {
	for (ll i = x; i <= n; i += lowbit(i)) {
		tree[i] += val;
	}
}

inline ll check(ll x) {
	ll res = 0;
	for (ll i = x; i; i -= lowbit(i)) {
		res += tree[i];
	}
	return res;
}

int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;

	for (ll i = 1; i <= n; i++) {
		// 树状数组维护差分数组
		cin >> a[i];
		add(i, a[i] - a[i - 1]);
	}
	while (m--) {
		ll opt;
		cin >> opt;
		if (opt == 1) {
			ll x, y, val;
			cin >> x >> y >> val;
			add(x, val);
			add(y + 1, -val);
		} else {
			ll x;
			cin >> x;
			cout << check(x) << endl;
		}
	}
	return ~~ (0 ^ 0);
}

P5057 CQOI2006 简单题

区间异或操作,变一下操作函数就可以。

考虑差分序列(用异或代替减法,异或就是无退位减法),则每个元素的真实值是差分序列的异或前缀和,修改的时候只需要改 \(l\)\(r+1\) 两个位置的差分。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5 + 5;
ll tree[N];
ll n, m;

inline ll lowbit(ll x) {
	return x & (-x);
}

inline void change(ll x) {
	for (ll i = x; i <= n; i += lowbit(i)) {
		tree[i] ^= 1;
	}
}
inline ll check(ll x) {
	ll res = 0 ;
	for (ll i = x; i ; i -= lowbit(i)) {
		res ^= tree[i];
	}
	return res;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	while (m--) {
		ll opt;
		cin >> opt;
		if (opt == 1) {
			ll l, r;
			cin >> l >> r;
			change(l);
			change(r + 1);
		} else {
			ll x;
			cin >> x;
			cout << check(x) << endl;
		}
	}

	return ~~ (0 ^ 0);
}

P1908 逆序对

没错,逆序对除了可以使用归并排序,线段树,当然也可以使用树状数组解决,且思维难度极低。

首先考虑 \(O(n^2)\) 的暴力,枚举每一个数,再枚举他前面比他大的数,时间超限,不做讲解。

考虑空间换时间的优化:

每次枚举到一个数,就在数组中这个数以及这个数之前的所有数的位置加一,这样下次枚举数字时,如果这个数字比上一个数字小,那上一个数字一定在这个数字的地方标记过。

举个例子:6 5 7 3,同时维护数组 \(\{a_i\}\)

第一个 \(6\) 显然不够成逆序对,我们让 \(a[1...6]\) 加一:1 1 1 1 1 1 0 0

第二个 \(5\) 这时 \(a_5\) 已经为 \(1\) ,说明形成了一个逆序对,然后继续加一:2 2 2 2 2 1 0 0

第三个 \(7\) 同样不构成逆序对,继续加一:3 3 3 3 3 2 1 0

第四个 \(3\)\(a_3\)\(3\) ,说明形成了三个逆序对。

综上所述,逆序对一共就有 \(4\) 个。

上述方法无论是空间还是时间上均有很大问题,空间上当 \(a_i\) 来到 \(1\times 10^9\) 级别时就已经 MLE 了。

所以我们需要进行离散化操作,这里需要注意的是,这一思路要求将相同值的多次出现映射到不同的位置,因为相同的数显然无法互相构成逆序对。

时间上如果直接暴力维护这一数组肯定也是超时的,因此我们用树状数组优化就可以了。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 5e5 + 86;
ll n, b[N];
pair<ll, ll> a[N];
ll tree[N];

inline ll lowbit(ll x) {
	return x & (-x);
}

inline void add(ll x, ll val) {
	for (ll i = x; i <= n; i += lowbit(i)) {
		tree[i] += val;
	}
}

inline ll check(ll x) {
	ll res = 0;
	for (ll i = x; i; i -= lowbit(i)) {
		res += tree[i];
	}
	return res;
}

int main() {
	// input and 离散化
	cin >> n;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i].first;
		a[i].second = i;
	}
	stable_sort(a + 1, a + n + 1);
	for (ll i = 1; i <= n; i++) {
		b[i] = a[i].second;
	}

	// solve
	ll cnt = 0;
	for (ll i = 1; i <= n; i++) {
		cnt += check(b[i]);
		add(b[i] + 1, -1);
		add(1,  1);
	}
	cout << cnt;
	return ~~ (0 ^ 0);
}

线段树

思路演示

线段树是一种二叉树结构,每个节点代表一个区间,支持高效的区间查询和更新。其核心思想是将数组递归划分为子区间,直到每个子区间仅含一个元素(叶子节点),非叶子节点存储子区间的聚合信息(如和、最大值等)。

线段树

例如上方的图片,可以看到,线段树的每个节点表示了一条线段,线段树的根节点表示了所要处理的最大线段区间,而叶节点则表示了形如 \([a,a]\) 的单位区间。对于每个非叶节点(包括根节点)所表示的区间 \([l,r]\),令 \(mid = \lfloor \frac{l + r}{2} \rfloor\),则其左儿子节点表示区间 \([l,mid]\),右儿子节点表示区间 \([mid+1,r]\)
然后我们还可以得到一个性质:节点 \(x\) 的权值等于左儿子权值与右儿子权值的和。

是不是较树状数组更好理解一些?就是一个二分的思想,所以说线段树是基于二分的数据结构。

代码实现

前置函数

由上方讲的一些性质,我们可以先写一些前置函数来降低编码难度。

  • 求左右儿子(这里使用了位运算,常数上会更快,如不理解使用正常运算也可以)

    inline ll ls(ll x) {
    	return x << 1;
    }
    inline ll rs(ll x) {
    	return x << 1 | 1;
    }
    
  • 用儿子节点更新值(根据题目进行变化,可以求最值,异或和等,这里以区间和为例)

    inline void push_up(ll x) {
    	tree[x] = tree[ls(x)] + tree[rs(x)];
    }
    

建树

根据刚才的思路铺垫,我们很轻松就可以建一颗线段树了:

inline void build(ll x, ll l, ll r) {
	if (l == r) {
		tree[x] = a[l];
		return ;
	}
	ll mid = l + r >> 1;
	build(ls(x), l, mid);
	build(rs(x), mid + 1, r);
	push_up(x);
}

这样建树的好处在于,对于每条要处理的线段,可以二分的进入线段树中处理,使得时间复杂度在
\(O(\log n)\) 量级,这也是线段树之所以高效的原因。

区间查询 &单点修改

这里特指没有修改操作或者只有单点修改操作的区间查询操作。

比如说我们要取出某个区间的区间和,该如何完成呢?

我们一般分两步:

  1. 检查是否覆盖,就是当前节点的区间范围是否被要查询的区间完全覆盖,如果是就直接返回该节点答案。
  2. 进行递归操作,查询左右儿子,这里需要判断查询区间是否和左右孩子之间有交集,避免无效查询。
inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) return tree[x];
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls(x), tl, mid, l, r);
	if (r > mid) res += check(rs(x), mid + 1, tr, l, r);
	return res;
}

单点修改比较简单。就是单点在哪个儿子就进入那个儿子里进行操作。

inline void add(ll x, ll tl, ll tr, ll pos, ll val) {
	if (tl == tr) {
		tree[x] += val;
		return ;
	}
	ll mid = tl + tr >> 1;
	if (pos <= mid) add(ls(x), tl, mid, l, r, val);
	if (pos > mid) add(rs(x), mid + 1, tr, l, r, val);
	push_up(x);
}

区间修改 & 区间查询

这里我们如果将区间修改转化为多次单点修改会导致复杂度不正确。所以我们引入一个进阶技巧:懒惰标记(Lazy Propagation)。

当需要对区间进行批量更新(如区间加、乘)时,传统方法会递归更新所有子节点,时间复杂度退化为 \(O(n)\)。懒惰标记通过延迟更新子节点的方式,将时间复杂度优化至 \(O( \log n)\)

这里需要注意的是,不同标记之间由于运算优先级不同,因此标记下传的顺序也不同。通常区间覆盖先于区间乘法先于区间加法。

区间加(Range Add)

在节点中添加 tag_add 标记,表示该区间未被处理的增量。更新时,若当前节点完全包含在目标区间内,则直接更新其值并标记,否则递归处理子节点。如果要进行进一步递归,首先要将标记下传。

inline void down_tag(ll x, ll l, ll r) { // 下传lazy-tag
	// 先将标记下传给两个儿子,然后将自身标记清空
	ll mid = l + r >> 1;
	tag[ls(x)] += tag[x];
	tree[ls(x)] += tag[x] * (mid - l + 1);
	tag[rs(x)] += tag[x];
	tree[rs(x)] += tag[x] * (r - mid);
	tag[x] = 0;
}

区间乘(Range Mul)

在节点中添加 tag_times 标记,需要注意的是,区间乘标记也会影响区间加法,因此需要先下传乘法标记。

此外,乘法标记的初值应赋值成 \(1\),务必多次检查,不要问我为什么这么说

inline void down_times(ll x) {
	tag_times[ls(x)] = tag_times[ls(x)] % mod * tag_times[x] % mod;
	tag_add[ls(x)] = tag_add[ls(x)] % mod * tag_times[x] % mod;
	tree[ls(x)] = tree[ls(x)] % mod *  tag_times[x] % mod;
	tag_times[rs(x)] = tag_times[rs(x)] % mod * tag_times[x] % mod;
	tag_add[rs(x)] = tag_add[rs(x)] % mod * tag_times[x] % mod;
	tree[rs(x)] = tree[rs(x)] % mod * tag_times[x] % mod;
	tag_times[x] = 1;
}

区间覆盖(Range Cover)

类似区间加,通过添加 cover 标记表示区间被覆盖的值。初值可以为 \(-1\)

inline void down_tag(ll x, ll l, ll r) {
	ll mid = l + r >> 1;
	// 下传覆盖标记到左儿子
	tag_cover[ls(x)] = tag_cover[x];
	tree[ls(x)] = tag_cover[x] * (mid - l + 1);
	// 下传覆盖标记到右儿子
	tag_cover[rs(x)] = tag_cover[x];
	tree[rs(x)] = tag_cover[x] * (r - mid);
	// 清空当前节点的覆盖标记
	tag_cover[x] = -1;
}

进行了上面的标记下传操作后,然后就正常对区间进行相关操作就可以,别忘了修改的时候增加标记,查询的时候先下传标记。(我们以区间加运算为例)

// 区间修改
inline void add(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	if (tl >= l && tr <= r) {
		tag[x] += val;
		tree[x] += val * (tr - tl + 1); // 值 * 区间长度
		return ;
	}
	if (tag[x]) down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) add(ls(x), tl, mid, l, r, val);
	if (r > mid) add(rs(x), mid + 1, tr, l, r, val);
	push_up(x);
}

// 区间查询
inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) return tree[x];
	if (tag[x]) down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls(x), tl, mid, l, r);
	if (r > mid) res += check(rs(x), mid + 1, tr, l, r);
	return res;
}

例题

我们看一下例题吧,针对于不同的例题,我们会衍生出很多不同种类的线段树。

P3373 【模板】线段树 2

板题 \(1\) 就不看了,我们直接来看加乘混合运算,注意标记下传顺序。

!!!大码量预警!!!

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5+86;
ll n, q, mod;
ll a[N], tree[N << 2], tag_add[N << 2];
vector<ll> tag_times(N << 2, 1);

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}

inline void push_up(ll x) {
	tree[ls(x)] %= mod;
	tree[rs(x)] %= mod;
	tree[x] = (tree[ls(x)] + tree[rs(x)]) % mod;
}

inline void down_times(ll x) {
	tag_times[ls(x)] = tag_times[ls(x)] % mod * tag_times[x] % mod;
	tag_add[ls(x)] = tag_add[ls(x)] % mod * tag_times[x] % mod;
	tree[ls(x)] = tree[ls(x)] % mod *  tag_times[x] % mod;
	tag_times[rs(x)] = tag_times[rs(x)] % mod * tag_times[x] % mod;
	tag_add[rs(x)] = tag_add[rs(x)] % mod * tag_times[x] % mod;
	tree[rs(x)] = tree[rs(x)] % mod * tag_times[x] % mod;
	tag_times[x] = 1;
}

inline void down_add(ll x, ll l, ll r) {
	ll mid = l + r >> 1;
	tag_add[ls(x)] += tag_add[x] % mod;
	tag_add[ls(x)] %= mod;

	tree[ls(x)] += tag_add[x] * (mid - l + 1) % mod;
	tree[ls(x)] %= mod;

	tag_add[rs(x)] += tag_add[x] % mod;
	tag_add[rs(x)] %= mod;

	tree[rs(x)] += tag_add[x] * (r - mid) % mod;
	tree[rs(x)] %= mod;

	tag_add[x] = 0;
}

inline void build(ll x, ll  l, ll r) {
	if (l == r) {
		tree[x] = a[l] % mod;
		return ;
	}
	ll mid = l + r >> 1;
	build(ls(x), l, mid);
	build(rs(x), mid + 1, r);
	push_up(x);
}

inline void times(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	if (tl >= l && tr <= r) {
		tag_times[x] = tag_times[x] % mod * val % mod;
		tag_add[x] = tag_add[x] % mod *  val % mod;
		tree[x] = tree[x] % mod *  val % mod;
		return ;
	}
	if (tag_times[x] != 1) down_times(x);
	if (tag_add[x]) down_add(x, tl, tr);

	ll mid = tl + tr >> 1;
	if (l <= mid) times(ls(x), tl, mid, l, r, val);
	if (r > mid) times(rs(x), mid + 1, tr, l, r, val);
	push_up(x);
}

inline void add(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	if (tl >= l && tr <= r) {
		tag_add[x] += val % mod;
		tree[x] = (tree[x] % mod + val * (tr - tl + 1) % mod ) % mod;
		return ;
	}
	if (tag_times[x] != 1) down_times(x);
	if (tag_add[x]) down_add(x, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) add(ls(x), tl, mid, l, r, val);
	if (r > mid) add(rs(x), mid + 1, tr, l, r, val);
	push_up(x);
}

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) {
		return tree[x] % mod;
	}
	if (tag_times[x] != 1) down_times(x);
	if (tag_add[x]) down_add(x, tl, tr);

	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res = (res + check(ls(x), tl, mid, l, r)) % mod;
	if (r > mid) res = (res + check(rs(x), mid + 1, tr, l, r)) % mod;
	return res % mod;
}

int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);


	cin >> n >> q >> mod;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	build(1, 1, n);
	while (q--) {
		ll opt;
		cin >> opt;
		if (opt == 1) {
			ll x, y, val;
			cin >> x >> y >> val;
			times(1, 1, n, x, y, val % mod);
		}
		if (opt == 2) {
			ll x, y, val;
			cin >> x >> y >> val;
			add(1, 1, n, x, y, val % mod);
		}
		if (opt == 3) {
			ll x, y;
			cin >> x >> y;
			cout << check(1, 1, n, x, y) << endl;
		}
	}

	return ~~ (0 ^ 0);
}

P3870 TJOI2009 开关

这是一类型板题,完成可获得多倍经验。

具体题意就是:有一个数列,你要维护以下两个操作:

  1. 对一段区间进行异或操作;

  2. 求一段区间和。

首先,查询没有任何变动,就是维护区间和。

然后,再是修改,对于区间去异或,我们发现,由于只有 \(0\)\(1\),所以只要拿区间长度减去原来的区间和就好,然后把异或操作用懒标记下传,对于懒标记,异或两次就是没变化,所以只要把奇数次的懒标记按上规则操作即可,这里将懒标记每次 tag ^= 1 就可以了,当 tag = 1 的时候就进行操作就可以了。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5+86;
ll n, m;
ll tree[N << 2] ;
bool tag[N << 2];

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}

inline void rec(ll x, ll l, ll r) {
	tree[x] = r - l + 1 - tree[x];
	tag[x] ^= 1;
}

inline void push_down(ll x, ll l, ll r) {
	ll mid = l + r >> 1;
	// 只传递奇偶性(0变1,1变0)
	if (!tag[x]) return ;
	rec(ls(x), l, mid);
	rec(rs(x), mid + 1, r);

	tag[x] = 0;
}

inline void push_up(ll x) {
	tree[x] = tree[ls(x)] + tree[rs(x)];
}

inline void update(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) {
		rec(x, tl, tr);
		return ;
	}

	push_down(x, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) update(ls(x), tl, mid, l, r);
	if (r > mid) update(rs(x), mid + 1, tr, l, r);

	push_up(x);
}

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) return tree[x];
	push_down(x, tl, tr);
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls(x), tl, mid, l, r);
	if (r > mid) res += check(rs(x), mid + 1, tr, l, r);
	return res;
}

int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n >> m;
	while (m--) {
		bool opt ;
		ll x, y;
		cin >> opt >> x >> y;
		if (opt) {
			cout << check(1, 1, n, x, y) << endl;
		} else {
			update(1, 1, n, x, y) ;
		}
	}

	return ~~ (0 ^ 0);
}

CF242E XOR on Segment

完成了上面那道题后,我们可以来看这个超级增强版了,因为是二进制,我们只可以往拆位方向想。

于是经过感性思考,我们想到可以同时维护大概 \(20\) 颗线段树,每颗线段树分别维护 \(\{a_i\}\) 的每一位的 \(0/1\) 情况。然后具体操作就是上面那道题讲的。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5 + 86;
int tree[21][N << 2]; // 维护二进制每一位
ll tag[21][N << 2];
ll a[N], n, m;

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}
inline void push_up(ll x) {
	for (ll i = 1; i <= 20; i++) {
		tree[i][x] = tree[i][ls(x)] + tree[i][rs(x)];
	}
}

inline void build(ll x, ll l, ll r) {
	if (l == r) {
		bitset<21> temp(a[l]);
		for (ll i = 1; i <= 20; i++) {
			tree[i][x] = temp[i - 1];
		}
		return ;
	}
	ll mid = l + r >> 1;
	build(ls(x), l, mid);
	build(rs(x), mid + 1, r);
	push_up(x);
}


inline void down_tag(ll x, ll l, ll r) {
	ll mid = l + r >> 1;
	for (ll i = 1; i <= 20; i++) {
		if (tag[i][x]) {
			// 左子节点
			tag[i][ls(x)] ^= 1;
			tree[i][ls(x)] = (mid - l + 1) - tree[i][ls(x)];
			// 右子节点
			tag[i][rs(x)] ^= 1;
			tree[i][rs(x)] = (r - mid) - tree[i][rs(x)];
		}
		tag[i][x] = 0;
	}
}


inline void add(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	if (tl >= l && tr <= r) {
		bitset<21> temp(val);
		for (ll i = 1; i <= 20; i++) {
			tag[i][x] = tag[i][x] ^ temp[i - 1];
			if (temp[i - 1]) tree[i][x] = (tr - tl + 1) - tree[i][x];
		}
		return ;
	}
	down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) add(ls(x), tl, mid, l, r, val);
	if (r > mid) add(rs(x), mid + 1, tr, l, r, val);
	push_up(x);
}

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) {
		ll res = 0, power = 1;
		for (ll i = 1; i <= 20; i++) {
			res += tree[i][x] * power;
			power *= 2;
		}
		return res;
	}
	down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls(x), tl, mid, l, r);
	if (r > mid) res += check(rs(x), mid + 1, tr, l, r);
	return res;
}


int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	build(1, 1, n);
	cin >> m;
	while (m--) {
		ll opt;
		cin >> opt;
		if (opt == 1) {
			ll l, r;
			cin >> l >> r;
			cout << check(1, 1, n, l, r) << endl;
		}
		if (opt == 2) {
			ll l, r, val;
			cin >> l >> r >> val;
			add(1, 1, n, l, r, val);
		}
	}

	return ~~ (0 ^ 0);
}

SP1043 GSS1 - Can you answer these queries I

Can you answer these queries 系列问题第一部。

简单的来说就是要求对于一段序列 \(a[l...r]\)

\[\max \{\sum_{k = i}^j a_k | l \le i \le j \le r \} \]

因为没有修改操作,所以这个题用猫树是最佳的,这里只讲一下思路。

好文推荐 这篇文章讲的非常好,在这里复述一下:

很显然对于这个问题如果我们要进行多次查询,每次暴力查找肯定是不行的,所以我们考虑动态规划,处理出所有区间的最大子段和。

首先我们规定变量:

变量名 表示意义
\(maxl\) 区间前缀最大和,即区间紧靠左端点的子段的最大和
\(maxr\) 区间后缀最大和,即区间紧靠右端点的子段的最大和
\(maxn\) 区间最大子段和,所求答案
\(sum\) 表示区间和

然后对于一段区间来说,我们把他分为左右两个小区间(从这里是不是已经能看出二分的思想了?)

最大子段和

其左右两个小区间假设我们对刚刚的四个变量已经处理完毕。我们如何使他转移状态到大区间呢?

显然有:

\[sum = sum_L + sum_R \]

求解 \(maxl\),那么我们有两个选择,直接使用左边区间的 \(maxl\),或者是把左边区间全部选择然后再选取右边区间的\(maxl\)。因为子段必须保持连续性,所以这两个选择是最优的。

因此有(\(maxr\) 同理):

\[maxl = \max\{maxl_L , sum_L + maxl_R \}\\ maxr = \max\{maxr_R , sum_R + maxr_L \} \]

求解 \(maxn\) 我们同样也有两个选择,直接继承和选取左区间的 \(maxl\) 和右区间的 \(maxr\)(这两段是连续的)

\[maxn = \max\{maxn_L , maxn_R , maxr_L + maxl_R\} \]

动态转移方程推出了之后,动规写法就很显然了,然后使用猫树进行优化,这道题你就可以拿下了。

SP1716 GSS3 - Can you answer these queries III

Can you answer these queries 系列问题第三部。

这道题与上一道题的区别就是多了修改操作,猫树肯定行不通了,我们就直接上线段树。

思路刚刚讲了,我们直接上代码(与普通线段树的区别就是 push_up()check()):

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 5e4 + 86;
ll n, m, a[N];

struct node {
	// 分别表示 区间前缀最大和、区间后缀最大和、区间最大子段和、区间和。
	ll maxl, maxr, maxn, sum;
} tree[N << 2];

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}

inline void push_up(ll x) {
	tree[x].sum = tree[ls(x)].sum + tree[rs(x)].sum;
	tree[x].maxl = max(tree[ls(x)].sum + tree[rs(x)].maxl, tree[ls(x)].maxl);
	tree[x].maxr = max(tree[rs(x)].sum + tree[ls(x)].maxr, tree[rs(x)].maxr);
	tree[x].maxn = max({tree[ls(x)].maxn, tree[rs(x)].maxn, tree[ls(x)].maxr + tree[rs(x)].maxl});
}


inline void build(ll x, ll l, ll r) {
	if (l == r) {
		tree[x] = node{a[l], a[l], a[l], a[l]};
		return ;
	}
	ll mid = l + r >> 1;
	build(ls(x), l, mid);
	build(rs(x), mid + 1, r);
	push_up(x);
}

inline node check(ll k, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) {
		return tree[k];
	}
	node x, y, res;
	x = y = res = {INT_MIN, INT_MIN, INT_MIN, INT_MIN};
	ll mid = tl + tr >> 1;
	if (mid >= l) x =  check(ls(k), tl, mid, l, r);
	if (mid < r) y = check(rs(k), mid + 1, tr, l, r);
	res.sum = x.sum + y.sum;
	res.maxl = max(x.sum + y.maxl, x.maxl);
	res.maxr = max(y.sum + x.maxr, y.maxr);
	res.maxn = max({x.maxn, y.maxn, x.maxr + y.maxl});
	return res;
}

inline void change(ll x, ll tl, ll tr, ll pos, ll val) {
	if (tl == tr) {
		tree[x] = node{val, val, val, val};
		return ;
	}
	ll mid = tl + tr >> 1;
	if (pos <= mid) change(ls(x), tl, mid, pos, val);
	if (pos > mid) change(rs(x), mid + 1, tr, pos, val);
	push_up(x);
}

int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	build(1, 1, n);

	cin >> m;
	while (m--) {
		ll opt;
		cin >> opt;
		if (opt == 0) {
			ll x, y;
			cin >> x >> y;
			change(1, 1, n, x, y);
		} else {
			ll l, r;
			cin >> l >> r;
			cout << check(1, 1, n, l, r).maxn << endl;
		}
	}

	return ~~ (0 ^ 0);
}

SP2916 GSS5 - Can you answer these queries V

Can you answer these queries 系列问题第五部。

发现区别于上道题,这道题增加了区间端点的范围限制。

首先,对于区间最大子段和,我们要熟练运用之前讲的转移操作。学会了这个,我们就要开始分类讨论这道题的情况。

我们将左区间记为 \([L_l , L_r]\) ,右区间记为 \([R_l,R_r]\)

  1. 出现 \(L_r < R_l\),即两个区间没有重合部分。

    情况1

    此时,我们只有一种选择方案:

    只能选取左区间的 \(maxl\) 和右区间的 \(maxr\) 以及中间的空白部分的 \(sum\)

    即:

    \[ans = \operatorname{check}(L_l , L_r).maxl + \operatorname{check}(L_r+1 , R_l -1).sum + \operatorname{check}(R_l , R_r).sum \]

  2. 出现 \(L_r \ge R_l\),即两个区间出现重合部分。

    情况2

    这个情况的特殊点在于,两个区间有重叠,那我们就不能只考虑一种方案了。

    分三种情况:

    • 区间 \([R_l , L_r]\)\(maxn\)
    • 区间 \([L_l , R_l)\)\(maxr\) 与区间 \([R_l , R_r]\)\(maxl\)
    • 区间 \([L_l,L_r]\)\(maxr\) 与区间 \((L_r , R_r]\)\(maxl\)

    需要注意的是,后两种情况必须写成如上的开闭情况,否则会造成答案错误。

    比如第二种情况写成 \([L_l , R_l]\)\((R_l, R_r]\) 就是错误的写法,这是为什么呢?因为我们既然将最终的合法答案区间拆成了两段,那我们需要保证这两段区间都是不合法的,所以最终我们才需要进行合并使其变成一个合法区间,否则如果其中一个子区间已经合法我们完全可以直接选择这个区间作为答案备选(因为存在负数,你无法保证两个区间合并后答案是更优还是更劣)。

因此代码就比较冗长了,仔细看。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e4+86;
ll n, m;
ll a[N];

struct node {
	ll maxl, maxr, maxn, sum;
} tree[N << 2];

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}

inline void push_up(ll x) {
	tree[x].sum = tree[ls(x)].sum + tree[rs(x)].sum;
	tree[x].maxl = max(tree[ls(x)].maxl, tree[ls(x)].sum + tree[rs(x)].maxl);
	tree[x].maxr = max(tree[rs(x)].maxr, tree[rs(x)].sum + tree[ls(x)].maxr);
	tree[x].maxn = max({tree[ls(x)].maxn, tree[rs(x)].maxn, tree[ls(x)].maxr + tree[rs(x)].maxl});
}


inline void build(ll x, ll l, ll r) {
	if (l == r) {
		tree[x] = {a[l], a[l], a[l], a[l]};
		return ;
	}
	ll mid = l + r >> 1;
	build(ls(x), l, mid);
	build(rs(x), mid + 1, r);
	push_up(x);
}

inline node check(ll k, ll tl, ll tr, ll l, ll r) {
	if(l > r){// 记得判断越界情况
		return {INT_MIN,INT_MIN,INT_MIN,0}; 
	}
	if (tl >= l && tr <= r) {
		return tree[k];
	}
	ll mid = tl + tr >> 1;
	node x, y, w;
	x = y = w = {INT_MIN, INT_MIN, INT_MIN, 0};
	if (l <= mid) x = check(ls(k), tl, mid, l, r);
	if (r > mid ) y = check(rs(k), mid + 1, tr, l, r);
	w.sum = x.sum + y.sum;
	w.maxl = max(x.maxl, x.sum + y.maxl);
	w.maxr = max(y.maxr, y.sum + x.maxr);
	w.maxn = max({x.maxn, y.maxn, x.maxr + y.maxl});
	return w;
}

int main() {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	ll T;
	cin >> T;
	while (T--) {
		memset(tree, 0, sizeof tree);
		cin >> n;
		for (ll i = 1; i <= n; i++) cin >> a[i];
		build(1, 1, n);
		
		cin >> m;
		while (m--) {
			pair<ll, ll> L, R;
			cin >> L.first >> L.second >> R.first >> R.second;
			if (L.second < R.first) { // 两段区间无交集,取后缀最大+中间区间和+前缀最大
				cout  << check(1, 1, n, L.first, L.second).maxr + check(1, 1, n, L.second + 1, R.first - 1).sum + check(1, 1, n, R.first, R.second).maxl;
			} else { // 区间有交集
				ll a, b, c; // 考虑三种情况
				a = b = c = INT_MIN;
				a = check(1, 1, n, R.first, L.second).maxn;    // 中间最大子段和
				b = check(1, 1, n, L.first, R.first - 1).maxr + check(1, 1, n, R.first, R.second).maxl;
				c = check(1, 1, n, L.first, L.second).maxr + check(1, 1, n, L.second + 1, R.second).maxl;
				cout << max({a, b, c});
			}
			cout << endl;
		}
	}
	
	return ~~ (0 ^ 0);
}

SP2713 GSS4 - Can you answer these queries IV

Can you answer these queries 系列问题第四部。双倍经验

这里我们要介绍势能线段树,又叫吉司机线段树。

我们知道传统的支持区间修改的线段树,我们都是靠懒标记来节省开销的。可以使用懒标记必须要满足下面两个条件:

  1. 区间节点的值可以根据标记来更新.
  2. 标记之间可以快速相互合并.

很显然这道题的开平方操作并不支持懒标记维护,因为这种运算都是依赖于叶子节点的值的,难道就不能在正确时间复杂度范围内解决这道题了吗?

我们无法直接对标记或者是区间的值进行修改。但是如果一直无脑递归到叶子节点,一个一个修改的话,显然时间成本我们是无法接受的。所以我们就要使用势能线段树,其实就是类似于在BFS里进行剪枝。我们发现每一个操作,总会使得其能够接受的继续进行修改的次数越来越少,就好像你一开始位于高空,每次修改会让你的高度下降,当你落到地面时,再对你修改就已经没有意义了。就是这个操作对你而言已经"退化"了。

换句话说,我们考虑对一个 \(1 \times 10 ^ 6\) 级别的大数开根号,最多需要开几次?\(6\) 次左右他就会退化成 \(1\) 了,而 \(\sqrt{1} = 1\),再持续开根号就一点意义也没有了。

所以我们可以进行如下操作:

  1. 在每个节点额外加入一个"势能标记",来记录和维护当前区间结点的势能情况。
  2. 对于每次的区间修改,若当前区间内所有结点的势能皆已为零,直接退出递归不再修改.
  3. 若当前区间内还存在势能不为零的结点,则继续向下递归,暴力修改要求区间内每一个势能不为零的结点.

当然对于这道题来说用不着这么麻烦,很好想到只要让区间和小于等于区间长度的时候就肯定不用进行任何操作了。(这道题数据范围没说数据存不存在 \(0\),但如果存在这种做法将不再适用,可见该帖子)。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5 + 86;
ll a[N], tree[N << 2];

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}
inline void push_up(ll x) {
	tree[x] = tree[ls(x)] + tree[rs(x)];
}

inline void build(ll x, ll l, ll r) {
	if (l == r) {
		tree[x] = a[l];
		return ;
	}
	ll mid = l + r >> 1;
	build(ls(x), l, mid);
	build(rs(x), mid + 1, r);
	push_up(x);
}

inline void change(ll x, ll tl, ll tr, ll l, ll r) {
	if (tree[x] <= tr - tl + 1) return ;
	if (tl == tr) {
		tree[x] = sqrt(tree[x]);
		return ;
	}
	ll mid = tl + tr >> 1;
	if (l <= mid) change(ls(x), tl, mid, l, r);
	if (r > mid ) change(rs(x), mid + 1, tr, l, r);
	push_up(x);
}

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) {
		return tree[x];
	}
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls(x), tl, mid, l, r);
	if (r > mid) res += check(rs(x), mid + 1, tr, l, r);
	return res;
}


int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	ll n, tot = 0;
	while (cin >> n && ++tot) {
		cout << "Case #" << tot << ":" << endl;
		memset(tree, 0, sizeof tree);
		for (ll i = 1; i <= n; i++) {
			cin >> a[i];
		}
		build(1, 1, n);
		ll m;
		cin >> m;
		while (m--) {
			ll opt, l, r;
			cin >> opt >> l >> r;
			if (r < l) swap(l, r);
			if (opt == 0) {
				change(1, 1, n, l, r);
			} else {
				cout << check(1, 1, n, l, r) << endl;
			}
		}
		cout << endl;
	}

	return ~~ (0 ^ 0);
}

P1438 无聊的数列

这道题就有些意思了。

我们单纯维护原数组的话,可以发现每次区间修改的增量对于区间的每一个值都是变化的。

而等差数列这很好的让我们的思绪引入到了差分数组。

我们在树状数组的时候就用到了差分数组的思想,可以使区间操作转化为单点操作,对于这道题来说,我们可以将复杂的区间操作转化为简单的区间操作。

我们进行一下手推样例(\(a_i\) 为原数组,\(c_i\) 为差分数组):

\(\{a_i\}\) \(1\) \(2\) \(3\) \(4\) \(5\)
\(\{c_i\}\) \(1\) \(1\) \(1\) \(1\) \(1\)

第一个操作需要将 \([2,4]\) 区间增加一个首项为 \(k = 1\),公差为 \(d = 2\) 的等差数列我们看这两个数列的增量变化:

\(\{a_i\}\) \(1\) \(2 + 1 = 3\) \(3 + 3 = 6\) \(4 + 5 = 9\) \(5\)
\(\{c_i\}\) \(1\) \(1 + 1 = 2\) \(1 + 2 = 3\) \(1 + 2 = 3\) \(1 - 5 = -4\)

我们主要观察差分数组的变化规律(这里就不继续举例子了,如果你还没看出来,就多举几个例子观察一下),不难发现其变化主要分为三部分:

对于 \(c_l\) 这一项,其增量是由首项带来的,相较于前一项并未增加值,所以这一项变得更大,差分就会变得更大。

而第 \(c[l + 1 ... r]\) 这一段,因为都受到了等差数列的不同程度的增量变化,所以增量主要是由等差数列的公差所造成的,在该区间内的值相邻项增加值的差值很明显就是公差,所以最后差分数组也会增加公差的同等增量。

而对于 \(c_{r+1}\) 这一项,因为 \(a_r\) 变大了,而且其增量是受到该等差数列在这一区间的末项导致的,因此这一项的差分很容易想到会变小。

综上所述,如果调整区间为 \([l,r]\),且将数列首项记为 \(k\) ,公差记为 \(d\) ,末项记为 \(last\)(很容易想到 $last = [(r - l + 1) - 1] \times d $),我们只需要对差分数列进行如下调整就可以:

  • \(c_l = c_l + k\)
  • \(c[l+1...r] = c[l+1...r] + d\)
  • \(c_{r+1} = c_{r+1} - last\)

这一过程用线段树维护区间修改操作就可以,因为最后题目只要求单点查询,而对差分数组求前缀和就可以得到原数组,因此对于每一次询问 \(pos\) 都返回 \([1,pos]\) 的区间和就可以了。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5 + 86;
ll n, m, a[N], c[N];
ll tree[N << 2], tag[N << 2];

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}
inline void push_up(ll x) {
	tree[x] = tree[ls(x)] + tree[rs(x)];
}

inline void add_tag(ll x, ll l, ll r, ll val) {
	tag[x] += val;
	tree[x] += (r - l + 1) * val;
	return ;
}

inline void down_tag(ll x, ll l, ll r) {
	ll mid = l + r >> 1;
	add_tag(ls(x), l, mid, tag[x]);
	add_tag(rs(x), mid + 1, r, tag[x]);
	tag[x] = 0;
}

inline void build(ll x, ll l, ll r) {
	if (l == r) {
		tree[x] = c[l];
		return ;
	}
	ll mid = l + r >> 1;
	build(ls(x), l, mid);
	build(rs(x), mid + 1, r);
	push_up(x);
}

inline void add(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	if (tl >= l && tr <= r) {
		add_tag(x, tl, tr, val);
		return ;
	}
	if (tag[x]) down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) add(ls(x), tl, mid, l, r, val);
	if (r > mid) add(rs(x), mid + 1, tr, l, r, val);
	push_up(x);
}

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) return tree[x];
	if (tag[x]) down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls(x), tl, mid, l, r);
	if (r > mid) res += check(rs(x), mid + 1, tr, l, r);
	return res;
}


int main() {

	cin >> n >> m;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
		c[i] = a[i] - a[i - 1];
	}
	build(1, 1, n);
	while (m--) {
		ll opt;
		cin >> opt;
		if (opt == 1) {
			ll l, r, k, d;
			cin >> l >> r >> k >> d;
			ll len = r - l + 1;
			ll last = k + (len - 1) * d;
			add(1, 1, n, l, l, k);
			if (l + 1 <= r) add(1, 1, n, l + 1, r, d);
			if (r + 1 <= n) add(1, 1, n, r + 1, r + 1, -last);
		}
		if (opt == 2) {
			ll pos;
			cin >> pos;
			cout << check(1, 1, n, 1, pos) << endl;
		}
	}


	return ~~ (0 ^ 0);
}

线段树进阶

在这里就不讲述树状数组的相关内容了,可拓展性太差,而且区间查询区间修改还得背公式。

权值线段树

思路讲解

想象这样一个场景:我们需要实时统计当前集合中数值在 \([L,R]\) 范围内的元素个数,或者快速查询第 \(k\) 大的数值。此时,权值线段树(Weight Segment Tree)便闪亮登场——它巧妙的维护基础从"数组下标"转换为"值域空间",开辟了线段树应用的新维度。

权值线段树一般支持以下三个操作:

  • update() 用于更新新加入的权值对应的结点,然后进行更新该节点的信息。
  • remove() 就是 update() 的反向操作,一般不会使用
  • check() 查询对应值的信息,输出答案。

其实这两个函数的写法就是单点修改区间查询线段树的写法,只不过维护的信息大相径庭。

inline void updata(ll x , ll p , ll tl , ll tr , ll d){
	if(tl == tr){
		tree[x] = d;
		return ;
	}
	ll mid = tl + tr >> 1;
	if(p <= mid) updata(ls(x) , p , tl , mid , d);
	else updata(rs(x) , p , mid + 1, tr , d);
	push_up(x);
}
inline ll check(ll x , ll tl , ll tr , ll l ,ll r){
	if (r < tl || tr < l) return 0;
	if(tl >= l && tr <= r) return tree[x];
	ll mid = tl + tr >> 1;
	return max(check(ls(x) , tl , mid , l, r) , check(rs(x) , mid + 1 , tr , l, r));
}

例题

P3369 【模板】普通平衡树

平衡树和权值线段树解决的问题是高度重合的,因此板题也是一道题。

因为这道题数据量太大了,所以要进行离散化,强制转为离线算法。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e5 + 86;

ll n;
pair<ll, ll> deal[N];
ll sorted[N];
ll tree[N << 2];

inline ll ls(ll x) {
	return x << 1;
}
inline ll rs(ll x) {
	return x << 1 | 1;
}
inline void push_up(ll x) {
	tree[x] = tree[ls(x)] + tree[rs(x)];
}

inline void update(ll x, ll tl, ll tr, ll pos, ll val) {
	if (tl == tr) {
		tree[x] = max(tree[x] + val, 0LL);
		return ;
	}
	ll mid = tl + tr >> 1;
	if (pos <= mid) update(ls(x), tl, mid, pos, val);
	if (pos > mid) update(rs(x), mid + 1, tr, pos, val);
	push_up(x);
}

inline ll check_sum(ll x, ll tl, ll tr, ll l, ll r) { // 区间求和
	if (tl >= l && tr <= r) {
		return tree[x];
	}
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check_sum(ls(x), tl, mid, l, r);
	if (r > mid ) res += check_sum(rs(x), mid + 1, tr, l, r);
	return res;
}
inline ll check_rnk(ll x, ll tl, ll tr, ll pos ) {  // 区间第k小
	if (tl == tr) {
		return tl;
	}
	ll mid = tl + tr >> 1;
	ll res = tree[ls(x)];
	if (res >= pos) {
		return check_rnk(ls(x), tl, mid, pos);
	} else {
		return check_rnk(rs(x), mid + 1, tr, pos - res);
	}
}


int main() {


	// input and 离散化
	cin >> n;
	for (ll i = 1; i <= n; i++) {
		cin >> deal[i].first >> deal[i].second;
		if (deal[i].first == 4) continue;
		sorted[i] = deal[i].second;
	}
	sort(sorted + 1, sorted + n + 1);
	ll len = unique(sorted + 1, sorted + n + 1) - sorted - 1;
	for (ll i = 1; i <= n; i++) {
		if (deal[i].first == 4) continue;
		deal[i].second = lower_bound(sorted + 1, sorted + len + 1, deal[i].second) - sorted;
	}

	// solve
	for (ll i = 1; i <= n; i++) {
		auto [opt, x] = deal[i];
		if (opt == 1) {
			update(1, 1, len, x, +1);
		}
		if (opt == 2) {
			update(1, 1, len, x, -1);
		}
		if (opt == 3) {
			if (x == 1) cout << 1 << endl;
			else cout << check_sum(1, 1, len, 1, x - 1) + 1 << endl;
		}
		if (opt == 4) {
			ll pos = check_rnk(1, 1, len, x);
			cout << sorted[pos] << endl;
		}
		if (opt == 5) {
			ll pos = check_sum(1, 1, len, 1, x - 1);
			cout << sorted[check_rnk(1, 1, len, pos)] << endl;
		}
		if (opt == 6) {
			ll pos = check_sum(1, 1, len, 1, x) + 1;
			cout << sorted[check_rnk(1, 1, len, pos)] << endl;
		}
	}

	return ~~ (0 ^ 0);
}

动态开点线段树

思路讲解

正常的线段树是需要 \(4n\) 的空间复杂度的,在 \(n\) 达到 \(1\times 10 ^ 8\) 的时候就会爆炸了。

很显然有的节点我们可能整组测试数据都不会去使用它,因此,我们可以采取不一次性建好树,而是在最初只建立一个根结点代表整个区间的方法来俭省空间。

我们使用一个 \(cnt\) 来存贮当前的点开到多少编号,然后进行动态调整,很显然这样的线段树儿子节点与父亲节点的关系很显然就不满足 \(2 x\)\(2x + 1\) 的关系了,我们需要再储存一下该节点的左右儿子节点的编号。

单次操作的时间复杂度是不变的,为\(O(\log n)\)。由于每次操作都有可能创建并访问全新的一系列结点,因此 \(m\)次单点操作后结点的数量规模是 \(O(m\log n)\)。最多也只需要 \(2n-1\) 个结点,没有浪费。

const int M = 1e5 * 30 + 86;
ll n, m;
ll tree[M << 2], ls[M << 2], rs[M << 2], tag[M << 2];
ll root, cnt;   // 记录节点编号。

其他的部分和正常线段树主要的区别就是增加了左右子树的相关维护。

首先是 update() 函数,我们此时就需要让其拥有一个返回值,用于返回其儿子的编号然后更新(或者网上更流行引用写法,博主不习惯于那种写法,有需要可自行查找)。

具体流程就是先检查当前节点是否存在,如果不存在就进行动态开点,然后递归进入左右子树,同时回溯时同步更新左右子树节点编号。

inline ll update(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	if (!x) x = ++ cnt;
	if (tl >= l && tr <= r) {
		return add_tag(x, tl, tr, val);
	}
	if (tag[x]) down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) ls[x] = update(ls[x], tl, mid, l, r, val);
	if (r > mid) rs[x] = update(rs[x], mid + 1, tr, l, r, val);
	push_up(x);
	return x;
}

然后就是 check() 函数,这里如果递归进入了空节点说明这里根本就没有任何信息,直接返回零值就可以。

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (!x) return 0;
	if (tl >= l && tr <= r) return tree[x];
	if (tag[x]) down_tag(x, tl, tr);
	ll res = 0;
	ll mid = tl + tr >> 1;
	if (l <= mid) res += check(ls[x], tl, mid, l, r);
	if (r > mid) res += check(rs[x], mid + 1, tr, l, r);
	return res;
}

例题

P13825 【模板】线段树 1.5

这是洛谷的新增板题。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll unsigned long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int M = 1e5 * 30 + 86;
ll n, m;
ll tree[M << 2], ls[M << 2], rs[M << 2], tag[M << 2];
ll root, cnt;   // 记录节点编号。

inline void push_up(ll x) {
	tree[x] = tree[ls[x]] + tree[rs[x]];
}

inline ll add_tag(ll x, ll l, ll r, ll val) {
	if (!x) x = ++cnt;
	tag[x] += val;
	tree[x] += (r - l + 1) * val;
	return x;
}

inline void down_tag(ll x, ll l, ll r) {
	ll mid = l + r >> 1;
	ls[x] = add_tag(ls[x], l, mid, tag[x]);
	rs[x] = add_tag(rs[x], mid + 1, r, tag[x]);
	tag[x] = 0;
}

inline ll update(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	if (!x) x = ++ cnt;
	if (tl >= l && tr <= r) {
		return add_tag(x, tl, tr, val);
	}
	if (tag[x]) down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) ls[x] = update(ls[x], tl, mid, l, r, val);
	if (r > mid) rs[x] = update(rs[x], mid + 1, tr, l, r, val);
	push_up(x);
	return x;
}

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (!x) return 0;
	if (tl >= l && tr <= r) return tree[x];
	if (tag[x]) down_tag(x, tl, tr);
	ll res = 0;
	ll mid = tl + tr >> 1;
	if (l <= mid) res += check(ls[x], tl, mid, l, r);
	if (r > mid) res += check(rs[x], mid + 1, tr, l, r);
	return res;
}


int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n >> m;

	while (m--) {
		ll opt;
		cin >> opt;
		if (opt == 1) {
			ll l, r, val;
			cin >> l >> r >> val;
			root = update(root, 1, n, l, r, val);
		} else {
			ll l, r;
			cin >> l >> r;
			ll org = (r - l + 1) * (l + r) / 2; // 区间初始值
			cout << org + check(root, 1, n, l, r) << endl;
		}
	}

	return ~~ (0 ^ 0);
}

可持久化线段树

思路讲解

是可持久化数据结构的一种,又称为主席树。

主席树的主要思想就是:保存每次插入操作时的历史版本。做一次修改操作,就生成一棵新版本线段树,去处理比较复杂的区间查询问题。

如果生成 \(n\) 个版本的线段树,有 \(m\) 个查询操作,那么单次生成、单次查询的时间复杂度 \(O(log n)\)

主席树

如上图所示,对于一个版本较上一个版本改变的节点,就直接新建,对于未改变的节点,则直接重复利用,这样可以获得最优的空间开销。

所以,主席树是要求动态开点的,而且,因为要解决区间第 \(k\) 小问题,所以也是一颗权值线段树。

综上所述,主席树就是结合了刚刚的两颗线段树的一种数据结构。

需要注意的是,虽然我们已经获得了最优的空间开销,但是因为其空间换时间的基本思想,其空间复杂度依旧很高,数组应该要开到 \(m \log n \times 2^5\) 左右。

例题

P3919 【模板】可持久化线段树 1(可持久化数组)

单点修改 & 单点查询,是最简单的可持久化线段树。

对于单点修改,我们可以对要更改的节点,先将节点复制,再去更改其左右子树。

对于每个版本,我们需要保存它的根节点,这样查询单点 \(x\) 的信息时,根据版本号,从根节点往下找到 \(x\) 位置对应的节点即可。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e6 + 86;

ll n, m;
ll a[N]; // 原始数组
ll tree[N << 5], ls[N << 5], rs[N << 5];   // 可持久化线段树
ll root[N]; // root[i] 表示 i 号版本线段树的头节点编号
ll cnt; // 可持久化线段树的节点空间计数

inline ll build(ll l, ll r) {
	ll root = ++ cnt;
	if (l == r) {
		tree[root] = a[l];
		return root;
	}
	ll mid = l + r >> 1;
	ls[root] = build( l, mid);
	rs[root] = build( mid + 1, r);
	return root;
}

inline ll update(ll x, ll tl, ll tr, ll pos, ll val) {
	ll root = ++ cnt; // 开一颗新树
	// 复制节点信息
	ls[root] = ls[x];
	rs[root] = rs[x];
	tree[root] = tree[x];
	if (tl == tr) {
		tree[root] = val;
		return root;
	}
	ll mid = tl + tr >> 1;
	if (pos <= mid) ls[root] = update(ls[x], tl, mid, pos, val);
	if (pos > mid ) rs[root] = update(rs[x], mid + 1, tr, pos, val);
	return root;
}

inline ll check(ll x, ll tl, ll tr, ll pos) {
	if (tl == tr) return tree[x];
	ll mid = tl + tr >> 1;
	if (pos <= mid) return check(ls[x], tl, mid, pos);
	if (pos > mid) return check(rs[x], mid + 1, tr, pos);
}


int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n >> m;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	root[0] = build(1, n);
	for (ll i = 1; i <= m; i++) {
		ll ver, opt;
		cin >> ver >> opt;
		if (opt == 1) {
			ll pos, val;
			cin >> pos >> val;
			root[i] = update(root[ver], 1, n, pos, val);
		} else {
			ll pos;
			cin >> pos;
			root[i] = root[ver];
			cout << check(root[ver], 1, n, pos) << endl;
		}
	}

	return ~~ (0 ^ 0);
}

P3834 【模板】可持久化线段树 2

单点修改 & 区间查询,区间第 \(k\) 小问题,这是可持久化线段树最经典的应用了。

查询范围 \(l \sim r\) 的信息时,我们使用前缀和思想,利用 \(r\) 版本的信息减去 \(l-1\) 版本的信息。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 2e4 * 10 + 86;

ll tree[N << 5], ls[N << 5], rs[N << 5];
ll root[N], a[N], sorted[N];
ll size[N << 5];
ll cnt, n, m;

inline ll build(ll l, ll r) {
	ll root = ++ cnt;
	if (l == r) return root;
	ll mid = l + r >> 1;
	ls[root] = build(l, mid);
	rs[root] = build(mid + 1, r);
	return root;
}
// 排名范围1~r,增加一个排名为 pos 的数字
inline ll update(ll x, ll tl, ll tr, ll pos) {
	ll root = ++ cnt;
	ls[root] = ls[x];
	rs[root] = rs[x];
	size[root] = size[x] + 1;
	if (tl == tr) return root;
	ll mid = tl + tr >> 1;
	if (pos <= mid) ls[root] = update(ls[x], tl, mid, pos);
	if (pos > mid) rs[root] = update(rs[x], mid + 1, tr, pos);
	return root;
}

// 查询区间第 k 小,从 u 号版本,查到 v 号版本
inline ll check(ll u, ll v, ll tl, ll tr, ll pos) {
	if (tl == tr) return tl;
	// 计算左子树节点的大小
	ll res = size[ls[v]] - size[ls[u]];
	ll mid = tl + tr >> 1;
	if (pos <= res) {
		return check(ls[u], ls[v], tl, mid, pos);
	} else { // 左子树不够
		return check(rs[u], rs[v], mid + 1, tr, pos - res);
	}
}


int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n >> m;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	// 离散化和建树
	memcpy(sorted, a, sizeof(a));
	stable_sort(sorted + 1, sorted + n + 1);
	ll len = unique(sorted + 1, sorted + n + 1) - sorted - 1;
	root[0] = build(1, len);
	for (ll i = 1; i <= n; i++) {
		ll x = lower_bound(sorted + 1, sorted + len + 1, a[i]) - sorted;
		root[i] = update(root[i - 1], 1, len, x);
	}

	while (m--) {
		ll l, r, pos;
		cin >> l >> r >> pos;
		cout << sorted[check(root[l - 1], root[r], 1, len, pos)] << endl;
	}

	return ~~ (0 ^ 0);
}

P1972 SDOI2009 HH 的项链

这道题的核心思想是:将“区间颜色数”问题转化为“区间权值大于某值的个数”问题,然后用主席树在线回答每一次询问。(其实这道题离线做法最简单,但我们学习的是主席树啊)

简单的说,我们把颜色出现位置转化为“\(next\) 数组”,然后用主席树查询“区间中 \(next\) 值大于 \(r\) 的个数”,即为颜色种类数。

为什么要这样做呢?我们要统计区间 \([l, r]\) 中有多少种不同的颜色种类,因此一个颜色在 \([l, r]\) 中只应被计数一次,即它最后一次出现在该区间的位置。为了实现这样的效果,我们预处理 \(next\) 数组,对于每个位置 \(i\),定义:\(next_i\) 为下一个与 \(a_i\) 颜色相同的颜色出现的位置,如果不存在,那就赋值为 \(n+1\)

这样就保证了一个位置 \(i\) 是“在区间 \([l,r]\) 最后一次出现”的充要条件是:\(next_i > r\)

所以问题转化为:在区间 \([l, r]\) 中,有多少个 \(i\) 满足 \(next[i] > r\)

很显然要用主席树进行处理,维护一颗可持久化的以 \(next_i\) 为权值的权值线段树,然后对于每次询问进行在线查询就可以了。

这道题我的方法必须使用快读快写。被卡常了呜呜呜。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll int
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e6 + 86;

ll n, m, a[N];
ll tree[N << 5], ls[N << 5], rs[N << 5];
ll root[N], cnt;
ll behind[N], pre[N];

template<class T>
void read(T &x) {
	char c;
	bool flag = false;
	while ((c = getchar()) < '0' || c > '9')
		if (c == '-') flag = true;
	x = c ^ 48;
	while ((c = getchar()) >= '0' && c <= '9')
		x = (x << 3) + (x << 1) + (c ^ 48);
	if (flag) x = ~x + 1;
}

template<class T>
void print(T x) {
	static char buf[20];
	int len = 0;
	if (x < 0) putchar('-'), x = -(unsigned long long)x;
	if (x == 0) buf[len++] = '0';
	else while (x) buf[len++] = x % 10 + '0', x /= 10;
	while (len--) putchar(buf[len]);
}

inline ll clone(ll x) {
	ll root = ++cnt;
	tree[root] = tree[x];
	ls[root] = ls[x];
	rs[root] = rs[x];
	return root;
}

inline void push_up(ll x) {
	tree[x] = tree[ls[x]] + tree[rs[x]];
}

inline ll build(ll l, ll r) {
	ll root = ++ cnt;
	if (l == r) {
		return root;
	}
	ll mid = l + r >> 1;
	ls[root] = build(l, mid);
	rs[root] = build(mid + 1, r);
	return root;
}

inline ll update(ll x, ll tl, ll tr, ll pos) {
	ll root = clone(x);
	if (tl == tr) {
		tree[root]++;
		return root;
	}
	ll mid = tl + tr >> 1;
	if (pos <= mid) ls[root] = update(ls[root], tl, mid, pos);
	if (pos > mid) rs[root] = update(rs[root], mid + 1, tr, pos);
	push_up(root);
	return root;
}

inline ll check(ll u, ll v, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) {
		return tree[v] - tree[u];
	}
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls[u], ls[v], tl, mid, l, r);
	if (r > mid) res += check(rs[u], rs[v], mid + 1, tr, l, r);
	return res;
}

int main() {
	read(n);
	for (ll i = 1; i <= n; i++) {
		read(a[i]);
		behind[pre[a[i]]] = i;
		behind[i] = n + 1;
		pre[a[i]] = i;
	}
	root[0] = build(1, n + 1);
	for (ll i = 1; i <= n; i++) {
		root[i] = update(root[i - 1], 1, n + 1, behind[i]);
	}

	read(m);
	while (m--) {
		ll l, r;
		read(l);
		read(r);
		ll ans = check(root[l - 1], root[r], 1, n + 1, r + 1, n + 1);
		print(ans);
		putchar(endl);
	}
	return ~~ (0 ^ 0);
}

P5648 Mivik的神力

难度预警!!!

这道题就是要求:

\[\sum_{i=l}^{l+q-1} \max_{l\le j\le i}a_j \]

这相当于从位置 \(l\) 开始向右扫描,过程中维护当前遇到的最大值,并将所有最大值累加起来。

我们从最暴力的做法开始想起,对于每次查询 \(O(n^2)\) 的做法肯定是显然的:

ll r = l + q - 1;
ll ans = 0;
for(ll i = l;i <= r;i++){
    ll maxn = INT_MIN;
    for(ll j = l;j <= i;j++){
        maxn = max(a[j] , maxn);
    }
    ans += maxn;
}
cout << ans << endl;

这样总复杂度达到了 \(O(Tn^2)\) ,是显然超时的,我们进行逐步优化:

我们在 单调队列 & 单调栈 的时候学过可以利用单调栈正反 \(O(n)\) 处理两次来预处理最值区间。

// 用单调栈预处理最大值区间:
stack<ll> st;
st.push(0);
for (ll i = 1; i <= n; i++) {
	while (!st.empty() && a[st.top()] <= a[i]) st.pop();
	l[i] = st.top() + 1;
	st.push(i);
}
while (!st.empty()) st.pop();
st.push(n + 1);
for (ll i = n; i >= 1; i--) {
	while (!st.empty() && a[st.top()] <= a[i]) st.pop();
	r[i] = st.top() - 1;
	st.push(i);
}

这样可以让时间达到 \(O(n)\) 预处理,\(O(Tn)\) 时间查询,还是无法达到满分。

因此我们可以使用线段树进行优化了,对每次查询的复杂度优化到 \(O(\log n)\)。因为这道题强制在线,所以我们考虑主席树。

如何进行建树呢?我们可以考虑对于每一个 \(a_i\) 处理出的最大值区间 \([L_i,R_i]\) ,在一颗权值线段树上将这一段区间上的值覆盖更新为 \(a_i\),这样我们在查询的时候只需要查询 \([l,r]\) 区间范围内的区间和就可以了,对于一颗线段树来说,这样的查询复杂度很明显是可以降到 \(O(\log n)\) 的。

但是因为我们查询的时候是从 \(l\) 开始进行正向扫描的,如果我们也顺序建树,那么区间上的信息版本和查询时对应的版本是不同的,就比如我们第一步会需要使用 \(a_l\) 的相关信息来操作,但是 \(a_l\) 的信息因为建树原因已经被 \(a_r\) 覆盖掉了,那就查询错误了,所以我们是逆向建立各个版本的树,这样可以保证查询顺序最靠前的 \(a_i\) 在版本最上层。

对于代码来说,我们发现只有区间覆盖操作和区间查询,之前我们典型的主席树都是单点修改的,所以没有涉及到 lazy-tag 的问题,但这次我们需要进行处理了,这里我提供两种思路,一种是正常懒标记下传,一种可以使用标记永久化进行优化(均可通过本题)。

懒标记下传(约800ms,120mb):

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 5e5 + 86;
int n, m, a[N], l[N], r[N];
// 构建主席树
ll tree[N << 5], ls[N << 5], rs[N << 5];
int root[N], tag[N << 5], cnt;

inline pair<ll, ll> unlock(ll u, ll v, ll last) {
	ll l = 1 + (u ^ last) % n;
	ll len = 1 + (v ^ (last + 1)) % (n - l + 1);
	return make_pair(l, l + len - 1);
}


inline ll clone(ll x) {
	ll root = ++ cnt;
	tree[root] = tree[x];
	ls[root] = ls[x];
	rs[root] = rs[x];
	tag[root] = tag[x];
	return root;
}

inline void push_up(ll x) {
	tree[x] = tree[ls[x]] + tree[rs[x]];
}

inline void add_tag(ll x, ll l, ll r, ll val) {
	tag[x] = val;
	tree[x] = (r - l + 1) * val;
	return ;
}

inline void down_tag(ll x, ll l, ll r) {
	ll mid = l + r >> 1;
	// 克隆子节点,避免修改历史版本
	ls[x] = clone(ls[x]);
	add_tag(ls[x], l, mid, tag[x]);
	rs[x] = clone(rs[x]);
	add_tag(rs[x], mid + 1, r, tag[x]);
	tag[x] = 0;
}

inline ll build(ll l, ll r) {
	ll root = ++ cnt;
	if (l == r) {
		return root;
	}
	ll mid = l + r >> 1;
	ls[root] = build(l, mid);
	rs[root] = build(mid + 1, r);
	return root;
}

// 区间 l ~ r  覆盖为 val;
inline ll update(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	ll root = clone(x);
	if (tl >= l && tr <= r) {
		add_tag(root, tl, tr, val);
		return root;
	}
	if (tag[root]) down_tag(root, tl, tr);
	ll mid = tl + tr >> 1;
	if (l <= mid) ls[root] = update(ls[root], tl, mid, l, r, val);
	if (r > mid) rs[root] = update(rs[root], mid + 1, tr, l, r, val);
	push_up(root);
	return root;
}

inline ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (tl >= l && tr <= r) return tree[x];
	if (tag[x]) down_tag(x, tl, tr);
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls[x], tl, mid, l, r);
	if (r > mid) res += check(rs[x], mid + 1, tr, l, r);
	return res;
}

int main() {

	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n >> m;
	memset(a, 0x3f, sizeof a);
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	// 用单调栈预处理最大值区间:
	stack<ll> st;
	st.push(0);
	for (ll i = 1; i <= n; i++) {
		while (!st.empty() && a[st.top()] <= a[i]) st.pop();
		l[i] = st.top() + 1;
		st.push(i);
	}
	while (!st.empty()) st.pop();
	st.push(n + 1);
	for (ll i = n; i >= 1; i--) {
		while (!st.empty() && a[st.top()] <= a[i]) st.pop();
		r[i] = st.top() - 1;
		st.push(i);
	}

	root[n + 1] = build(1, n);
	for (ll i = n; i >= 1; i--) {
		root[i] = update(root[i + 1], 1,  n, l[i], r[i], a[i]);
	}

	ll last = 0;
	while (m --) {
		ll u, v;
		cin >> u >> v;
		auto[l, r] = unlock(u, v, last);
		last = check(root[l], 1, n, l, r);
		cout << last << endl;
	}

	return ~~ (0 ^ 0);
}

标记永久化(约650ms,120mb):

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define const constexpr
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 5e5 + 86;
int n, m, a[N], l[N], r[N];
ll tree[N << 4], ls[N << 4], rs[N << 4];
int root[N], tag[N << 4], cnt;

inline pair<ll, ll> unlock(ll u, ll v, ll last) {
	ll l = 1 + (u ^ last) % n;
	ll len_max = n - l + 1;
	ll len = 1 + (v ^ (last + 1)) % len_max;
	return make_pair(l, l + len - 1);
}

inline ll clone(ll x) {
	ll root = ++cnt;
	tree[root] = tree[x];
	ls[root] = ls[x];
	rs[root] = rs[x];
	tag[root] = tag[x]; // 复制标记
	return root;
}

inline void push_up(ll x) {
	if (tag[x]) return; // 有标记时无需合并子树
	tree[x] = tree[ls[x]] + tree[rs[x]];
}

inline void add_tag(ll x, ll l, ll r, ll val) {
	tag[x] = val;
	tree[x] = val * (r - l + 1);
}

inline void down_tag(ll x, ll l, ll r) {
	// 若当前节点有标记,先复制子节点并应用标记
	ll mid = l + r >> 1;
	ls[x] = clone(ls[x]);
	add_tag(ls[x], l, mid, tag[x]);
	rs[x] = clone(rs[x]);
	add_tag(rs[x], mid + 1, r, tag[x]);
	tag[x] = 0;
}

inline ll build(ll l, ll r) {
	ll root = ++cnt;
	tag[root] = 0;
	if (l == r) {
		tree[root] = 0;
		return root;
	}
	ll mid = (l + r) >> 1;
	ls[root] = build(l, mid);
	rs[root] = build(mid + 1, r);
	push_up(root);
	return root;
}

ll update(ll x, ll tl, ll tr, ll l, ll r, ll val) {
	ll root = clone(x);
	if (tl >= l && tr <= r) {
		add_tag(root, tl, tr, val);
		return root;
	}
	// 不下放标记,直接基于当前标记处理子树
	ll mid = (tl + tr) >> 1;
	if (tag[root]) down_tag(root, tl, tr);
	if (l <= mid) ls[root] = update(ls[root], tl, mid, l, r, val);
	if (r > mid) rs[root] = update(rs[root], mid + 1, tr, l, r, val);
	push_up(root);
	return root;
}

ll check(ll x, ll tl, ll tr, ll l, ll r) {
	if (l <= tl && tr <= r) {
		return tree[x];
	}
	if (tag[x]) {
		// 有标记时直接计算
		ll templ = max(tl, l), tempr = min(tr, r); // 防止计算越界信息
		return tag[x] * (tempr - templ + 1);
	}
	ll mid = tl + tr >> 1;
	ll res = 0;
	if (l <= mid) res += check(ls[x], tl, mid, l, r);
	if (r > mid) res += check(rs[x], mid + 1, tr, l, r);
	return res;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	cin >> n >> m;
	memset(a, 0x3f, sizeof a);
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}
	// 用单调栈预处理最大值区间:
	stack<ll> st;
	st.push(0);
	for (ll i = 1; i <= n; i++) {
		while (!st.empty() && a[st.top()] <= a[i]) st.pop();
		l[i] = st.top() + 1;
		st.push(i);
	}
	while (!st.empty()) st.pop();
	st.push(n + 1);
	for (ll i = n; i >= 1; i--) {
		while (!st.empty() && a[st.top()] <= a[i]) st.pop();
		r[i] = st.top() - 1;
		st.push(i);
	}

	root[n + 1] = build(1, n);
	for (ll i = n; i >= 1; i--) {
		root[i] = update(root[i + 1], 1,  n, l[i], r[i], a[i]);
	}


	ll last = 0;
	while (m--) {
		ll u, v;
		cin >> u >> v;
		auto [l, r] = unlock(u, v, last);
		last = check(root[l], 1, n, l, r);
		cout << last << endl;
	}

	return 0;
}
posted @ 2025-11-11 09:24  Justskr  阅读(64)  评论(0)    收藏  举报