可持久化 树

可持久化


可持久化线段树


注意到 这里的内容 可能包括了 狭义的

可持久化线段树,可持久化权值线段树,”主席树“,可持久化 \(Trie\)...



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

特定版本 单点修改,特定版本 单点查询,每次操作 生成新版本

单点查询 则 在 给定版本基础上 生成 一样的版本

就是 主席树(可持久化线段树)板子,这个东西不多讲

简单来说就是 利用线段树 单点修改只影响 \(\log N\) 个区间的性质

在 原树 上每次修改 新增 \(\log N\) 个修改后的节点,记录新节点的根即可

注意到此时的 “主席树” 实质上 不是一棵树,而是一个 \(DAG\)

好图,偷了

~

然后这个题就很简单了,看代码就行

#include <bits/stdc++.h>

const int MAXN = 1000005;
const int LOGN = 24;

using namespace std;

int Num[MAXN];

namespace PSDTree {
	struct Node {
		int L, R, v, lc, rc;
	} T[MAXN * LOGN];
	
	int RT[MAXN], tot, Cnt;
	
	#define LC T[x].lc
	#define RC T[x].rc
	#define M ((T[x].L + T[x].R) >> 1)
	
	inline int NewNode (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline int Build (const int L, const int R, int x = 0) {
		x = ++ tot, T[x].L = L, T[x].R = R;
		if (L == R) return T[x].v = Num[L], x;
		LC = Build (L, M), RC = Build (M + 1, R);
		return x;
	}
	
	inline int Modify (const int p, const int v, int x = 0) {
		x = NewNode (x);
		if (T[x].L == T[x].R) return T[x].v = v, x;
		if (p <= M) LC = Modify (p, v, LC);
		else RC = Modify (p, v, RC);
		return x;
	}
	
	inline int Query (const int p, const int x = 0) {
		if (T[x].L == T[x].R) return T[x].v;
		if (p <= M) return Query (p, LC);
		else return Query (p, RC);
	}
	
	#undef M
}

using namespace PSDTree;

int N, M;
int Ver, Opt, P, x;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> M;
	
	for (int i = 1; i <= N; ++ i) cin >> Num[i];
	
	RT[Cnt ++] = Build (1, N);
	
	for (int i = 1; i <= M; ++ i, ++ Cnt) {
		cin >> Ver >> Opt >> P;
		if (Opt == 1) cin >> x, RT[Cnt] = Modify (P, x, RT[Ver]);
		if (Opt == 2) RT[Cnt] = RT[Ver], cout << Query (P, RT[Ver]) << '\n';
	}
	
	return 0;
}

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

给定有 \(N\) 个正整数构成的序列 \(a\),查询区间 \([L, R]\) 里面的 第 \(k\) 小值

就是 可持久化权值线段树 的板子,如果是 动态区间 \(k\) 就得 树套树

我们 每插入一个数就新建一个版本一个区间 其实就是 两个版本的差值

然后查询的时候就是 两树之间做差分,然后有点像 线段树分治 的感觉

如果 左子树差值大于 \(k\),则向左子树走,否则向右子树走,\(k\) 减去 左子树差值

然后注意 权值离散化 就行了

#include <bits/stdc++.h>

const int MAXN = 200005;
const int LOGN = 24;

using namespace std;

int Num[MAXN], Uni[MAXN];
unordered_map <int, int> Mp;

namespace PSDTree {
	struct Node {
		int L, R, v, lc, rc;
	} T[MAXN * LOGN];
	
	int RT[MAXN], tot = 0, Cnt = 0;
	
	#define LC T[x].lc
	#define RC T[x].rc
	#define M ((T[x].L + T[x].R) >> 1)
	
	inline int NewNode (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline int Build (const int L, const int R, int x = 0) {
		x = ++ tot, T[x].L = L, T[x].R = R;
		if (L == R) return x;
		LC = Build (L, M), RC = Build (M + 1, R);
		return x;
	}
	
	inline int Ins (const int p, int x = 0) {
		x = NewNode (x), T[x].v ++ ;
		if (T[x].L == T[x].R) return x;
		if (p <= M) LC = Ins (p, LC);
		else RC = Ins (p, RC);
		return x;
	}
	
	inline int Query (const int L, const int R, const int k) {
		if (T[L].L == T[L].R) return T[L].L;
		int New = T[T[R].lc].v - T[T[L].lc].v;
		if (k <= New) return Query (T[L].lc, T[R].lc, k);
		else return Query (T[L].rc, T[R].rc, k - New);
	}
	
	#undef M
}

using namespace PSDTree;

int N, M, L, R, k;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> M;
	
	for (int i = 1; i <= N; ++ i) cin >> Num[i];
	
	memcpy (Uni, Num, sizeof Num);
	sort (Uni + 1, Uni + N + 1);
	
	for (int i = 1; i <= N; ++ i) Mp[Uni[i]] = i;
	
	RT[Cnt ++] = Build (1, N);
	
	for (int i = 1; i <= N; ++ i) RT[i] = Ins (Mp[Num[i]], RT[i - 1]);
	
	for (int i = 1; i <= M; ++ i) 
		cin >> L >> R >> k, cout << Uni[Query (RT[L - 1], RT[R], k)] << '\n';
	
	return 0;
}

Luogu P5795 [THUSC2015] 异或运算

注意到 \(N, M\) 不同阶,\(M\)\(3 \times 10 ^ 5\),所以 \(O(NM\log M)\) 之类的暴力不可取

考虑 异或的性质,注意到可以 贪心,高位尽量选 \(1\) 一定大

可以对 \(M\) 对应的的 \(Y\) 序列建立 可持久化 \(Trie\)

考虑每次给定 \(X\) 序列区间,于是我们 暴力多树二分

具体一点,我们二进制下 一位一位的贪心 找到第 \(k\) 大值

\(A_{i, j}\) 在二进制下 第 \(t\) 位为 \(1\) 的数 个数 实质上是


\(X_i (i \in[u, d])\) 中 第 \(t\) 位 为 \(0\) 的数 个数 \(\times\) \(Y_i (i \in [l, r])\) 中 第 \(t\) 位 为 \(1\) 的数 个数

\(+\) \(X_i (i \in[u, d])\) 中 第 \(t\) 位 为 \(1\) 的数 个数 \(\times\) \(Y_i (i \in [l, r])\) 中 第 \(t\) 位 为 \(0\) 的数 个数

\(X_i (i \in [u, d])\) 代入进行 多树二分 即可

注意需要记录下每个 \(X_i\)\(Trie\)上一步走的是 左 / 右 子树

具体看代码吧,讲的有些抽象

#include <bits/stdc++.h>

const int MAXN = 1005;
const int MAXM = 300005;
const int LOGN = 22;
const int LOGV = 32;

using namespace std;

int N, M, Q;
int A[MAXN], B[MAXM];

namespace PSDTree {
	struct Node {
		int v, ch[2];
	} T[MAXM * LOGV];
	
	int RT[MAXM], tot = 0, Cnt = 0;
	
	inline int NewNode (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline int Ins (const int p, int x = 1) {
		int t, ret; ret = x = NewNode (x), ++ T[x].v;
		for (int i = 30; ~ i; -- i) {
			t = (p >> i) & 1;
			T[x].ch[t] = NewNode (T[x].ch[t]);
			x = T[x].ch[t], ++ T[x].v;
		}
		return ret;
	}
	
	int rd[MAXN], ld[MAXN];
	bool v[MAXN][LOGV];
	
	inline int Query (const int l1, const int r1, const int l2, const int r2, int K) {
		int Ret = 0, cnt = 0, t = 0;
		for (int i = l1; i <= r1; ++ i) rd[i] = RT[r2];
		for (int i = l1; i <= r1; ++ i) ld[i] = RT[l2];
		
		for (int k = 30; ~ k; -- k, cnt = 0) {
			for (int i = l1; i <= r1; ++ i) t = v[i][k], cnt += T[T[rd[i]].ch[t ^ 1]].v - T[T[ld[i]].ch[t ^ 1]].v;
			
			if (K <= cnt) {
				Ret += (1 << k);
				for (int i = l1; i <= r1; ++ i) t = v[i][k], rd[i] = T[rd[i]].ch[t ^ 1], ld[i] = T[ld[i]].ch[t ^ 1];
			} else {
				K -= cnt;
				for (int i = l1; i <= r1; ++ i) t = v[i][k], rd[i] = T[rd[i]].ch[t], ld[i] = T[ld[i]].ch[t];
			}
		}
		return Ret;
	}
}

using namespace PSDTree;

int U, D, L, R, K;

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

