0x40「数据结构进阶」例题
可持久化数据结构能够维护数据集的历史状态,其核心思想在于仅仅维护数据集改变的量,这样其时间复杂度不会改变,空间复杂度增长仅为与时间同级的规模。
可持久化Trie
向可持久化Trie依次插入cat,rat,cab,fry的过程如下。
我们发现,每次从第i个根节点开始遍历,就会访问到前i个字符串。
例题
BZOJ 3261
题意:非负整数序列a,长度n,m个操作共两类。第一类操作A x在序列末尾插入一个数x,序列长度+1。第二类操作P l r x,要求找到l<=p<=r,使得a[p] xor a[p+1] xor … xor a[n] xor x最大,求最大值。n,m<=3e5
由异或的前缀和性质,我们设s[i]表示a序列的前i个数异或起来的结果,那么a[p] xor a[p+1] xor … xor a[q]=s[q] xor s[p-1]。因此,我们转化为求一个p满足使得s[p] xor s[n] xor x最大。
若只有,那么就是可持久化Trie模板:将每个数进行二进制拆位,每次从字典树根节点出发贪心访问与当前位相反的边即可。
考虑的情况,为了维护,我们为每个节点增加一些信息,设end[x]表示x节点时第几个二进制数的末尾节点。lastst[x]表示以x为根的子树中end的最大值,每次只需要考虑lastst值大于等于l-1的节点即可。
代码如下
void insert(int i, int k, int p, int q) {
if (k < 0) {
latest[q] = i;
return;
}
int c = s[i] >> k & 1;
if (p) trie[q][c ^ 1] = trie[p][c ^ 1];
trie[q][c] = ++tot;
insert(i, k - 1, trie[p][c], trie[q][c]);
latest[q] = max(latest[trie[q][0]], latest[trie[q][1]]);
}
int ask(int now, int val, int k, int limit) {
if (k < 0) return s[latest[now]] ^ val;
int c = val >> k & 1;
if (latest[trie[now][c ^ 1]] >= limit)
return ask(trie[now][c ^ 1], val, k - 1, limit);
else
return ask(trie[now][c], val, k - 1, limit);
}
int main() {
cin >> n >> m;
latest[0] = -1;
root[0] = ++tot;
insert(0, 23, 0, root[0]);
for (int i = 1; i <= n; i++) {
int x; scanf("%d", &x);
s[i] = s[i - 1] ^ x;
root[i] = ++tot;
insert(i, 23, root[i - 1], root[i]);
}
for (int i = 1; i <= m; i++) {
char op[2]; scanf("%s", op);
if (op[0] == 'A') {
int x; scanf("%d", &x);
root[++n] = ++tot;
s[n] = s[n - 1] ^ x;
insert(n, 23, root[n - 1], root[n]);
}
else {
int l, r, x; scanf("%d%d%d", &l, &r, &x);
printf("%d\n", ask(root[r - 1], x ^ s[n], 23, l - 1));
}
}
}
可持久化线段树
显然,可持久化线段树与可持久化Trie实现机理相同,这里不做赘述。
PS:可持久化线段树难以完成区间修改,因为延迟标记不可用。解决方法是用有局限性的标记永久化操作(例题 HDOJ 4348)
类型定义
struct SegmentTree {
int lc, rc;
int dat;
} tree[MAX_MLOGN];
int tot, root[MAX_M];
int n, a[MAX_N];
建树
int build(int l, int r) {
int p = ++tot;
if (l == r) { tree[p].dat = a[l]; return p; }
int mid = (l + r) >> 1;
tree[p].lc = build(l, mid);
tree[p].rc = build(mid + 1, r);
tree[p].dat = max(tree[tree[p].lc].dat, tree[tree[p].rc].dat);
return p;
}
//main()中
root[0] = build(1, n);
插入节点
int insert(int now, int l, int r, int x, int val) {
int p = ++tot;
tree[p] = tree[now];
if (l == r) {
tree[p].dat = val;
return p;
}
int mid = (l + r) >> 1;
if (x <= mid)
tree[p].lc = insert(tree[now].lc, l, mid, x, val);
else
tree[p].rc = insert(tree[now].rc, mid + 1, r, x, val);
tree[p].dat = max(tree[tree[p].lc].dat, tree[tree[p].rc].dat);
return p;
}
//main()中
root[i] = insert(root[i - 1], 1, n, x, val);
例题
POJ2104 K-th Number
本题除了可持久化线段树做法外,还有归并树,整体二分,线段树套平衡树等做法,这里主要介绍可持久化线段树做法。
离散化A序列,值域为[1,T]。
在[1,T]上建立可持久化线段树(在值域上建立),每个节点保留一个cnt值表示节点对应值域区间[L,R]中有多少个数,初始值为0。
对于每个A[i],则在A[i]离散化后对应的节点完成单点修改,令其cnt+1。
而因为对于每个询问和对应值域区间相同,所以的值域区间的cnt值减去的值域区间的cnt值就是有多少个数落在值域区间内,即可持久化线段树中两个代表相同值域的节点具有可减性。
核心代码如下
int ask(int p, int q, int l, int r, int k) {
if (l == r) return l;
int mid = (l + r) >> 1;
int lcnt = tree[tree[p].lc].cnt - tree[tree[q].lc].cnt;
if (k <= lcnt) return ask(tree[p].lc, tree[q].lc, l, mid, k);
else return ask(tree[p].rc, tree[q].rc, mid + 1, r, k - lcnt);
}
//main()中
int ans = ask(root[ri], root[li - 1], 1, t, ki);