可持久化线段树

定义

建出的存储信息为不同版本情况的线段树(又称主席树)


核心思想

但直接每次复制一次,空间时间复杂度受不了

于是我们每次只新建有修改的点,没修改的直接连到老版本上

因此动态开点,记录新的根即可

注意它“认子不认父”

时间空间复杂度均为 \(O(nlogn)\)

运用时要注意,找到前后序列的变化,


代码:

流程也跟着代码推出来

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

0. 建树(有时可以省略)

inline ll build(ll l, ll r, ll p)
{
	p = ++idx; // 开点
	if(l == r)
	{
		tree[p] = a[l];
		return p;
	}
	ll mid = (l + r) >> 1;
	lc[p] = build(l, mid, lc[p]), rc[p] = build(mid + 1, r, rc[p]); // 单独用数组存子节点
	return p; // 动态开点的线段树返回新开的节点
}
root[0] = build(1, n, 1); // main中

1. 修改

与以前的版本共同递归下去,先复制以前版本,再做修改,同时更新所在版本的根节点

inline ll update(ll last, ll id, ll val, ll l, ll r)
{
	ll p = ++idx; 
   tree[p] = tree[last], lc[p] = lc[last], rc[p] = rc[last]; // 指向上个版本,复制上个版本的对应信息	
	if(l == r && l == id)	
	{
		tree[p] = val;
		return p;
	}
	ll mid = (l + r) >> 1;
	if(mid >= id)	lc[p] = update(lc[last], id, val, l, mid);
	else	rc[p] = update(rc[last], id, val, mid + 1, r, );
	return p;
}
root[i] = update(root[ban], loc, val, 1, n, root[i]); // main

2. 查询

这里就跟普通线段树相同,\(p\) 一开始为查找的对应版本的根

inline ll query(ll id, ll l, ll r, ll p)
{
	if(l == r && l == id)	return tree[p];
	ll mid = (l + r) >> 1;
	if(mid >= id)	return query(id, l, mid, lc[p]);
	else	return query(id, mid + 1, r, rc[p]);
}

应用

1. 静态区间第 \(k\)

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

这个题利用的是主席树维护信息的可减性

建可持久化权值线段树,对应权值离散化后在对应位置插入

\(1\sim n\),每个位置都是一个版本,不用建树,修改相同

查询则类似于平衡树上找第 \(k\) 小的思想:直接在树上二分

\(tree[l[now]]-tree[l[pre]]\ge x\) 时,进入左边

这个式子的含义就是 \(l\sim r\) 区间中比 \(mid\) 小的数的个数

否则进入右边,注意 \(x\)减去左子树的节点数(同平衡树)

inline ll update(ll pre, ll id, ll val, ll l, ll r)
{
	ll p = ++idx; 
	lc[p] = lc[pre], rc[p] = rc[pre], tree[p] = tree[pre] + val;
	if(l == r && l == id)	return p;
	ll mid = (l + r) >> 1;
	if(mid >= id)	lc[p] = update(lc[pre], id, val, l, mid);
	else	rc[p] = update(rc[pre], id, val, mid + 1, r);
	return p;
}
inline ll query(ll pre, ll noww, ll x, ll l, ll r)
{
	if(l >= r)	return l;
	ll mid = (l + r) >> 1, res = tree[lc[noww]] - tree[lc[pre]];
	if(res >= x)	return query(lc[pre], lc[noww], x, l, mid);
	else	return query(rc[pre], rc[noww], x - res, mid + 1, r);
}
// main 函数中
for(reg ll i = 1; i <= n; ++i)	root[i] = update(root[i - 1], lsh[a[i]], 1, 1, cnt);
for(reg ll i = 1; i <= m; ++i)
{
	li = read(), ri = read(), ki = read();
	print(rel[query(root[li - 1], root[ri], ki, 1, cnt)]), putchar('\n'); 
}

2. 找区间绝对众数

P3567 [POI2014]KUR-Couriers