	cin >> N >> M;
	
	for (int i = 1; i <= N; ++ i) cin >> A[i];
	
	for (int i = 1; i <= M; ++ i) cin >> B[i];
	
	for (int i = 1; i <= M; ++ i) RT[i] = Ins (B[i], RT[i - 1]);
	
	for (int i = 30; ~ i; -- i) 
		for (int j = 1; j <= N; ++ j) 
			v[j][i] = (A[j] >> i) & 1;
	
	cin >> Q;
	
	for (int i = 1; i <= Q; ++ i) {
		cin >> U >> D >> L >> R >> K;
		cout << Query (U, D, L - 1, R, K) << '\n';
	}
	
	return 0;
}

Luogu P2839 [国家集训队] middle

好题,牛牛的一个套路 —— \(\textsf H\)\(\textsf {anghang}\)

就是题面比较 如如

可以 理解成 对于序列 \(A\)\(a_1 ... a_N\)中位数\(a_{N / 2}\) ,其中 除法上取整

直接维护是 困难的,但是注意到我们要维护一个 中位数的最大值

可以考虑 极值转存在 —— 二分答案

对于一个区间,如果我们把 大于等于 \(k\) 的所有数 记为 \(1\),反之为 \(-1\)

容易发现,当这个 区间和 大于等于 \(0\) 时,这个区间中位数大于等于 \(k\)

于是我们只需要考虑 如何维护这个区间和 就行了

注意到每个询问给出了 左右端点所在区间,显然想让 中位数大,那么 区间和应当大

于是我们用 线段树维护 区间 最大前缀 \(Suf\)最大后缀 \(Pre\)区间和 \(Sum\)

不难发现对于左端点在 \([L_1, R_1]\),右端点在 \([L2,R2]\) 范围内的 所有区间 \([l_i, r_i]\)

最大区间和 应当是 \(Pre_{L1,R1} + Sum_{R1 + 1, L2 - 1} + Suf_{L2, R2}\)

但是我们注意到,这里的 \(-1,1\) 序列是 对于某个数 \(k\) 而言的

最大区间和大于等于 \(0\) 时,这个询问的 最大中位数大于等于 \(k\)

我们需要 二分答案,故对于 每个可能的 \(k\),我们 都需要 预处理出这个 \(-1,1\) 序列与线段树

\(k\) 的可能取值即种数序列长度 \(N\),显然我们不可能处理出 \(N\) 个这样的 \(-1,1\) 序列

但是我们观察到,当 \(k\) 不断变大时,更多的数从 \(1\) 变成 \(-1\)且不会变回来

也就是说这 \(N\) 个序列中实质上 只有 \(N\) 次单向的变化,可以想到用 可持久化数据结构维护

于是此处我们 建立一棵主席树 来维护上述 区间信息

同时 所有位置初始值为 \(1\)从小到大 遍历权值

当前遍历到的权值对应位置 改为 \(-1\),并 建立新版本

于是我们得到了有 \(N\) 个版本的主席树,其中 \(i\) 个版本 就对应了 \(k = i + 1\) 时的 线段树

这里 \(k = i + 1\) 是指的 \(k\) 等于 排序后 \(i + 1\) 小的数

注意到我们建立的不是 权值线段树,故此处 不能离散化去重,每个值都有其贡献

代码还是比较简单的

#include <bits/stdc++.h>

const int MAXN = 25005;
const int LOGN = 16;

using namespace std;

int N, M, LA;
int Num[MAXN], Uni[MAXN];
int Q[4];

namespace PSDTree {
	struct Node {
		int L, R, lc, rc;
		int Sum = 0, Pre = 0, Suf = 0;
	} T[MAXN * LOGN << 1];
	
	int RT[MAXN], tot = 0, Cnt = 0;
	
	#define LC T[x].lc
	#define RC T[x].rc
	#define M ((T[x].L + T[x].R) >> 1)
	
	inline int NewNode (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline int ModNode (const int x, const int v) {
		return T[x].Sum = T[x].Pre = T[x].Suf = v;
	}
	
	inline void Update (const int x) {
		T[x].Sum = T[LC].Sum + T[RC].Sum;
		T[x].Pre = max (T[RC].Sum + T[LC].Pre, T[RC].Pre);
		T[x].Suf = max (T[LC].Sum + T[RC].Suf, T[LC].Suf);
	}
	
	inline int Build (const int L, const int R, int x = 0) {
		x = ++ tot, T[x].L = L, T[x].R = R;
		if (L == R) return ModNode(x, 1), x;
		LC = Build (L, M), RC = Build (M + 1, R), Update (x);
		return x;
	}
	
	inline int Ins (const int p, const int v, int x = 0) {
		x = NewNode (x);
		if (T[x].L == T[x].R) return ModNode (x, v), x;
		if (p <= M) LC = Ins (p, v, LC), Update (x);
		else RC = Ins (p, v, RC), Update (x);
		return x;
	}
	
	inline int Sum (const int L, const int R, const int x = 0) {
		if (T[x].L > R || T[x].R < L) return 0;
		if (L <= T[x].L && T[x].R <= R) return T[x].Sum;
		return Sum (L, R, LC) + Sum (L, R, RC);
	}
	
	inline int Pre (const int L, const int R, const int x = 0) {
		if (T[x].L > R || T[x].R < L) return -1e9;
		if (L <= T[x].L && T[x].R <= R) return T[x].Pre;
		return max (Sum (L, R, RC) + Pre (L, R, LC), Pre (L, R, RC));
	}
	
	inline int Suf (const int L, const int R, const int x = 0) {
		if (T[x].L > R || T[x].R < L) return -1e9;
		if (L <= T[x].L && T[x].R <= R) return T[x].Suf;
		return max (Sum (L, R, LC) + Suf (L, R, RC), Suf (L, R, LC));
	}
	
	#undef M
}

using namespace PSDTree;

inline bool Check (const int x, const int L1, const int R1, const int L2, const int R2) {
	int Ret = Pre (L1, R1, RT[x - 1]) + Sum (R1 + 1, L2 - 1, RT[x - 1]) + Suf (L2, R2, RT[x - 1]); // Node >= x -> 1, Node < x -> 0
	return Ret >= 0;
}

inline void Solve (const int L1, const int R1, const int L2, const int R2) {
	int L = 0, R = N, Mid = (L + R) >> 1, Ans = Mid;
	while (L <= R) {
		Mid = (L + R) >> 1;
		if (Check (Mid, L1, R1, L2, R2)) L = Mid + 1, Ans = Mid;
		else R = Mid - 1;
	}
	cout << (LA = Uni[Ans]) << '\n';
}

struct Value {
	int v, id;
	
	inline bool operator < (const Value &a) const {
		return v < a.v;
	}
} Secret[MAXN];

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N;
	
	for (int i = 1; i <= N; ++ i) cin >> Num[i], Secret[i] = {Num[i], i};
	
	memcpy (Uni, Num, sizeof Num);
	
	sort (Uni + 1, Uni + N + 1), sort (Secret + 1, Secret + N + 1);
	
	RT[0] = Build (1, N);
	
