主席树
\(\text{luogu-3919}\)
如题,你需要维护这样的一个长度为 \(n\) 的数组,支持如下两种操作:
- 在某个历史版本上修改某一个位置上的值。
- 访问某个历史版本上的某一位置的值。
具体来说:
- 对于操作 \(1\),格式为
v 1 p c,即为在版本 $ v $ 的基础上,将 $ a_{p} $ 修改为 \(c\)。 - 对于操作 \(2\),格式为
v 2 p,即访问版本 $ v $ 中的 $ a_{p} $ 的值,注意:生成一样版本的对象应为 \(v\)。
每进行一次操作,就会生成一个新的版本。版本编号即为当前操作的编号(从 \(1\) 开始编号,版本 \(0\) 表示初始状态数组)。
对于操作 \(2\),即为生成一个完全一样的版本,不作任何改动。即,询问生成的版本是询问所访问的那个版本的复制。
\(1 \le n,m \le 10^5\),\(-10^9 \le a_i, c \le 10^9\)。
可持久化数组(主席树)板子。
以下部分题解来自于 题解:P3919 【模板】可持久化线段树 1(可持久化数组) - 洛谷专栏。
一种简单,容易想到的暴力做法就是每一次创建一个新版本就复制一个新数组,时间和空间复杂度都是 \(\mathcal{O}(mn)\)(\(m\) 次操作,每次创建一个大小为 \(n\) 的数组,从原数组复制要进行 \(n\) 次操作)。本题 \(1 \le n,m \le 10^5\),这种做法显然不可接受。
我们观察需要创建新版本的场景,可以发现每次最多只会修改一个数,有时甚至不会修改,这些不修改的数字如果每一次都复制的话代价太高了。很容易想到每一次只记录修改的部分。
我们可以使用一种叫做可持久化线段树的数据结构。(没学过线段树的请右转学习线段树。)
我们现在先将版本 \(0\) 到版本 \(2\) 的线段树单独画出来:

我们发现版本 \(0\) 和版本 \(1\) 是重复的,我们可以直接将这些节点合并(将第二颗树的根节点的儿子设成第一颗树的根节点的儿子):

对于版本 \(2\),它只修改了一个数,我们可以想到只记录根节点到修改部分的一条“链”,其余的按照上面的方法操作:

总结:先将版本 \(0\) 的树完整的记录下来,接着之后的版本只记录修改的部分。
现在我们考虑复杂度:
空间复杂度:版本 \(0\) 要记录所有的数,会有 \(2n-1\) 个节点。总共有 \(m\) 个版本。假设每一个版本都有修改,那么每一次都要记录长为 \(\lceil \log_2 n \rceil+1\) 的“链”,空间复杂度约为 \(\mathcal{O}(n+m\log_2 n)\)。在此题中 \(n\) 和 \(m\) 最大为 \(10^6\),内存限制为 \(1\) GB,可以接受。
时间复杂度:建第一棵树复杂度为 \(\mathcal{O}(n)\),后面每一次查找复杂度都是 \(\mathcal{O}(\log n)\) 的,合起来就是 \(\mathcal{O}(n+m \log n)\)。
#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 1000005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
struct node { long long l, r, w; } t[MAXN << 5];
long long n, m, rt[MAXN], np, cnt;
long long build(long long l, long long r) {
long long p = (++ cnt), mid = (l + r) >> 1;
if(l == r) { t[p].w = read(); return p; }
t[p].l = build(l, mid), t[p].r = build(mid + 1, r);
return p;
}
long long upd(long long x, long long k, long long l, long long r, long long v) {
long long p = (++ cnt), mid = (l + r) >> 1;
t[p].l = t[v].l, t[p].r = t[v].r;
if(l == r) { t[p].w = k; return p; }
if(x <= mid) t[p].l = upd(x, k, l, mid, t[v].l);
else t[p].r = upd(x, k, mid + 1, r, t[v].r);
return p;
}
long long qry(long long x, long long l, long long r, long long p) {
if(l == r) return t[p].w;
long long mid = (l + r) >> 1;
if(x <= mid) return qry(x, l, mid, t[p].l);
return qry(x, mid + 1, r, t[p].r);
}
int main() {
n = read(), m = read(), rt[np ++] = build(1, n);
while(m --) {
long long v = read(), op = read(), x = read(), k;
if(op == 1) k = read(), rt[np ++] = upd(x, k, 1, n, rt[v]);
else cout << qry(x, 1, n, rt[np ++] = rt[v]) << "\n";
}
return 0;
}
\(\text{luogu-3834}\)
这是个非常经典的可持久化权值线段树入门题——静态区间第 \(k\) 小。
给定 \(n\) 个整数构成的序列 \(a\),将对于指定的闭区间 \([l, r]\) 查询其区间内的第 \(k\) 小值。
\(1 \leq n,m \leq 2\times 10^5\),\(0\le a_i \leq 10^9\),\(1 \leq l \leq r \leq n\),\(1 \leq k \leq r - l + 1\)。
可持久化线段树/主席树模板题。
以下部分题解来自于 主席树 - 洛谷专栏。
模板题就是主席树的典型例题,询问区间第 \(k\) 大。先不说区间 \([l,r]\) 吧,就说说 \([1,r]\) 怎么做。
模板题的 \([1,r]\) 情况
由题意知道我们肯定要对区间进行排序,但是我们的排序不是每次询问才排序,是初始化就排序并离散化 —— 针对数字较大但数据不大的情况下(具体见方法)。排序离散化完毕后,以离散化数组建主席树,设 \(i\) 属于区间 \([1,n]\),对原数组的 \([1,i]\) 区间的数做统计(例如下图,区间中按离散化数组顺序统计 \(1\) 的个数、\(2\) 的个数、\(3\) 的个数、\(4\) 的个数、\(8\) 的个数、\(9\) 的个数),有序地插入节点到离散化数组的主席树中,记录好原数组每个节点对应的线段树起点,针对样例有几个示意图。注意,这里的橙色节点是新节点,与之前出现的那个图不一样。
- \([1,1]\) 的情况

- \([1,4]\) 的情况

情况以此类推。
我们按照上面的做法构建的主席树是为了方便我们查找第 \(k\) 小值。因为我们是以离散数组构建的主席树,那么从根节点出发,左子树部分的数必定不大于右子树部分的数。于是就可以将左儿子的节点个数 \(x\) 与 \(k\) 做比较,若 \(k\leq x\),则第 \(k\) 小值一定在左子树里面,若 \(x\leq k\),则第 \(k\) 小值一定在右子树里面,然后递归往下走,缩小范围。值得注意的是,前者递归时,\(k\) 直接传下去即可,后者递归时,需要将 \(k\) 减去左子树的数的个数再传递这个 \(k\) 值。
例如我们查找 \([1,4]\) 中第 \(2\) 小的值,图示如下,绿色节点为该值存在的区间位置。