这里用到一个性质:(值域为 \(l\sim r\),中间为 \(mid\)

若有绝对众数,则在值域为 \(l\sim mid\)\(mid+1\sim r\) 两段中一定有且仅有一段数的总个数大于总区间长度的一半

就转化为上面的题,建可持久化权值线段树查询每段中的数个数并判断,注意特判无解的情况

inline int update(int pre, int id, int val, int l, int r)
{
	int p = ++idx, mid = (l + r) >> 1;
	lc[p] = lc[pre], rc[p] = rc[pre], tree[p] = tree[pre] + val;
	if(l == r && l == id)	return p;
	if(mid >= id)	lc[p] = update(lc[pre], id, val, l, mid);
	else	rc[p] = update(rc[pre], id, val, mid + 1, r);
	return p;
}
inline int query(int pre, int noww, int l, int r, int x)
{
	if(l >= r)	return l;
	int mid = (l + r) >> 1, sl = tree[lc[noww]] - tree[lc[pre]], sr = tree[rc[noww]] - tree[rc[pre]];
	if(sl > x)	return query(lc[pre], lc[noww], l, mid, x);
	if(sr > x)	return query(rc[pre], rc[noww], mid + 1, r, x);
	return 0;
}
// main 中
for(reg int i = 1; i <= n; ++i)	a = read(), root[i] = update(root[i - 1], a, 1, 1, n);
for(reg int i = 1; i <= m; ++i)
{
	li = read(), ri = read();
	print(query(root[li - 1], root[ri], 1, n, (ri - li + 1) >> 1)), putchar('\n');
}

3. 在线找区间内某一值域内的数

仍然用前缀相减的思路,查出 \(1\sim r,1\sim l- 1\)

对每个前缀开权值线段树,再按序列下标可持久化

如果卡空间且可以离线,也可以用树状数组 + 扫描线

P4587 [FJOI2016]神秘数

\(ans\) 的意思是当前没有被表示的最小数

如果区间内值在 \(1\sim ans\) 的数的和 \(tot\ge ans\)

意味着凑出 \(1\sim ans-1\) 后还有多余和 \(x\)\(x=tot-(ans-1)\)

\(1\sim tot\) 都可以凑出,因为 \(ans\sim tot\) 中的数 \(-x\) 后在 \(1\sim ans-1\) 内,可以凑

\(ans\) 就变为 \(tot+1\),直至 \(tot<ans\),无法继续了

复杂度 ?

考虑从 \(ans\to tot+1\) 后又扩展一次

那么 \(1\sim tot+1\) 中数的和要 \(\ge tot+1\),此时 \(1\sim ans\) 和为 \(tot\)

说明 \(ans+1\sim tot+1\) 中一定有数

那么 \(1\sim tot+1\) 中数的和即大于 \(2\times ans\)\(ans\) 扩展两次至少翻倍

因此只会扩展 \(\log V\)

用主席树维护值域

inline int update(int pre, int id, int val, int l, int r)
{
	int p = ++idx;
	ls[p] = ls[pre], rs[p] = rs[pre], sum[p] = sum[pre] + val;
	if(l == r)	return p;
	int mid = (l + r) >> 1;
	if(mid >= id)	ls[p] = update(ls[pre], id, val, l, mid);
	else	rs[p] = update(rs[pre], id, val, mid + 1, r);
	return p;
}
inline int query(int pre, int nw, int l, int r, int nl, int nr)
{
	if(l <= nl && nr <= r)	return sum[nw] - sum[pre];
	int mid = (nl + nr) >> 1, res = 0;
	if(mid >= l)	res += query(ls[pre], ls[nw], l, r, nl, mid);
	if(mid < r)	res += query(rs[pre], rs[nw], l, r, mid + 1, nr);
	return res;
}
int main()
{
	n = read();
	for(reg int i = 1; i <= n; ++i)	a[i] = read();
	m = read();
	for(reg int i = 1; i <= n; ++i)	root[i] = update(root[i - 1], a[i], a[i], 1, inf);
	for(reg int i = 1; i <= m; ++i)
	{
		li = read(), ri = read(), ans = 1;
		while(1)
		{
			tot = query(root[li - 1], root[ri], 1, ans, 1, inf);
			if(tot >= ans)	ans = tot + 1;
			else	break;
		}
		print(ans), putchar('\n');
	}
	return 0;
} 

4. 区间增加,之后找单点前 k 小

P3168 [CQOI2015] 任务查询系统

想对每个时刻维护以重要度为下标的权值线段树

但是空间不够,也不能一个个加入区间内元素

由于之后一次性询问,可以将区间差分,利用前缀相减,得到这个时刻的权值线段树

发现每个位置对上个位置,总共有 \(O(n)\) 个修改,可以用主席树维护

然后在主席树上二分即可

inline ll update(ll pre, ll id, ll val, ll l, ll r)
{
	ll p = ++idx;
	sum[p] = sum[pre] + val * id, num[p] = num[pre] + val, ls[p] = ls[pre], rs[p] = rs[pre];
	if(l == r)	return p;
	ll mid = (l + r) >> 1;
	if(mid >= id)	ls[p] = update(ls[pre], id, val, l, mid);
	else	rs[p] = update(rs[pre], id, val, mid + 1, r);
	return p;
}
inline ll query(ll p, ll k, ll l, ll r)
{
	if(l == r)	return min(k, num[p]) * l;
	ll mid = (l + r) >> 1;
	if(num[ls[p]] >= k)	return query(ls[p], k, l, mid);
	else	return query(rs[p], k - num[ls[p]], mid + 1, r) + sum[ls[p]];
}
int main()
{
	m = read(), n = read();
	for(int i = 1; i <= m; ++i)
	{
		u = read(), v = read(), d = read();
		lin[u].pb(mp(d, 1)), lin[v + 1].pb(mp(d, -1));
	}
	for(int i = 1; i <= n; ++i)
	{
		root[i] = root[i - 1];
		for(pll j : lin[i])	root[i] = update(root[i], j.fi, j.se, 1, cnt);
	}
	for(int i = 1; i <= n; ++i)
	{
		xi = read(), ai = read(), bi = read(), ci = read(), ki = (ai * last + bi) % ci + 1;
		last = query(root[xi], ki, 1, cnt);
		print(last), putchar('\n');
	}
	return 0;
}

5. 找到相对上一位置变化较小的信息,维护

P2839 [国家集训队]middle

很有意思的题

首先发现直接优化不好做,这时一般考虑二分答案,发现答案确实有单调性

如果已知答案是 \(x\),把 \(<x\) 的数看作 \(-1\)\(\ge x\) 的数看作 \(1\)

如果左端点在 \([a,b]\),右端点在 \([c,d]\) 的区间有和 \(\ge 0\) 的区间,那么就符合要求,否则不行

想到这我就一直在想随着每次二分的答案变化,感觉 \(-1,1\) 变化很多,不好维护

但是我忽略了:这是可以二分前预处理的!

如果把数按值从大到小排序,那么每个位置相对于前面总共只有 \(O(n)\) 个位置变化

那么就可以主席树维护出这棵以序列位置为下标的线段树

维护区间和,前缀和的最大值,后缀和的最大值

inline int cmp(const int &c, const int &d)
{
	if(c != d)	return a[c] < a[d];
	return c < d;
}
struct segt
{
	int sum, ls, rs, lmx, rmx;
}tree[M];
inline segt pushup(segt p, segt lson, segt rson)
{
	p.sum = lson.sum + rson.sum;
	p.lmx = max(lson.lmx, lson.sum + rson.lmx), p.rmx = max(rson.rmx, rson.sum + lson.rmx);
	return p;
}
inline int build(int l, int r)
{
	int p = ++idx;
	if(l == r)
	{
		tree[p] = (segt){1, 0, 0, 1, 1};
		return p;
	}
	int mid = (l + r) >> 1;
	tree[p].ls = build(l, mid), tree[p].rs = build(mid + 1, r);
	tree[p] = pushup(tree[p], tree[tree[p].ls], tree[tree[p].rs]);	return p; 
}
inline int update(int pre, int id, int l, int r)
{
	int p = ++idx;
	tree[p] = tree[pre], tree[p].sum -= 2;
	if(l == r)
	{
		tree[p].lmx = tree[p].rmx = -1;
		return p;
	}
	int mid = (l + r) >> 1;
	if(mid >= id)	tree[p].ls = update(tree[pre].ls, id, l, mid);
	else	tree[p].rs = update(tree[pre].rs, id, mid + 1, r);
	tree[p] = pushup(tree[p], tree[tree[p].ls], tree[tree[p].rs]);	return p;
}
inline segt query(int p, int l, int r, int nl, int nr)
{
	if(l <= nl && nr <= r)	return tree[p];
	int mid = (nl + nr) >> 1;	segt res;	res.ls = res.rs = 0;
	if(mid >= l && mid < r)
	{
		res = pushup(res, query(tree[p].ls, l, r, nl, mid), query(tree[p].rs, l, r, mid + 1, nr));
		return res;
	}
	if(mid >= l)	return query(tree[p].ls, l, r, nl, mid);
	return query(tree[p].rs, l, r, mid + 1, nr);	
}
inline int check(int x)
{
	ans = 0;
	if(q[2] < q[3] - 1)	ans = query(root[x], q[2] + 1, q[3] - 1, 1, n).sum;
	ans += query(root[x], q[1], q[2], 1, n).rmx + query(root[x], q[3], q[4], 1, n).lmx;
	if(ans >= 0)	return 1;
	return 2;
}
int main()
{
	n = read();
	for(int i = 1; i <= n; ++i)	a[i] = read(), id[i] = i;
	sort(id + 1, id + n + 1, cmp), root[1] = build(1, n);
	for(int i = 2; i <= n; ++i)	root[i] = update(root[i - 1], id[i - 1], 1, n);
	Q = read();
	for(int i = 1; i <= Q; ++i)
	{
		q[1] = (read() + las) % n + 1, q[2] = (read() + las) % n + 1;
	    q[3] = (read() + las) % n + 1, q[4] = (read() + las) % n + 1;
		sort(q + 1, q + 5);
		int l = 1, r = n;
		while(l < r)
		{
			int mid = (l + r + 1) >> 1;
			if(check(mid) == 1)	l = mid;
			else	r = mid - 1;
 		}
		las = a[id[l]], print(las), putchar('\n'); 
	}
	return 0;
}

6. 优化贪心过程

P3293 [SCOI2016] 美味

发现异或值最大?二进制下按位贪心是常见思路

但每次加上一个数,可持久化 trie 树不好做

假设现在从最高位开始取,发现贪心过程中前面的位已经固定了

想让当前位为 \(1\),根据 \(x\) 的值也可确定这一位想要 \(0/1\)

也就是说,想要取的数的范围确定了

\([l,r]\) 内找有没有值域为 \([a,b]\) 的数,这就是主席树维护的经典问题

时间复杂度 \(O(q\log n\log V)\)

#include<bits/stdc++.h>
using namespace std;

const int N = 200010, M = 4000010, mx = 200001;
int n, m, a[N], bi, xi, li, ri, ans, idx, ll, rr, tot, sum[M], ls[M], rs[M], root[N];
inline int read()
{
	char ch = getchar();	int x = 0;
	while(ch < '0' || ch > '9')	ch = getchar();
	while(ch >= '0' && ch <= '9')	x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x;
}
inline void print(int x)
{
	if(x / 10)	print(x / 10);
	putchar(x % 10 + '0');
}
inline int update(int pre, int id, int val, int l, int r)
{
	int p = ++idx;
	sum[p] = sum[pre] + val, ls[p] = ls[pre], rs[p] = rs[pre];
	if(l == r)	return p;
	int mid = (l + r) >> 1;
	if(mid >= id)	ls[p] = update(ls[pre], id, val, l, mid);
	else	rs[p] = update(rs[pre], id, val, mid + 1, r);
	return p;
}
inline int query(int pre, int nw, int l, int r, int nl, int nr)
{
	if(l <= nl && nr <= r)	return sum[nw] - sum[pre];
	int mid = (nl + nr) >> 1, res = 0;
	if(mid >= l)	res += query(ls[pre], ls[nw], l, r, nl, mid);
	if(mid < r)	res += query(rs[pre], rs[nw], l, r, mid + 1, nr);
	return res;
}
int main()
{
	n = read(), m = read();
 	for(int i = 1; i <= n; ++i)
 	{
 		a[i] = read();
 		root[i] = update(root[i - 1], a[i] + 1, 1, 1, mx);
    }
    for(int i = 1; i <= m; ++i)
    {
    	bi = read(), xi = read(), li = read(), ri = read();
    	ans = tot = 0, ll = rr = 0;
    	for(int j = 18; j >= 0; --j)
    	{
    		if((bi >> j) & 1)	ll = ans - xi, rr = ans + (1 << j) - xi - 1;
    		else	ll = ans + (1 << j) - xi, rr = ans + (1 << (j + 1)) - xi - 1;
    		ll = max(ll, 0), rr = min(rr, mx - 1);
    		if(query(root[li - 1], root[ri], ll + 1, rr + 1, 1, mx))	ans += ((bi >> j) & 1) ? 0 : (1 << j), tot += (1 << j);
			else	ans += ((bi >> j) & 1) ? (1 << j) : 0;
		}
		print(tot), putchar('\n');
	}
	return 0;
}
posted @ 2024-02-15 10:31  KellyWLJ  阅读(29)  评论(0编辑  收藏  举报