浅谈可持久化
可持久化权值线段树
可持久化意味着在某次操作时,可以操作历史版本的结构,支持回退历史的功能。在权值线段树中,不需要知道某个数具体是多少,只需要保留数与数之间的相对大小关系,这就涉及到离散化问题。离散化数组后再进行建树操作,在该题中,查询静态区间第k大,考虑使用可持久化权值线段树。
正常情况下,我们需要用权值线段树的每个节点去记录当前区间有多少个数,现在考虑有一颗完整的、初始化的权值线段树等着我们去插入数值,每当插入一个数以后,能够发现的是这个数影响到的节点个数只有 l o g n logn logn个,其他节点是不受影响的,那么我们就可以利用这个性质建立一个新的根节点,并把这个根节点复制成上一个数插入后的版本,当前根节点和上一个根节点不同的地方在于插入新的数值后影响的那条树链,这样我们就有了 n n n个版本的权值线段树。
然后查询区间第k大,可知这 n n n个版本的权值线段树相当于一个前缀和数组,因为插入一个数就新建一棵树,那么查询 [ L , R ] [L,R] [L,R]区间第 k k k大,只需要用第 R R R棵树整体减去第 L − 1 L-1 L−1棵树,那么剩下的就是区间 [ L , R ] [L,R] [L,R]的权值线段树,剩下的就是在权值线段树上找第 k k k大了。
找第 k k k大,需要先判断左子树有多少数,如果 k k k大于整棵左子树的大小,那么要找的数一定在右子树,否则一定在左子树,按照这个思路便可以找到第 k k k大,还需要注意的是,我们找到的第 k k k大是离散化后的数,需要在映射回它原来的数。
建立可持久化权值线段树需要开一个节点内存池,根节点数组和节点计数器,然后一个节点记录的就是左子树根节点的编号,右子树根节点的编号和它记录的区间内有多少数。节点内存池需要*40或者*50,看个人喜好,然后数组名称是为了纪念发明者。
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 2e5 + 10;
int n, m;
struct node {
int l, r, sum;
}hjt[N * 40];
int a[N], cnt, root[N], id[N];
void insert(int l, int r, int pre, int &now, int p) {
hjt[++cnt] = hjt[pre];
now = cnt;
hjt[now].sum++;
if (l == r) {
return;
}
int mid = l + r >> 1;
if (p <= mid) {
insert(l, mid, hjt[pre].l, hjt[now].l, p);
} else {
insert(mid + 1, r, hjt[pre].r, hjt[now].r, p);
}
}
int query(int l, int r, int L, int R, int k) {
if (l == r) {
return l;
}
int mid = l + r >> 1;
int num = hjt[hjt[R].l].sum - hjt[hjt[L].l].sum;
if (k <= num) {
return query(l, mid, hjt[L].l, hjt[R].l, k);
} else {
return query(mid + 1, r, hjt[L].r, hjt[R].r, k - num);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
vector<int> z;
for (int i = 1; i <= n; i++) {
cin >> a[i];
z.push_back(a[i]);
}
sort(z.begin(), z.end());
z.erase(unique(z.begin(), z.end()), z.end());
for (int i = 1; i <= n; i++) {
auto getid = [&](int x) -> int {
return lower_bound(z.begin(), z.end(), x) - z.begin() + 1;
};
insert(1, n, root[i - 1], root[i], getid(a[i]));
}
while (m--) {
int l, r, k;
cin >> l >> r >> k;
cout << z[query(1, n, root[l - 1], root[r], k) - 1] << '\n';
}
return 0;
}
主席树单点修改+区间查询
E. Army Creation
AC代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
struct node {
int l, r, sum;
}hjt[100010 * 40];
vector<int> G[100010];
int n, k, q, root[100010], cnt;
void insert(int l, int r, int pre, int &now, int p) {
hjt[++cnt] = hjt[pre];
now = cnt;
hjt[now].sum++;
if (l == r) {
return;
}
int mid = l + r >> 1;
if (p <= mid) {
insert(l, mid, hjt[pre].l, hjt[now].l, p);
} else {
insert(mid + 1, r, hjt[pre].r, hjt[now].r, p);
}
}
int query(int l, int r, int L, int R, int pre, int now) {
if (L <= l && r <= R) {
return hjt[now].sum - hjt[pre].sum;
}
int mid = l + r >> 1;
LL ans = 0;
if (L <= mid) {
ans += query(l, mid, L, R, hjt[pre].l, hjt[now].l);
}
if (R > mid) {
ans += query(mid + 1, r, L, R, hjt[pre].r, hjt[now].r);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
G[x].push_back(i);
int sz = G[x].size();
int last;
if (sz > k) {
last = G[x][sz - k - 1];
} else {
last = 0;
}
insert(0, n, root[i - 1], root[i], last);
}
cin >> q;
LL ans = 0;
while (q--) {
int x, y;
cin >> x >> y;
x = (x + ans) % n + 1;
y = (y + ans) % n + 1;
if (x > y) {
swap(x, y);
}
ans = query(0, n, 0, x - 1, root[x - 1], root[y]);
cout << ans << '\n';
}
return 0;
}
可持久化数组
先看题目,操作一是在某个历史版本上修改某一个位置上的值,操作二是访问某个历史版本上的某一位置的值,那么我们需要的就是记录历史版本的所有位置的值是多少,但这样空间和时间都是不允许的。
考虑建立可持久化数组,可持久化数组是依靠线段树的性质进行建立的,即除了线段树的叶子节点,所有节点都不保存数值信息,只保留它的左子树的根节点编号和右子树的根节点编号,保留数值信息的只有叶子节点。这样每次修改或者访问,我们通过可持久化就可以在时空复杂度都是 l o g log log的情况下处理。
可持久化权值线段树没有建树操作,但可持久化数组需要建树操作,也就是版本0的状态。在该题中,询问操作只需要查询历史版本的值即可,然后复制一份历史版本。修改操作需要先复制历史版本,然后将 p o s pos pos位置的数改为 k k k,其余均为简单的线段树操作
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 1e6 + 5;
int n, m, a[N], x, y, ver, op;
struct node {
int l, r, val;
}hjt[N * 40];
int cnt, root[N];
void buildtree(int l, int r, int &now) {
now = ++cnt;
if (l == r) {
hjt[now].val = a[l];
return;
}
int mid = l + r >> 1;
buildtree(l, mid, hjt[now].l);
buildtree(mid + 1, r, hjt[now].r);
}
void modify(int l, int r, int pre, int &now, int pos, int k) {
now = ++cnt;
hjt[now] = hjt[pre];
if (l == r) {
hjt[now].val = k;
return;
}
int mid = l + r >> 1;
if (pos <= mid) {
modify(l, mid, hjt[pre].l, hjt[now].l, pos, k);
} else {
modify(mid + 1, r, hjt[pre].r, hjt[now].r, pos, k);
}
}
int query(int l, int r, int now, int pos) {
if (l == r) {
return hjt[now].val;
}
int mid = l + r >> 1;
if (pos <= mid) {
return query(l, mid, hjt[now].l, pos);
} else {
return query(mid + 1, r, hjt[now].r, pos);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
buildtree(1, n, root[0]);
for (int i = 1; i <= m; i++) {
cin >> ver >> op;
if (op == 1) {
cin >> x >> y;
modify(1, n, root[ver], root[i], x, y);
} else {
cin >> x;
cout << query(1, n, root[ver], x) << '\n';
root[i] = root[ver];
}
}
return 0;
}
可持久化并查集
先看题目,操作一是合并两个集合,操作二是回退到第 k k k个版本,操作三是询问两个数是否在同一集合。如果只有操作一和操作三,那么普通的并查集就可以做,但是操作二的回退操作让并查集失了智,那么考虑可持久化并查集来记录历史版本的状态。
可知,我们要可持久化的是 f a fa fa数组,另外还有一个 d e p dep dep数组后文再说。初始的时候都令 d e p dep dep数组的值为0,而 f a fa fa数组则是下标值,所以对 f a fa fa数组进行建树操作。
根据并查集,知道并查集有两种优化方式,使得查询时不那么暴力,一种是常见的路径压缩操作,就是一边查询一边修改路径。而可持久化并查集用的是第二种优化方式,按秩合并。并查集的找父亲的操作和合并操作,像是在一棵树上找根节点一样,那么我们就可以记录树高,然后将较矮的树合并到较高的树上去,而路径压缩则会破坏树的性质。
对树深和 f a fa fa数组同时进行可持久化,需要注意的是 f i n d find find数组不要写 f a [ x ] = f i n d ( f a [ x ] ) fa[x]=find(fa[x]) fa[x]=find(fa[x]),在合并的时候要将矮的树合并到深的树上去,若两棵树高度相同,则随意
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 2e5 + 5;
int n, m, op;
struct node {
int l, r, val;
}hjt[N * 80];
int cnt, tot, rootdep[N], rootfa[N];
void buildtree(int l, int r, int &now) {
now = ++cnt;
if (l == r) {
hjt[now].val = ++tot;
return;
}
int mid = l + r >> 1;
buildtree(l, mid, hjt[now].l);
buildtree(mid + 1, r, hjt[now].r);
}
void modify(int l, int r, int pre, int &now, int pos, int k) {
hjt[now = ++cnt] = hjt[pre];
if (l == r) {
hjt[now].val = k;
return;
}
int mid = l + r >> 1;
if (pos <= mid) {
modify(l, mid, hjt[pre].l, hjt[now].l, pos, k);
} else {
modify(mid + 1, r, hjt[pre].r, hjt[now].r, pos, k);
}
}
int query(int l, int r, int now, int pos) {
if (l == r) {
return hjt[now].val;
}
int mid = l + r >> 1;
if (pos <= mid) {
return query(l, mid, hjt[now].l, pos);
} else {
return query(mid + 1, r, hjt[now].r, pos);
}
}
int find(int ver, int x) {
int xx = query(1, n, rootfa[ver], x);
return xx == x ? x : find(ver, xx);
}
void merge(int ver, int x, int y) {
x = find(ver - 1, x);
y = find(ver - 1, y);
if (x == y) {
rootfa[ver] = rootfa[ver - 1];
rootdep[ver] = rootdep[ver - 1];
} else {
int depx = query(1, n, rootdep[ver - 1], x);
int depy = query(1, n, rootdep[ver - 1], y);
if (depx < depy) {
modify(1, n, rootfa[ver - 1], rootfa[ver], x, y);
rootdep[ver] = rootdep[ver - 1];
} else if (depx > depy) {
modify(1, n, rootfa[ver - 1], rootfa[ver], y, x);
rootdep[ver] = rootdep[ver - 1];
} else {
modify(1, n, rootfa[ver - 1], rootfa[ver], x, y);
modify(1, n, rootdep[ver - 1], rootdep[ver], y, depy + 1);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
buildtree(1, n, rootfa[0]);
for (int i = 1; i <= m; i++) {
cin >> op;
int x, y;
if (op == 1) {
cin >> x >> y;
merge(i, x, y);
} else if (op == 2) {
cin >> x;
rootfa[i] = rootfa[x];
rootdep[i] = rootdep[x];
} else {
cin >> x >> y;
rootfa[i] = rootfa[i - 1];
rootdep[i] = rootdep[i - 1];
int xx = find(i, x);
int yy = find(i, y);
if (xx == yy) {
cout << 1 << '\n';
} else {
cout << 0 << '\n';
}
}
}
return 0;
}