	for (int i = 1; i <= N; ++ i) RT[i] = Ins (Secret[i].id, -1, RT[i - 1]);
	
	cin >> M;
	
	for (int i = 1; i <= M; ++ i) {
		cin >> Q[0] >> Q[1] >> Q[2] >> Q[3];
		Q[0] = (Q[0] + LA) % N + 1;
		Q[1] = (Q[1] + LA) % N + 1;
		Q[2] = (Q[2] + LA) % N + 1;
		Q[3] = (Q[3] + LA) % N + 1;
		sort (Q, Q + 4), Solve (Q[0], Q[1], Q[2], Q[3]);
	}
	
	return 0;
}

CF464E The Classic Problem

典 题

注意到这道题 \(N, M, K\) 同阶,故下文部分 时间复杂度 的分析可能 不会区分 \(N, M, K\)

最短路问题,边权改为 \(2 ^ k\)\(k \le 10 ^ 5\),求 最短路距离与路径\(N, M \le 10 ^ 5\)

显然问题的关键在这个 \(2 ^ k\) 的边权,而 真正 最短路部分 由于 没有什么其他性质

所以仍然考虑 朴素的 \(Dijkstra\) 算法

于是当前需要解决的就是 距离的比较 以及 更新(加法,修改)问题

我们将维护的 距离 用一个 \(01\) 序列来表示,单边边权 \(2 ^ k\) 就是 一个 \(1\) 后面 若干个 \(0\) 的序列

这里默认序列长为 \(\max k\),实际上 单边边权至多 \(2 ^ {10 ^ 5}\),而最多 \(10 ^ 5\) 条边

故序列长 至少为 \(10 ^ 5 + 17\)

于是 距离的比较 本质上就是 两个 \(01\) 序列,比较 字典序大小

我们可以使用 二分 + \(Hash\) 来实现,具体来说,我们 二分 出 两序列上第一个值不同的位置

比较该位的值即可,单次比较的时间复杂度是 \(O(\log N)\)

只剩下 距离更新 的问题了,容易发现,距离 加上一个边权 的实质是

将距离对应的 \(01\) 序列 一段连续的 \(1\) 改成 \(0\),然后将原本这段 \(1\) 前面的第一个 \(0\) 改成 \(1\)

\(eg. 0110~1000~1111~0010 + 0000~0000~0001~0000 = 0110 ~ 1001 ~ 0000 ~ 0010\)

于是我们需要维护 单点 \(0 \to 1\)区间 \(1 \to 0\) 的操作,容易想到 线段树维护

对每一个 \(01\) 序列 建立线段树

对于结点 \(x \to [L, R],\)我们钦定其 右子结点 将维护 \([M + 1, R]\) ,对应 \(2 ^ {M + 1} \sim 2 ^ R\)

实际上在 \(01\) 序列中可能是 左边的值注意区分

我们维护 区间长 \(Siz\)区间后缀 \(1\) 最大长度 \(Pre\)区间 \(Hash\) \(Hsh\) 三个信息

这里的后缀指的是 将距离二进制表达下的 \(01\) 序列后缀

在我们维护的 \(01\) 序列(左起 \(0\) 到右止于 \(10 ^ 5 + 17\)) 中 可能是一段前缀

就和上面 右子结点 的理解一样,实际上就是因为 写的时候是从最高位开始写的

注意倒过来就行

其中 \(Siz\)\(Hsh\) 是简单的,不知道咋搞就看代码,容易理解

\(Pre\) 的更新需要注意一些细节,我们判断一下 左区间 是否都是 \(1\)

如果是,那么 \(Pre\) 就是 左区间的长度 \(+\) 右区间的 \(Pre\)

反之直接取 左区间的 \(Pre\) 即可

总结一下 加上一个边权 的流程

  1. 找到 从边权的 \(1\) 所在位 向前的一段 连续 \(1\),通过维护的 区间最大后缀\(O(\log N)\) 找出
  2. 将这段连续 \(1\) 改为 \(0\),这里也是 \(O(\log N)\) 的复杂度,方法比较妙,下面讲
  3. 将前面的第一个 \(0\) 改为 \(1\),单点修改,时间也是 \(O(\log N)\)

但是这里我们又发现一个 经典的问题

我们显然不能对 每个点的距离 都去维护一个 完整的 \(01\) 序列

但是这里 修改次数 又很有限,只有 \(Dijkstra\)\(O(M \log N)\)

于是 可持久化线段树 维护即可,注意到每个点的距离 对应一个版本的根

\(Dis_v = Dis_u + w\) 的时候我们 \(u\) 对应版本 为基础修改,将 \(v\) 对应版本 改为这个新版本

原来 \(v\) 对应的版本就被 弃用了

这里是可以做 内存回收 的,但是影响 时间常数,而且不做也能过...

区间修改 的操作呢?注意到我们 建立一个 对应全 \(0\) 序列 的版本

每次修改,当前结点的区间被需要修改的区间包含时

我们直接将 这个结点 换成 \(0\) 序列版本的对应结点 即可,实现也可以看代码

剩下的细节 不是很多,大胆实现就行