需要注意的是,第二个绿色节点才是绿色根节点的左子树,因为左子树表示的区间是靠前的那一半。
方法总结如下:
- 将原始数组复制一份,然后排序好,然后去掉多余的数,即将数据离散化。
- 以离散化数组为基础,建一个全 \(0\) 的线段树,称作基础主席树;
- 对原数据中每一个 \([1,i]\) 区间统计,有序地插入新节点(题目中 \(i\) 每增加 \(1\) 就会多一个数,仅需对主席树对应的节点增加 \(1\) 即可);
- 对于查询 \([1,r]\) 中第 \(k\) 小值的操作,找到 \([1,r]\) 对应的根节点,我们按照线段树的方法操作即可(这个根节点及其子孙构成的必定是一颗线段树)。
模板题的解决
现在我们真正来解决区间询问 \([l,r]\) 的问题。
构建主席树的方法是没有问题的,问题正在于区间询问怎么写。其实,解决方案就是将主席树 \([1,r]\) 减去主席树 \([1,l-1]\) 就行了。其实这个原因并不难想,首先看到主席树的底层,全部是对数的统计。当主席树 \([1,r]\) 减去主席树 \([1,l-1]\) 时,统计也跟着减了,也就是说,现在统计记录的是 \([l,r]\) 区间。
而我们不需要单独减,只需要边递归查询边减,具体见查询部分代码。
// 初始的 x 和 y 分别代表的是点 l-1 和点 r,l 和 r 分别表示线段树点代表的区间,初始的 k 如题
ll qry(ll x, ll y, ll l, ll r, ll k) {
ll res, mid = (l + r) >> 1, t = sum[ls[y]] - sum[ls[x]];
// 因为主席树是区间统计好了的,只要减一下即可,无需递归到叶子再处理
if(l == r) return l; // 找到目标位置
if(t >= k) res = qry(ls[x], ls[y], l, mid, k);
else res = qry(rs[x], rs[y], mid + 1, r, k - t); // 右子树记得改变 k 的值
return res;
}
题目复杂度分析
题目一开始的离散化复杂度为 \(O(n\lg n)\),构建基础主席树复杂度为 \(O(n\lg n)\),统计并插入的复杂度是 \(O(n\lg n + n\lg n)=O(n\lg n)\),询问的复杂度是 \(O(m\lg n)\)。
复杂度总和就是 \(O((m+n)\lg n)\)。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 200005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, m, q, cnt, a[MAXN], b[MAXN], sum[MAXN << 5];
long long rt[MAXN], ls[MAXN << 5], rs[MAXN << 5];
void build(long long &p, long long l, long long r) {
p = (++ cnt); if(l == r) return;
long long mid = (l + r) >> 1;
build(ls[p], l, mid), build(rs[p], mid + 1, r);
return;
}
long long upd(long long t, long long l, long long r, long long k) {
long long p = (++ cnt);
ls[p] = ls[t], rs[p] = rs[t], sum[p] = sum[t] + 1;
if(l == r) return p;
long long mid = (l + r) >> 1;
if(k <= mid) ls[p] = upd(ls[p], l, mid, k);
else rs[p] = upd(rs[p], mid + 1, r, k);
return p;
}
long long qry(long long x, long long y, long long l, long long r, long long k) {
long long res, mid = (l + r) >> 1, t = sum[ls[y]] - sum[ls[x]];
if(l == r) return l;
if(t >= k) res = qry(ls[x], ls[y], l, mid, k);
else res = qry(rs[x], rs[y], mid + 1, r, k - t);
return res;
}
int main() {
n = read(), m = read();
for(int i = 1; i <= n; i ++) b[i] = a[i] = read();
sort(b + 1, b + n + 1);
q = unique(b + 1, b + n + 1) - b - 1;
build(rt[0], 1, q);
for(int i = 1; i <= n; i ++) {
long long t = lower_bound(b + 1, b + q + 1, a[i]) - b;
rt[i] = upd(rt[i - 1], 1, q, t);
}
while(m --) {
long long l = read(), r = read(), k = read();
cout << b[qry(rt[l - 1], rt[r], 1, q, k)] << "\n";
}
return 0;
}
\(\text{luogu-4559}\)
作为一名大学生,九条可怜在去年参加了她人生中的最后一次军训。
军训中的一个重要项目是练习列队,为了训练学生,教官给每一个学生分配了一个休息位置。每次训练开始前,所有学生都在各自的休息位置休息,但是当教官发出集合命令后,被点到的学生必须要到指定位置集合。
为了简化问题,我们把休息位置和集合位置抽象成一根数轴。一共有 \(n\) 个学生,第 \(i\) 个学生的休息位置是 \(a_i\)。每一次命令,教官会指定一个区间 \([l,r]\) 和集合点 \(K\) ,所有编号在 \([l,r]\) 内的学生都必须赶到集合点列队。在列队时,每一个学生需要选择 \([K,K+r-l]\) 中的一个整数坐标站定且不能有任何两个学生选择的坐标相同。学生从坐标 \(x\) 跑到坐标 \(y\) 需要耗费体力 \(\vert y-x \vert\) 。
在一天的训练中,教官一共发布了 \(m\) 条命令 \((l,r,K)\) ,现在你需要计算对于每一条命令,在所有可能的列队方案中,消耗的体力值总和最小是多少。
以下是对题意的一些补充:
-
任何两条命令是无关的,即在一条集合命令结束后,所有学生都会回到自己的休息位置,然后教官才会发出下一条命令。
-
在集合的时候,可能有编号不在 \([l,r]\) 内的学生处在区间 \([K,K+r-l]\) 中,这时他会自己跑开,且跑动的距离不记在消耗的体力值总和中。
\(n,m \leq 5 \times 10^5,1 \leq a_i,K \leq 10^6\),学生休息的位置两两不同。
以下部分题解来自于 题解 P4559 【JSOI2018列队】 - 洛谷专栏。
不难证明,学生跑到最终的位置时,他们的相对位置不改变至少是最优解之一,这可以脑补一下。
所以我们只需要求最终相对位置不变时的答案即可。
因为学生两两位置不同,所以最终有一部分学生向右跑,有一部分学生向左跑。
向右跑的学生对答案的贡献是 \(k+rk_i-1-a_i\),\(rk_i\) 表示他的位置在这个编号区间中的学生是第 \(rk_i\) 小的。
向左跑的学生对答案的贡献是 \(a_i-k-rk_i+1\)。
显然左边一部分学生向右跑,右边一部分学生向左跑。
考虑使用主席树处理这个问题。
对权值线段树进行可持久化,则编号区间内的学生就是两个线段树相减。
考虑递归进一个区间 \([l,r]\),有 \(4\) 种情况。
-
这个区间中没有学生。直接返回 \(0\)。
-
这个区间中的学生全部往右跑。返回 \((\sum k+rk_i-1)-(\sum a_i)\),左边是等差数列求和的形式,右边可以直接记。
-
这个区间中的学生全部往左跑。返回 \((\sum a_i)-(\sum k+rk_i-1)\)。
-
不能确定这个区间中的学生的方向,递归到子树处理。
直接在主席树上实现即可。
时间复杂度 \(O(n\log n+m\log n\times\text{wys})\),因为我不会分析递归的复杂度,可能是 \(O(m\log n)\) 的。
#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 500005
#define MAXM 11000005
#define ll long long
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
ll n, m, rt[MAXN], ls[MAXM], rs[MAXM], cnt, sum[MAXM], sz[MAXM];
void upd(ll &p, ll l, ll r, ll x) {
ls[++ cnt] = ls[p], rs[cnt] = rs[p];
sz[cnt] = sz[p] + 1, sum[cnt] = sum[p] + x;
p = cnt; if(l == r) return;
long long mid = (l + r) >> 1;
if(x <= mid) upd(ls[p], l, mid, x);
else upd(rs[p], mid + 1, r, x);
return;
}
ll qry(ll x, ll y, ll l, ll r, ll s, ll k) {
if(!(sz[x] - sz[y])) return 0;
long long si = sz[x] - sz[y], sm = sum[x] - sum[y];
if(l >= s + k) return sm - (2 * k + 2 * s + si - 1) * si / 2;
if(r <= s + k + si - 1) return (2 * k + 2 * s + si - 1) * si / 2 - sm;
ll mid = (l + r) >> 1, t = sz[ls[x]] - sz[ls[y]];
return qry(ls[x], ls[y], l, mid, s, k) +
qry(rs[x], rs[y], mid + 1, r, s + t, k);
}
int main() {
n = read(), m = read();
for(int i = 1; i <= n; i ++)
upd(rt[i] = rt[i - 1], 1, 1e6, read());
while(m --) {
ll l = read(), r = read(), k = read();
cout << qry(rt[r], rt[l - 1], 1, 1e6, 0, k) << "\n";
}
return 0;
}
\(\text{luogu-2633}\)
给定一棵 \(n\) 个节点的树,每个点有一个权值。有 \(m\) 个询问,每次给你 \(u,v,k\),你需要回答 \(u \text{ xor last}\) 和 \(v\) 这两个节点间第 \(k\) 小的点权。
其中 \(\text{last}\) 是上一个询问的答案,定义其初始为 \(0\),即第一个询问的 \(u\) 是明文。
\(1\le n,m \le 10^5\),点权在 \([1, 2 ^ {31} - 1]\) 之间。
实际上主席树就是一个类似前缀的东西。
对于这题只需要从根节点建主席树,每次询问就是把两个点路径上的主席树提取出来。
然后求区间第 \(k\) 小就好了,实际上是四个主席树加减。
下面是比较形式化的描述。
定义 \(s_x\) 为从根节点到 \(x\) 节点的“前缀主席树”,那么,包含 \(x \to y\) 的所有信息的主席树就是:
注意离散化和主席树的代码细节。
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
using namespace std;
#define MAXN 100005
#define ll long long
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
ll n, m, q, cnt, rt[MAXN], lst, a[MAXN], b[MAXN], fa[MAXN][20], dep[MAXN], id[MAXN];
struct node { ll l, r, sum; } t[MAXN << 5];
vector<ll> v[MAXN];
void build(node &p, ll l, ll r) {
p.sum = 0; if(l == r) return;
ll mid = (l + r) >> 1;
build(t[p.l = (++ cnt)], l, mid);
build(t[p.r = (++ cnt)], mid + 1, r);
return;
}
void insert(node s, node &p, ll l, ll r, ll x) {
p.sum = s.sum + 1; if(l == r) return;
ll mid = (l + r) >> 1;
if(x <= mid) insert(t[s.l], t[p.l = (++ cnt)], l, mid, x), p.r = s.r;
else insert(t[s.r], t[p.r = (++ cnt)], mid + 1, r, x), p.l = s.l;
return;
}
void dfs(ll x, ll f) {
insert(t[rt[f]], t[rt[x] = (++ cnt)], 1, q, id[x]);
fa[x][0] = f, dep[x] = dep[f] + 1;
for(int i = 1; i <= 19; i ++) fa[x][i] = fa[fa[x][i - 1]][i - 1];
for(auto y : v[x]) if(y != f) dfs(y, x);
return;
}
ll lca(ll x, ll y) {
if(dep[x] < dep[y]) swap(x, y);
for(int i = 19; i >= 0; i --)
if(dep[fa[x][i]] >= dep[y]) x = fa[x][i];
if(x == y) return x;
for(int i = 19; i >= 0; i --)
if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
ll qry(node x, node y, node z, node w, ll l, ll r, ll k) {
if(l == r) return l;
ll sum = t[x.l].sum + t[y.l].sum - t[z.l].sum - t[w.l].sum;
ll mid = (l + r) >> 1;
if(k <= sum) return qry(t[x.l], t[y.l], t[z.l], t[w.l], l, mid, k);
return qry(t[x.r], t[y.r], t[z.r], t[w.r], mid + 1, r, k - sum);
}
ll query(ll x, ll y, ll k) {
ll la = lca(x, y);
return b[qry(t[rt[x]], t[rt[y]], t[rt[la]],
t[rt[fa[la][0]]], 1, q, k)];
}
int main() {
n = read(), m = read();
for(int i = 1; i <= n; i ++) b[i] = a[i] = read();
for(int i = 1; i < n; i ++) {
ll x = read(), y = read();
v[x].push_back(y), v[y].push_back(x);
}
sort(b + 1, b + n + 1);
q = unique(b + 1, b + n + 1) - b - 1;
for(int i = 1; i <= n; i ++)
id[i] = lower_bound(b + 1, b + q + 1, a[i]) - b;
build(t[rt[0] = (++ cnt)], 1, q), dfs(1, 0);
while(m --) {
ll x = read(), y = read(), k = read();
ll res = query(x ^ lst, y, k);
cout << res << "\n"; lst = res;
}
return 0;
}
本文来自博客园,作者:So_noSlack,转载请注明原文链接:https://www.cnblogs.com/So-noSlack/p/19514051

浙公网安备 33010602011771号