「学习笔记」可持久化线段树

注:此博客写于 2017.12

可持久化线段树

常见的一个实现是主席树,由HJT主席引入中国OI界。

基本思想

考虑一颗不同的线段树,进行单点修改。注意到每一次只会修改 \(\log\) 个节点。

引入函数式编程的思想,我们不进行修改,而是新建节点。操作的时间复杂度仍然为 \(O(log n)\),但是空间为 \(O(n log n)\)

从第 \(i\) 个根开始访问即可得到第 \(i\) 次修改后的历史版本。

经典应用:区间K小值。

考虑建立权值线段树,按照下标插入,\([l,r]\) 中,某个元素出现了多少遍,可以通过 \(r\)\(l-1\) 版本的线段树作差得到。然后在主席树上二分即可。

一些好题

BZOJ1146-[CTSC2008]网络管理Network 维护一棵树,要求支持:单点修改权值,路径第 \(K\) 大。
\(n, Q \leq 80000\)

树上带修改主席树,真·裸题。 首先这道题从2个log到4个log的算法都能过。话说zzd Dalao用了二分答案+树链剖分+主席树,竟然只比两个log慢4倍!

树上主席树有一个常见的套路:每一个节点的版本,都是在父节点的基础上修改。这样 \(root[x] + root[y] - root[lca(x,y)] - root[fa[lca(x,y)]]\) 就得到这条链了。

再考虑这个带修改。注意到,一个点修改之后,只对它的子节点与外界的询问有影响。稍微分类讨论一下,就能发现,我们只需要修改子树中所有版本即可。

这个可以用树状数组套权值线段树解决。修改的时候,需要改变 \(\log\) 个节点;查询的时候,也只需要考虑 \(\log\) 个节点的贡献总和。

复杂度 \(O(n \log ^ 2 n)\)

BZOJ3674-可持久化并查集加强版 维护可持久化并查集。

可持久化数组。 注意到主席树就是一个可持久化数组,维护并查集的 \(fa,rank\) ,按秩合并即可。

BZOJ2653-middle 给定一个长度为 \(n\) 的序列,和若干个询问。每个询问 \((a,b,c,d)\) 要求求出 \(l\)\([a,b]\) 之间, \(r\)\([c,d]\) 之间的,构成的子序列的中位数的最大值。

二分答案,主席树,中位数。 中位数嘛,有一个常见的套路:二分答案 \(x\) ,令所有小于 \(x\)\(-1\),否则为 \(1\) 。如果左后所有数之和大于等于 \(0\) ,说明符合要求,可以继续变大。

这题同样也是二分答案。注意到,对于区间 \([b,c]\) 是一定要选择的。而对于 \([a,b-1]\)\([c+1,d]\) 我们最好是选择一个最大的前缀和后缀。就是线段树最基础的维护了。

预处理主席树的时候,默认所有数都为 \(1\) ,插入一个数之后,修改对应的位置为 \(-1\) ,这样就能方便判断了。

BZOJ3545-[ONTAK2010]Peaks 给定一张图,每个点有点权,边有边权。查询 \((v,x,k)\) 要求回答:从 \(x\) 出发,走边权不超过 \(x\) 的边,能都到达的第 \(k\) 大点权。

并查集虚拟点,dfs序,主席树。 显然,根据最小瓶颈树的理论,求出的最小生成树一定是最优的。

考虑按照边权从小到大插入。对于边 \((u,v)\) ,我们找到它们对应的祖先 \(a,b\) 造一个虚拟点 \(c\) 对应的点权是 \((u,v)\) 的边权。

这样有什么用处呢?发现对于点 \((x,y)\) ,他们的 \(lca\) 对应的点权,就是从 \(x\)\(y\) 需要经过路径最大值的最小值。

这样就可以搞出整棵,包含 \(2n-1\) 个节点的树了。首先倍增预处理,对于 \(v\) ,我们倍增向上跳到点 \(u\),但是点权不能超过 \(x\) ,那么, \(v\) 能达到的点就是 \(u\) 的子树!

dfs序预处理一下,然后就是区间K大了。

BZOJ3772-精神污染 给定一棵树,和若干条路径,求其中一条路径包含另一条路径的概率。

欧拉序,主席树。 确实是一道好题。注意到,这相当于求 一条路径的两个端点都在另一条路径上的方案数。

考虑首先限制 \(x\) 。对于 \((x,y)\),每个点建一棵主席树,\(x\) 在父亲版本的基础上加入另一个端点 \(y\)

对于查询路径上两个端点都被包含,相当于是路径上插入的另一个点也在路径上。

于是问题就转化为 统计路径上的点数之和。

可以用欧拉序:保存一个节点入栈和出栈的时间。入栈的位置 \(+1\) ,出栈的位置 \(-1\)

有一个很神奇的性质:\((x,y)\) 之间的点数之和(\(x,y\)是祖先/后代关系),就是 \([in[x],in[y]]\)之和(其他节点都没有计算,或者被抵消;其实就是 \((x,y)\)当前还在栈中)。路径就拆成 \([x,lca],[lca,y],[lca,lca]\)

主要代码,

void insert(int &o, int l, int r, int x, int y);
int query(int o1, int o2, int o3, int o4, int l, int r, int x, int y) {
    if (l == x && y == r) return T[o1].sum + T[o2].sum - T[o3].sum - T[o4].sum;
  ...
}
void dfs1(int u) {
    in[u] = ++clk;
  ...
    out[u] = ++clk;
}
void dfs2(int u) {
    root[u] = root[fa[u][0]];
    loop (k, headV[u], linkV) {
        insert(root[u], 1, 2*n, in[v[k]], 1);
        insert(root[u], 1, 2*n, out[v[k]], -1);
    }
  ...
}
int main() {
    rep (i, 1, m) {
        a[i] = read(); b[i] = read();
        addEdge(headV, v, linkV, sizeV, a[i], b[i]);
    }
    dfs1(1);
  ...
    dfs2(1);
    rep (i, 1, m) {
        c = lca(a[i], b[i]);
        ans += query(root[a[i]], root[b[i]], root[c], root[fa[c][0]], 1, 2*n, in[c], in[a[i]]);
        ans += query(root[a[i]], root[b[i]], root[c], root[fa[c][0]], 1, 2*n, in[c], in[b[i]]);
        ans -= query(root[a[i]], root[b[i]], root[c], root[fa[c][0]], 1, 2*n, in[c], in[c]) + 1;
    }
  ...
    return 0;
}

BZOJ4546-[CodeChef]XRQRS 给定一个初始时为空的整数序列以及一些询问:
类型1:在数组后面就加入数字 \(x\)
类型2:在区间 \([L,R]\) 中找到 \(y\),最大化 \(x xor y\)
类型3:删除数组最后 \(K\)个元素。
类型4:在区间 \([L,R]\) 中,统计小于等于 \(x\) 的元素个数。
类型5:在区间 \([L,R]\) 中,找到第 \(k\) 小的数。

可持久化Trie树。 本质上就是主席树,都是最基础的操作。需要注意的是操作 \(2\) ,就是经典的 Trie树 上贪心。

猜你喜欢

转载自www.cnblogs.com/cyanic/p/9159457.html