时间复杂度 \(O(M \log N \log k = O (N \log ^ 2 N)\)

空间复杂度 \(O(M \log N \log K) = O(N \log ^ 2 N)\) 卡不满,内存回收后可以做到 \(O(N \log k)\)

\(\log ^ 2 N\) 空间的话开满差不多 \(400 ~ MB\),没有问题

#include <bits/stdc++.h>

const int MAXN = 100050;
const int MOD  = 1000000007;
const int LOGN = 80;
const int D = 100020;

using namespace std;

int N, M, U, V;

struct Edge {
	int to, nxt, w;
} E[MAXN << 1];

int H[MAXN], tot1;

inline void Add_Edge (const int u, const int v, const int w) {
	E[++ tot1] = {v, H[u], w}, H[u] = tot1;
	E[++ tot1] = {u, H[v], w}, H[v] = tot1;
}

unsigned long long P[MAXN];

namespace PSDTree {
	struct Node {
		int L, R, lc, rc;
		int Siz, Pre;
		unsigned long long Hsh = 0;
	} T[MAXN * LOGN];
	
	int RT[MAXN], tot = 0, Cnt = 0;
	
	#define LC T[x].lc
	#define RC T[x].rc
	#define M ((T[x].L + T[x].R) >> 1)
	
	inline int NewNode (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline void Update (const int x) {
		T[x].Siz = T[LC].Siz + T[RC].Siz;
		T[x].Pre = (T[LC].Pre == T[LC].Siz) * T[RC].Pre + T[LC].Pre;
		T[x].Hsh = (T[RC].Hsh * P[T[LC].Siz] % MOD + T[LC].Hsh) % MOD;
	}
	
	inline int Build (const int L, const int R, int x = 0) {
		x = ++ tot, T[x].L = L, T[x].R = R;
		if (L == R) return T[x].Siz = 1, x;
		LC = Build (L, M), RC = Build (M + 1, R), Update (x);
		return x;
	}
	
	inline int Pre (const int L, const int R, const int x = 0) {
		if (R < T[x].L || T[x].R < L) return 0;
		if (L <= T[x].L && T[x].R <= R) return T[x].Pre;
		int LCP = Pre (L, R, LC), LCS = max (min (T[LC].R, R) - max (T[LC].L, L) + 1, 0);
		return LCP == LCS ? LCP + Pre (L, R, RC) : LCP;
	}
	
	inline int Modify_1_0 (const int L, const int R, const int Clr = RT[0], int x = 0) {
		if (L <= T[x].L && T[x].R <= R) return x = Clr;
		x = NewNode (x);
		if (L <= M) LC = Modify_1_0 (L, R, T[Clr].lc, LC);
		if (R >  M) RC = Modify_1_0 (L, R, T[Clr].rc, RC);
		return Update (x), x;
	}
	
	inline int Modify_0_1 (const int p, int x = 0) {
		x = NewNode (x);
		if (T[x].L == T[x].R) return T[x].Pre = T[x].Hsh = 1, x;
		if (p <= M) LC = Modify_0_1 (p, LC);
		else RC = Modify_0_1 (p, RC);
		return Update (x), x;
	}
	
	inline void Modify (int &Ans, const int Gen, const int Add) {
		int p = Pre (Add, D, Gen);
		if (p) Ans = Modify_1_0 (Add, Add + p - 1, RT[0], Gen); else Ans = Gen;
		Ans = Modify_0_1 (Add + p, Ans);
	}
	
	inline bool Cmp (const int x, const int y) { // return x > y ?
		if (T[x].L == T[x].R) return T[x].Hsh > T[y].Hsh;
		if (T[T[x].rc].Hsh == T[T[y].rc].Hsh) return Cmp (T[x].lc, T[y].lc);
		return Cmp (T[x].rc, T[y].rc);
	} 
	
	#undef M
}

struct Que {
	int id, rt;
	inline bool operator < (const Que &a) const {
		return PSDTree::Cmp (rt, a.rt);
	}
};

priority_queue <Que> Q;
int Pat[MAXN];
bool Vis[MAXN];

inline void Dijkstra () {
	using namespace PSDTree;
	RT[U] = RT[0], Q.push ({U, RT[U]});
	while (!Q.empty ()) {
		int u = Q.top ().id, v; Q.pop ();
		if (Vis[u]) continue ;
		Vis[u] = 1;
		if (u == V) break ;
		for (int i = H[u]; i; i = E[i].nxt) {
			Modify (RT[N + 1], RT[u], E[i].w), v = E[i].to;
			if (!RT[v] || Cmp (RT[v], RT[N + 1])) {
				RT[v] = RT[N + 1], Pat[v] = u; 
				if (!Vis[v]) Q.push ({v, RT[v]});
			}
		}
	}
}

int u, v, w;

stack <int> S;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> M;
	
	for (int i = 1; i <= M; ++ i)
		cin >> u >> v >> w, Add_Edge (u, v, w);
	
	P[0] = 1;
	for (int i = 1; i <= D; ++ i) P[i] = P[i - 1] * 2 % MOD;
	
	cin >> U >> V;
	
	PSDTree::RT[0] = PSDTree::Build (0, D);
	
	Dijkstra ();
	
	if (!Vis[V]) return cout << -1 << endl, 0;
	
	else cout << PSDTree::T[PSDTree::RT[V]].Hsh << endl;
	
	for (int i = V; i != U; i = Pat[i]) S.push (i); 
	
	S.push (U); cout << S.size () << endl;
	
	while (!S.empty ()) cout << S.top () << ' ', S.pop ();
	
	return 0;
}

Luogu P3302 [SDOI2013] 森林

感觉大体思路是简单的

考虑只有 \(Opt.1\) 时,对 树建主席树

也就是 \(DFS\) 一遍树,每个点 基于它父亲的版本 插入它自己的权值

然后 树上差分询问 即可,这里注意 版本不是 \(DFS\)

而是 子节点继承父节点 信息

于是 T[RT[u]] - T[RT[F[LCA]]] 相当于 \(LCA \to u\)路径信息

另一边就是 \(LCA\) 的儿子 到 \(v\)路径信息\(LCA\) 不能算两遍)

询问是简单的,考虑连边,即 \(Opt.2\)

不想写 \(LCT\),所以考虑 启发式合并 的思路,小树往大树合并

然后直接 暴力重构\(DFS\)重建主席树 即可

总复杂度 \(O (N \log ^ 2 N)\),虽然常数较大,但是还是很轻松

注意到 \(i >= 0\)\(i\) 并不等价,在求 \(LCA\) 的最后那个循环 终止条件写成 \(i\)

怒调 \(3 ~ Hrs\),本来多好写一道题...

#include <bits/stdc++.h>

const int MAXN = 100005;
const int LOGN = 90;

using namespace std;

int Val[MAXN], Uni[MAXN], Tmp;

struct Edge {
	int to, nxt;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v) {
	E[++ tot] = {v, H[u]}, H[u] = tot;
	E[++ tot] = {u, H[v]}, H[v] = tot;
}

namespace PSDTree {
	struct Node {
		int lc, rc;
		int s;
	} T[MAXN * LOGN];
	
	#define LC T[x].lc
	#define RC T[x].rc
	#define M ((L + R) >> 1)
	
	int RT[MAXN], Cnt = 0;
	int Tot = 0;
	
	inline int Cpy (const int x) {
		return T[++ Tot] = T[x], Tot;
	}
		
	inline int Ins (int x, const int v, const int L = 1, const int R = Tmp) {
		x = Cpy (x), ++ T[x].s;
		if (L == R) return x;
		if (v <= M) LC = Ins (LC, v, L, M);
		else RC = Ins (RC, v, M + 1, R);
		return x;
	}
	
	inline int Que (const int k, const int u, const int v, const int lca, const int flca, const int L = 1, const int R = Tmp) {
		if (L == R) return Uni[L];
		int Siz = T[T[u].lc].s + T[T[v].lc].s - T[T[lca].lc].s - T[T[flca].lc].s;
		if (k <= Siz) return Que (k, T[u].lc, T[v].lc, T[lca].lc, T[flca].lc, L, M);
		else return Que (k - Siz, T[u].rc, T[v].rc, T[lca].rc, T[flca].rc, M + 1, R);
	}

	#undef M
}

using namespace PSDTree;

int F[MAXN][LOGN], FF[MAXN], S[MAXN], D[MAXN];
int LOG[MAXN];

inline void DFS (const int x, const int f, const int rt) {
	RT[x] = Ins (RT[f], Val[x]);
	F[x][0] = f, FF[x] = rt, S[rt] ++ , D[x] = D[f] + 1;
	for (int i = 1; i < 18; ++ i)
		F[x][i] = F[F[x][i - 1]][i - 1];
	for (int i = H[x]; i; i = E[i].nxt)
		if (E[i].to != f) DFS (E[i].to, x, rt);
}

inline int LCA (int u, int v) {
	if (D[u] < D[v]) swap (u, v);
	while (D[u] != D[v]) u = F[u][LOG[D[u] - D[v]]];
	if (u == v) return u;
	
	
	for (int i = LOG[D[u]]; i >= 0; -- i)
		if (F[u][i] != F[v][i])
			u = F[u][i], v = F[v][i];
			
	return F[u][0];
}

int P, N, M, Q;
int u, v, k;
char Opt;
int Ans;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> P >> N >> M >> Q;
	
	for (int i = 1; i <= N; ++ i) cin >> Val[i], Uni[i] = Val[i];
	
	for (int i = 2; i <= N; ++ i) LOG[i] = LOG[i >> 1] + 1;
	
	sort (Uni + 1, Uni + N + 1);
	
	Tmp = unique (Uni + 1, Uni + N + 1) - (Uni + 1);
	
	for (int i = 1; i <= N; ++ i) Val[i] = lower_bound (Uni + 1, Uni + N + 1, Val[i]) - Uni;
	
	for (int i = 1; i <= M; ++ i)
		cin >> u >> v, Add_Edge (u, v);
	
	for (int i = 1; i <= N; ++ i)
		if (!FF[i]) DFS (i, 0, i);
	
	for (int i = 1; i <= Q; ++ i) {
		cin >> Opt >> u >> v;
		u ^= Ans, v ^= Ans;
		if (Opt == 'Q') {
			cin >> k, k ^= Ans;
			P = LCA (u, v);
			cout << (Ans = Que (k, RT[u], RT[v], RT[P], RT[F[P][0]])) << '\n';
		} else {
			Add_Edge (u, v);
			if (S[FF[v]] < S[FF[u]]) DFS (v, u, FF[u]);
			else DFS (u, v, FF[v]);
		}
	}
	
	return 0;
}

Luogu P7561 [JOISC 2021 Day2] 道路の建設案 (Road Construction)

小清新 \(DS\),甚至都不算 \(DS\)

\(N\) 个位置,求 曼哈顿距离 最小的 \(K\)

\(KD-Tree\)什么东西,蛤?

注意一个 套路曼哈顿距离切比雪夫距离

目的就是把 曼哈顿距离 这个 和两维差都相关 的值

转化成 切比雪夫距离 这个 和两维差最大值相关 的值

对于 原坐标系 的点 \((x_i, y_i)\),转化成 \((x_i - y_i, x_i + y_i)\)

然后 \(Dis (u, v) \le D\) 就可以转化为 \(\max (|x_u - x_v|, |y_u, y_v|) \le D\)

这样就只需要两维 分别满足 差小于 \(D\) 即可

显然这是一个 二维数点 的问题,但由于我们要知道 具体距离

而不是简单的,小于某个距离的 个数,所以考虑把 树状数组 改为 std::set

我们可以考虑 二分最小距离 \(D\),检查是否有 \(\ge K\) 对点 满足条件

读入点,按 横坐标排序扫描线扫横轴std::queue 维护 当前 横坐标区间

不合法的点(\(x < x_i - D\))弹出队列,同时从下面的 std::set删除

当扫到一个点 \((x_i, y_i)\) 时,合法的横坐标是 \([x_i - D, x_i]\)避免算重

注意到 std::queue 是按 \(x\) 排序的,std::set\(y\) 排序

删除的时候 能保证删对std::set 中的点 按两维排序 即可,代码显然

然后 放一个 std::set 维护 所有满足横坐标限制的点,按 纵坐标排序

然后每次 二分找到纵坐标 \(\ge y_i - D\) 的点,向上遍历到纵坐标 \(\le y_i + D\)

全部 暴力加入答案,总点数 \(\ge K\)立即返回,保证 时间复杂度合理

最后设 第 \(K\) 大距离为 \(L\),那么先把所有 距离 \(\le L - 1\) 的距离输出

然后用 \(L\) 补齐即可(因为所有距离 \(\le L\) 的点对就可能不止 \(K\) 个了)

代码十分简单,细节少

#include <bits/stdc++.h>

const int MAXN = 250005;

using namespace std;

struct Node1 {
	long long x, y;
	
	inline bool operator < (const Node1 &a) const {
		return x == a.x ? y < a.y : x < a.x;
	}
} P[MAXN];

struct Node2 {
	long long x, y;
	
	inline bool operator < (const Node2 &a) const {
		return y == a.y ? x < a.x : y < a.y;
	}
};

long long C;
long long Ans[MAXN];
int N, K;

inline bool Check (const long long x) {
	C = 0;
	multiset <Node2> S;
	queue <Node1> Q;
	for (int i = 1; i <= N; ++ i) {
		while (!Q.empty () && P[i].x - Q.front ().x > x) 
			S.erase (S.find ((Node2) {Q.front ().x, Q.front ().y})), Q.pop ();
		auto it = S.lower_bound ((Node2) {(P[i].x - x), (P[i].y - x)});
		while (it != S.end () && (* it).y <= P[i].y + x) {
			Ans[++ C] = max (abs (0ll + P[i].x - (* it).x), abs (0ll + P[i].y - (* it).y)), ++ it;
			if (C >= K) return 1;
		}
		S.insert ({P[i].x, P[i].y}), Q.push (P[i]);
	}
	return 0;
}

inline void Solve () {
	long long L = 1, R = 4e9, M;
	while (R > L) {
		M = L + ((R - L) >> 1);
		if (Check (M)) R = M;
		else L = M + 1;
	}
	
	Check (L - 1);
	
	sort (Ans + 1, Ans + C + 1);
	
	for (int i = 1; i <= C; ++ i) cout << Ans[i] << '\n';
	
	for (int i = C + 1; i <= K; ++ i) cout << L << '\n';
}

long long x, y;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> K;
	
	for (int i = 1; i <= N; ++ i) cin >> x >> y, P[i] = {x - y, x + y};
	
	sort (P + 1, P + N + 1);
	
	Solve ();
	
	return 0;
}

Luogu P4216 [SCOI2015] 情报传递

树上 单点修改(且每个点只会被 修改一次),询问路径上 权值 \(\le K\)节点数量

考虑由于 每个人只会被启动一次,给一个权值

所以可以先把 权值(每个人启动的时刻)离线,建树,避免后面修改

这里可以想到和 Luogu P3302 [SDOI2013] 森林 一样的 \(trick\)

每个节点基于父亲版本 修改,然后 树上差分询问

这里建的是 可持久化权值线段树,每个点 建立一个版本,插入对应权值

查询的时候 查两点 \(LCA\),距离通过 \(Dep_u + Dep_v - 2 Dep_{LCA} + 1\) 即得

权值我们就计算一下 路径上权值小于 \(Now - C\) 的点数

直接询问 \(u, v, LCA, fa_{LCA}\) 然后做差即可,详情见代码

实现是简单的,甚至 权值不用离散化,十分良心

#include <bits/stdc++.h>

const int MAXN = 200005;
const int LOGN = 40;

using namespace std;

struct Edge {
	int to, nxt;
} E[MAXN << 1];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v) {
	E[++ tot] = {v, H[u]}, H[u] = tot;
}

int N, Q;
int F[MAXN][LOGN], W[MAXN], D[MAXN];
int LOG[MAXN];

namespace PSDTree {
	struct Node {
		int L, R;
		int lc, rc;
		int s;
	} T[MAXN * LOGN];
	
	#define LC T[x].lc
	#define RC T[x].rc
	#define M ((L + R) >> 1)
	
	int RT[MAXN], Tot;
	
	inline int Cpy (const int x) {
		return T[++ Tot] = T[x], Tot;
	}
	
	inline void Maintain (const int x) {
		T[x].s = T[LC].s + T[RC].s;
	}
	
	inline int Ins (const int p, int x, const int L = 0, const int R = Q) {
		x = Cpy (x), T[x].L = L, T[x].R = R, T[x].s ++ ;
		if (L == R) return x;
		if (p <= M) LC = Ins (p, LC, L, M);
		else RC = Ins (p, RC, M + 1, R);
		return x;
	}
	
	inline int Sum (const int L, const int R, const int x) {
		if (!x || R < L) return 0;
		if (T[x].L >  R || L >  T[x].R) return 0;
		if (L <= T[x].L && T[x].R <= R) return T[x].s;
		return Sum (L, R, LC) + Sum (L, R, RC);
	}
	
	inline int Que (const int u, const int v, const int lca, const int flca, const int p) {
		return Sum (0, p - 1, u) + Sum (0, p - 1, v) - Sum (0, p - 1, lca) - Sum (0, p - 1, flca);
	}
	
	#undef M
}

using namespace PSDTree;

inline void DFS (const int x, const int f) {
	RT[x] = Ins (W[x], RT[f]), D[x] = D[f] + 1;
	for (int i = H[x]; i; i = E[i].nxt) 
		if (E[i].to != f)
			DFS (E[i].to, x);
}

inline int LCA (int u, int v) {
	if (D[u] < D[v]) swap (u, v);
	while (D[u] > D[v]) u = F[u][LOG[D[u] - D[v]]];
	if (u == v) return u;
	for (int i = 18; i >= 0; -- i)
		if (F[u][i] != F[v][i])
			u = F[u][i], v = F[v][i];
	return F[u][0];
}

int Opt[MAXN], U[MAXN], V[MAXN], C[MAXN];
int p, fp;
int Dis, Ans;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N;
	
	for (int i = 1; i <= N; ++ i) cin >> F[i][0], Add_Edge (F[i][0], i);
	
	for (int i = 2; i <= N; ++ i) LOG[i] = LOG[i >> 1] + 1;
	
	cin >> Q;
	
	for (int i = 1; i <= N; ++ i) W[i] = Q;
	
	for (int i = 1; i <= Q; ++ i) {
		cin >> Opt[i];
		if (Opt[i] == 2) cin >> C[i], W[C[i]] = i;
		else cin >> U[i] >> V[i] >> C[i];
	}
	
	DFS (1, 0);

	for (int i = 1; i <= 19; ++ i)
		for (int x = 1; x <= N; ++ x)
			F[x][i] = F[F[x][i - 1]][i - 1];
	
	for (int i = 1; i <= Q; ++ i) {
		if (Opt[i] == 1) {
			p = LCA (U[i], V[i]), fp = F[p][0];
			Dis = D[U[i]] + D[V[i]] - D[p] - D[p] + 1;
			cout << Dis << ' ' << Que (RT[U[i]], RT[V[i]], RT[p], RT[fp], i - C[i]) << '\n';
		}
	}
	
	return 0;
}

Luogu P3567 [POI2014] KUR-Couriers

主席树板子

按顺序插入序列,建立 可持久化值域线段树

树上二分 查询 区间内 当前值域数的个数 是否 大于区间一半

就完了

#include <bits/stdc++.h>

const int MAXN = 500005;
const int LOGN = 60;

using namespace std;

int N, Q, P;

namespace PSDTree {
	struct Node {
		int L, R, lc, rc;
		int s;
	} T[MAXN * LOGN];
	
	#define M ((T[x].L + T[x].R) >> 1)
	
	int RT[MAXN], tot = 0;
	
	inline int Cpy (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline int Ins (const int p, int x, const int L = 1, const int R = N) {
		x = Cpy (x), ++ T[x].s, T[x].L = L, T[x].R = R;
		if (L == R) return x;
		if (p <= M) T[x].lc = Ins (p, T[x].lc, L, M);
		else T[x].rc = Ins (p, T[x].rc, M + 1, R);
		return x;
	}
	
	inline int Query (const int rt1, const int rt2, const int p) {
		if (T[rt2].L == T[rt2].R) return T[rt2].L;
		int SumL = T[T[rt2].lc].s - T[T[rt1].lc].s, SumR = T[T[rt2].rc].s - T[T[rt1].rc].s;
		if (SumL >= p) return Query (T[rt1].lc, T[rt2].lc, p);
		if (SumR >= p) return Query (T[rt1].rc, T[rt2].rc, p);
		return 0;
	}
	
	#undef M
}

using namespace PSDTree;

int L, R;

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N >> Q;
	
	for (int i = 1; i <= N; ++ i)
		cin >> P, RT[i] = Ins (P, RT[i - 1]);
	
	for (int i = 1; i <= Q; ++ i) {
		cin >> L >> R;
		cout << Query (RT[L - 1], RT[R], ((R - L + 1) >> 1) + 1) << '\n';
	}
	
	return 0;
}

CF543E Listening to Music

确实是个套路,思路很牛,而且 卡空间的那个法子太玄妙了

考虑区间长 \(M\) 是常数,可以用 区间左端点 来代替 唯一的一个的区间

注意代码中使用 \(K\) 来表示 给定区间长

于是我们把 给定权值从大到小排序,考虑一个值 排序前 如果在位置 \(i\)

左端点 \(l \in [i - M + 1, i]\) 代表的区间 都会被这个值贡献

于是我们把 这些区间(对应的左端点区间)插入一棵 主席树(区间加 \(1\)

区间修改查询主席树 写的比较少,但是其实实现是简单的

这里版本的 继承顺序较小权值从较大权值 处继承

于是 权值 \(x\) 代表的主席树 就记录了

每个左端点对应区间权值大于等于 \(x\) 的数个数

我们要求 一些区间小于 \(x\) 的数个数最少的区间,求 小于 \(x\) 的数个数

转化为求 大于等于 \(x\) 的数个数的最大值

由于 主席树上 一个点代表原序列上的一个区间

那么这个问题就转变成了 主席书上区间最大值,最后用 \(M\) 减去这个值 即可


然后问题来了,注意到这个题的空间只有 \(64 ~ MB\)(正解是 毒瘤分块

主席树空间就很大,就点数来说,一般的主席树 这道题需要 \(42N\) 个点

实验表明,这道题的 总空间\(83N \sim 84N\)int 类型左右(理论 \(83.88 N\)

再加上 维护一堆信息空间直接爆炸

考虑如下方法来 减小内存

  1. 标记永久化动态计算标记
inline int Ins (int x, const int l, const int r, const int L = 1, const int R = N - K + 1) {
    if (L == R) return x + 1;
    x = Cpy (x);
    if (l <= L && R <= r) return T[x].mx ++, x;
    int Tag = T[x].mx - max (LMAX, RMAX); // 计算标记
    if (l <= M) LC = Ins (LC, l, r, L, M);
    if (r >  M) RC = Ins (RC, l, r, M + 1, R);
    T[x].mx = max (LMAX, RMAX) + Tag; // 加上标记
    return x;
}

区间加最大值 的影响 显然需要标记维护

但是由于 影响对整个区间有效,那么这个标记可以 永久化 当成值看待

更极端一点,标记 即是 当前区间的 mx子节点 mx 的差,可以 实时计算

这样就可以 在节点里少记录一个信息省下不少空间

  1. unsigned long long按位内存分配(无需 重载运算符,十分好用)
struct Node {
    unsigned long long mx : 18, lc : 23, rc : 23;
} T[MAXN * LOGN];

这样就相当于 一个节点只开了一个 unsigned long long,然后分给 三个信息

mx 的值域为 \([0, N]\)lx, rc 的值域均为 \(0\)结点数

\(N < 2 ^ {18}, 40N < 2 ^ {23}\)(为什么 结点数\(40N\) 不是 \(42N\),看 \(3.\)

  1. 直接在 叶子节点 记录 ,省去 \(2N\) 个结点
#define LMAX (L == M ? int (LC) : int (T[LC].mx))
#define RMAX (R == M + 1 ? int (RC) : int (T[RC].mx))
// 如果是 一般节点,最大值就是 mx,如果是 叶子节点,最大值就是 父亲的’儿子标号‘ 记录的值


PSDTree::Ins : if (L == R) return x + 1;
// 如果到叶子,直接标号 +1,这样 每一次访问到这个叶子,其父亲对应的 ’儿子标号‘ 就会 +1
// 于是这个 ’标号‘ 就相当于记录的权值,也即 mx

PSDTree::Que : if (L == R) return x;
// 查询时 直接返回标号

由于 叶子节点 没有儿子,也 无需记录标记

于是我们可以 不建立真实的叶子,也不去记录它 真正的标号

而是把 它父亲指向它标号 的这个变量 拿来直接记录它的值

由于 \(N\) 次修改至多产生 \(2N\) 个(\(N?\))叶子,于是就 省下了这些空间


有了以上操作,我们就可以以 \(63.51 ~ MB\) 的空间 轻 松 通 过 此 题

上面的每一步 其实都卡的很死,十分牛

注意 信息 如果 多一个 \(N\)int 的大小,那么就 多出来 \(800 ~ KB\) 左右,寄喽

\(2 ^ {18} = 262144\),也就是如果 \(N\)大一点寄喽

\(2 ^ {23} = 8388608\),也就是如果点数真的是 \(42N\),那么 \(8400000\) 个点,寄喽

\(18 + 2 \times 23 = 64\),也就是 任意一个信息再多一位都不行,十分神秘

实现起来倒是不难,码量也很小,就是注意 不要多开数组 就行

#include <iostream>
#include <algorithm> 

const int MAXN = 200005;
const int LOGN = 40;

using namespace std;

int N, K, Q;

namespace PSDTree {
	struct Node {
		unsigned long long mx : 18, lc : 23, rc : 23;
	} T[MAXN * LOGN];
	
	int RT[MAXN], tot = 0;
	
	#define M ((L + R) >> 1)
	#define LC T[x].lc
	#define RC T[x].rc
	#define LMAX (L == M ? int (LC) : int (T[LC].mx))
	#define RMAX (R == M + 1 ? int (RC) : int (T[RC].mx))
	
	inline int Cpy (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline int Ins (int x, const int l, const int r, const int L = 1, const int R = N - K + 1) {
		if (L == R) return x + 1;
		x = Cpy (x);
		if (l <= L && R <= r) return T[x].mx ++, x;
		int Tag = T[x].mx - max (LMAX, RMAX);
		if (l <= M) LC = Ins (LC, l, r, L, M);
		if (r >  M) RC = Ins (RC, l, r, M + 1, R);
		T[x].mx = max (LMAX, RMAX) + Tag;
		return x;
	}
	
	inline int Que (int x, const int l, const int r, const int L = 1, const int R = N - K + 1) {
		if (L == R) return x;
		if (l <= L && R <= r) return T[x].mx;
		int Ret = 0, Tag = T[x].mx - max (LMAX, RMAX);
		if (l <= M) Ret = max (Ret, Que (LC, l, r, L, M));
		if (r  > M) Ret = max (Ret, Que (RC, l, r, M + 1, R));
		return Ret + Tag;
	}
	
	#undef M
}

using namespace PSDTree;

struct node {
	int v, id;
	
	inline bool operator < (const node &a) const {
		return v > a.v;
	}
} P[MAXN];

int L, R, Lst, x;

int main () {
	
	cin >> N >> K;
	
	for (int i = 1; i <= N; ++ i) cin >> P[i].v, P[i].id = i;
	
	sort (P + 1, P + N + 1);
	
	for (int i = 1; i <= N; ++ i) {
		L = max (1, P[i].id - K + 1), R = min (N - K + 1, P[i].id);
		RT[i] = Ins (RT[i - 1], L, R);
	}
	
	cin >> Q;
	
	for (int i = 1; i <= Q; ++ i) {
		cin >> L >> R >> x, x ^= Lst;
		x = upper_bound (P + 1, P + N + 1, node ({x, 0})) - P - 1;
		cout << (Lst = K - Que (RT[x], L, R)) << '\n';
	}
	
	return 0;
}

虽然但是,剩下的 \(0.49 ~ MB\) 来开一个 \(Buff = 5 \times 10 ^ 4\)快读 还是 绰绰有余


# 口胡

感觉 可持久化线段树 这个东西经常用来解决

需求 两个变量上的区间 的问题,可能有点抽象

就比如上面这道题,给定 左端点区间,询问 大于等于 \(x\) 这个 值域区间

本质上应当是因为 可持久化 相当于 建出了 \(O(N)\) 棵树

于是 线段树 本身能 解决一个区间,然后 两棵树差分 / 二分 又能解决一个区间

如果把 \(O(N)\)完整线段树 建出来 是 好理解的

解决 第二区间 的问题相当于一个 多树二分差分

然后想想 可持久化 其实 没有改变这 \(O(N)\) 棵树自身的结构

只是 把相同结构部分 合并一下(或说 只新建有改动部分)来省空间

于是确实是解决这种问题的 好方式,但是显然也有一些 限制

比如考虑 一次修改两区间上的改动量乘积 应当是 \(O(N)\) 级别的

也就是说,一般情况下 只能改其中一个区间(靠 差分 等手段 解决另一个)

否则容易导致 线段树上修改量 \(O(\log N) \to O (N)\),这样就不得不 重建树

时空复杂度都会假掉,如果遇到这种东西感觉得考虑 根号

上述东西似乎很披风 并不保证上述言论的准确性


可持久化平衡树

一般情况下还是使用 \(FHQ\_Treap\)

以下 平衡树 部分,没有特殊说明即默认使用 \(FHQ\_Treap\)

但是 \(WBLT\)替罪羊树 也是不错的选择,有时间会写(

很多时候又可以用 可持久化线段树 甚至 可持久化树状数组 代替

十分神秘,于是我们先来看一道

Luogu P3835 【模板】可持久化平衡树

注意到 没有强制在线可持久化 (通常)就是 假的可持久化

我们可以尝试建出 操作树,每个版本和对应操作 代表一个点

具体来讲把 当前版本它基于的那个版本 连边,然后 \(DFS\) 遍历一遍 操作树

我们在 进入某个点 的时候 执行操作退出撤销

显然 询问操作 无需撤销

我们就能得到 正确的结果

这个东西的 正确性是显然的,我们可以 这么理解

由于我们 每个版本它基于的版本 连了边

于是 某个版本的前继版本 一定是 该版本对应点在操作树上的祖先

当遍历到一个点时,必然 经过其祖先,也就是 执行了前面版本的操作

于是此时的状态正好是 正常顺序维护时 到达该版本时的状态

然后退出时,消除这个版本的影响即可


上面讲了 如何消除可持久化的影响,于是后面就只剩 普通平衡树 的维护了

显然,这个时候写平衡树就 太没必要了

又长,又慢,又难写,空间还大

我们可以先离散化权值,然后采用 权值树状数组权值线段树 实现

这里给出 权值树状数组 的实现,一般见的比较少

唯一要注意的点在于 查询排名为 \(k\) 的数 时的 树状数组上二分

其实和 线段树二分 相似,我们在树状数组上 枚举每一位

判断是否能取到,取到的话就使 \(k\) 减去 满足当前情况的数的数量

讲解抽象的话就看实现,只有一行,十分好写

#include <bits/stdc++.h>

const int MAXN = 524288;
const int LOGN = 18;

using namespace std;

int N, Now;

namespace BIT {
	#define lowbit(x) (x & -x)
	
	int T[MAXN];
	
	inline void Add (int x) {
		while (x <= Now) ++ T[x], x += lowbit (x);
	}
	
	inline void Del (int x) {
		while (x <= Now) -- T[x], x += lowbit (x);
	}
	
	inline int Sum (int x) {
		int Ret = 0;
		while (x) Ret += T[x], x -= lowbit (x);
		return Ret;
	}
	
	inline int Que (int x) {
		int Ret = 0;
		for (int i = (1 << LOGN); i; i >>= 1) 
			if ((Ret | i) <= Now && T[Ret | i] <= x) x -= T[Ret |= i]; // There
		return Ret;
	}
}

struct Edge {
	int to, nxt;
} E[MAXN];

int H[MAXN], tot;

inline void Add_Edge (const int u, const int v) {
	E[++ tot] = {v, H[u]}, H[u] = tot;
}

int Ver;
int Vis[MAXN];
short Opt[MAXN];
int Val[MAXN], Uni[MAXN], Ans[MAXN];

inline int Solve (const int opt, const int x) {
	
	using namespace BIT;
	
	if (opt == 2 && !Vis[x]) return 0;
	
	if (opt == 1) return Add (x), Vis[x] ++, 1;
	if (opt == 2) return Del (x), Vis[x] --, 1;
	if (opt == 3) return Sum (x - 1);
	if (opt == 4) return Que (x) + 1;
	if (opt == 5) return Que (Sum (x - 1) - 1) + 1;
	if (opt == 6) return Que (Sum (x)) + 1;
	
	return 0;
}

inline void DFS (const int x, const int f) {
	
	if (Opt[x] <= 3) Ans[x] = Solve (Opt[x], Val[x]); 
	else Ans[x] = Uni[Solve (Opt[x], Val[x])];
	
	for (int i = H[x]; i; i = E[i].nxt)
		if (E[i].to != f) DFS (E[i].to, x);
	
	if (Opt[x] == 1) Solve (2, Val[x]);
	if (Opt[x] == 2 && Ans[x]) Solve (1, Val[x]);
}

int main () {
	
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	
	cin >> N;
	
	for (int i = 1; i <= N; ++ i) {
		cin >> Ver >> Opt[i] >> Val[i];
		if (Opt[i] != 4) Uni[++ Now] = Val[i];
		Add_Edge (Ver, i);
	}
	
	Uni[++ Now] = - 2147483647;
	Uni[++ Now] = + 2147483647;
	
	sort (Uni + 1, Uni + Now + 1);
	
	Now = unique (Uni + 1, Uni + Now + 1) - Uni - 1;
	
	for (int i = 1; i <= N; ++ i)
		if (Opt[i] != 4) 
			Val[i] = lower_bound (Uni + 1, Uni + Now + 1, Val[i]) - Uni;
	
	BIT::Add (1), BIT::Add (Now), DFS (0, 0);
	
	for (int i = 1; i <= N; ++ i)
		if (Opt[i] > 2) cout << Ans[i] << '\n';
	
	return 0;
}

容易发现 树状数组常数较小,于是 加上快读的我们拿到了最优解

可持久化线段树的实现可以看 \(\color {black} \textsf {z} \color {red} \textsf {hicheng}\)


Luogu P5055 【模板】可持久化文艺平衡树

注意非常恶心的一个问题:输入会爆 int

题目只保证了 异或后的真实值 的值域

总算是一个 真正的 可持久化了...

Luogu P3391 【模板】文艺平衡树 的基础上,这道题是简单的

我们只需要修改 关键操作,使得其在修改时复制节点即可

具体来说,我们新增一个复制结点的操作 FHQ_Treap::Cpy,与 可持久化线段树 相似

inline int Cpy (const int x) {
	return T[++ tot] = T[x], tot;
}

然后将 可能对树形态有修改的 FHQ_Treap::UpdateFHQ_Treap::Split 操作对应变化

这里为什么 FHQ_Treap::Merge 不算修改形态?

因为 未被 Merge 的树 仅作为 一个操作的中间形态 出现

也就是不存在任意操作使得 做完这个操作之后 树还没有被 Merge

所以拷贝 Merge 之前的状态是 无意义的

我们在每次 修改形态之前拷贝版本,对于 Update,自然是 反转前 复制

inline void Update (const int x) {
    if (T[x].rev) {
        if (LC) LC = Cpy (LC); // Copy LeftChild
        if (RC) RC = Cpy (RC); // Copy RightChild
        swap (LC, RC), T[x].rev = 0; // Swap NewChild
        if (LC) T[LC].rev ^= 1;
        if (RC) T[RC].rev ^= 1;
    }
}

对于 Split,在 向下走前 复制

inline void Split (int x, const int s, int &rt1, int &rt2) {
    if (!x) return rt1 = rt2 = 0, void ();
    Update (x);
    if (T[LC].s < s) x = rt1 = Cpy (x), /* There */ Split (RC, s - T[LC].s - 1, RC, rt2), Maintain (rt1);
    else x = rt2 = Cpy (x), /* There */ Split (LC, s, rt1, LC), Maintain (rt2);
}

注意一个 十分重要的问题,考虑如果我们 每次复制新节点

复制出来的 随机权值 是和 源节点 一样的,当 大量基于同一版本复制

显然我们会出现 大量随机权值相等的点,这会对 平衡性产生破坏

也就是 如果直接这样复制随机权值,复杂度是错误的

而由于权值又代表了 子树的合并顺序

所以随便给 新点赋一个新的随机权值 又会导致 树的结构错误

实际效果是 答案正确新建点多了一大堆

是因为这样的话 子树需要重新合并,导致 最劣时 相当于 重构,复制了整个子树

可能需要 巨佬教教

于是我们得想出一种 新的随机合并方式

并保证它 不会因为某个节点的复制 而必须 重构整个子树

考虑 不维护随机权值,而在每次 Merge 的时候 根据子树大小 随机合并

具体来说,将 T[x].r > T[y].r 改为 rd () % (T[x].s + T[y].s) + 1 <= T[x].s

注意 rd () 出来会是 unsigned int 类型

直接转 int 再取模 或 不取模 均可能会导致 左边出现异常负数

右边显然是正的,左边经常负的话就不平衡了

所以在 取模完了之后 再转即可,具体 FHQ_Treap::Merge 实现如下

inline int Merge (const int x, const int y) {
    if (!x || !y) return x | y;
    if ((int) (rd () % (T[x].s + T[y].s)) + 1 <= T[x].s) return Update (x), T[x].rc = Merge (T[x].rc, y), Maintain (x);
    else return Update (y), T[y].lc = Merge (x, T[y].lc), Maintain (y);
}

然后其他操作 正常执行 就行,注意到这个题需要 多维护一个区间和

FHQ_Treap::Maintain 中和 子树大小 一起更新即可

贴个部分代码

namespace FHQ_Treap {
	struct Node {
		int v, s, lc, rc;
		int rev;
		long long sum = 0;
		
		inline void Clr () {
			sum = v = s = lc = rc = rev = 0;
		} 
	} T[MAXN * LOGN];
	
	#define LC T[x].lc
	#define RC T[x].rc
	
	int RT[MAXN], Cnt = 0;
	int rt = 0, tot = 0;
	
	inline int New (const int x) {
		return T[++ tot] = {x, 1, 0, 0, 0, x}, tot;
	}
	
	inline int Cpy (const int x) {
		return T[++ tot] = T[x], tot;
	}
	
	inline int Maintain (const int x) {
		T[x].s = T[LC].s + T[RC].s + 1;
		T[x].sum = T[LC].sum + T[RC].sum + T[x].v;
		return x;
	}
	
	inline void Update (const int x) {
		if (T[x].rev) {
			if (LC) LC = Cpy (LC);
			if (RC) RC = Cpy (RC);
			swap (LC, RC), T[x].rev = 0;
			if (LC) T[LC].rev ^= 1;
			if (RC) T[RC].rev ^= 1;
		}
	}
	
	inline void Split (int x, const int s, int &rt1, int &rt2) {
		if (!x) return rt1 = rt2 = 0, void ();
		Update (x);
		if (T[LC].s < s) x = rt1 = Cpy (x), Split (RC, s - T[LC].s - 1, RC, rt2), Maintain (rt1);
		else x = rt2 = Cpy (x), Split (LC, s, rt1, LC), Maintain (rt2);
	}
	
	inline int Merge (const int x, const int y) {
		if (!x || !y) return x | y;
		if ((int) (rd () % (T[x].s + T[y].s)) + 1 <= T[x].s) return Update (x), T[x].rc = Merge (T[x].rc, y), Maintain (x);
		else return Update (y), T[y].lc = Merge (x, T[y].lc), Maintain (y);
	}
	
	inline void Ins (const int p, const int v, int x = 0, int y = 0) {
		Split (rt, p, x, y), rt = Merge (Merge (x, New (v)), y);
	}
	
	inline void Del (const int p, int x = 0, int y = 0, int z = 0) {
		Split (rt, p, x, y), Split (x, p - 1, x, z), rt = Merge (x, y);
	}
	
	inline void Rev (const int L, const int R, int x = 0, int y = 0, int z = 0) {
		Split (rt, L - 1, x, y), Split (y, R - L + 1, y, z);
		T[y].rev ^= 1;
		rt = Merge (x, Merge (y, z));
	}
	
	inline long long Sum (const int L, const int R, int x = 0, int y = 0, int z = 0, long long Ans = 0) {
		Split (rt, L - 1, x, y), Split (y, R - L + 1, y, z);
		Ans = T[y].sum;
		rt = Merge (x, Merge (y, z));
		return Ans;
	}
}

posted @ 2024-05-03 21:55  FAKUMARER  阅读(15)  评论(0编辑  收藏  举报