2532 字
13 分钟
算法笔记:可持久化数据结构

狠补一下一些不太熟的东西。

可持久化线段树#

可持久化数组#

要求维护一个数组,进行mm次操作,每次操作都生成一个新的版本(版本编号从11mm)。操作如下:

① 在某个给定历史版本上的基础上,该版本修改某个数的值。

② 在某个给定历史版本的基础上,该版本不修改任何值,但要求查询第xx个数。

朴素的实现肯定是爆时间空间的。考虑有什么方法维护,可持久化线段树可以维护。对于线段树,单次修改实际上只修改了一个长度为logn\log n的路径。那我们重新建树就行了,每次的新树,继承了前一个版本的树的剩余节点,新增的节点只有logn\log n个。

template<typename T>
class PersistentArray {
private:
struct Node {
T val;
int l, r;
int lson, rson;
};
vector<Node> node;
vector<int> root;
vector<T> data;
/* 克隆当前节点,返回新节点的索引 */
int clone(int base) {
node.push_back(node[base]);
return node.size() - 1;
}
/* 创建新节点,返回新节点的索引 */
int create(T val, int l, int r, int lson, int rson) {
node.push_back({val, l, r, lson, rson});
return node.size() - 1;
}
/* 建立持久化数组的初始版本 */
int build(int l, int r) {
if (l == r)
return create(data[l], l, r, -1, -1);
else {
int mid = (l + r) / 2;
int lson = build(l, mid);
int rson = build(mid + 1, r);
return create(0, l, r, lson, rson);
}
}
/* 修改持久化数组路径 */
int revise_path(int u, int pos, const T val) {
int p = clone(u);
if (node[u].l == node[u].r)
node[p].val = val;
else {
int mid = (node[u].l + node[u].r) / 2;
if (pos <= mid)
node[p].lson = revise_path(node[u].lson, pos, val);
else
node[p].rson = revise_path(node[u].rson, pos, val);
}
return p;
}
/* 查询持久化数组的某个版本的值 */
T ask_path(int u, int pos) {
if (node[u].l == node[u].r)
return node[u].val;
else {
int mid = (node[u].l + node[u].r) / 2;
if (pos <= mid)
return ask_path(node[u].lson, pos);
else
return ask_path(node[u].rson, pos);
}
}
public:
PersistentArray(vector<T>& initData) {
data = initData;
// 好像不加这个会RE
node.reserve(32 * initData.size());
root.reserve(initData.size()); // 预留空间
root.push_back(build(0, data.size() - 1));
}
/* 修改指定版本的数组并返回新版本号 */
int revise(int v, int pos, const T val) {
root.push_back(revise_path(root[v], pos, val));
return root.size() - 1;
}
/* 查询指定版本的数组的某个位置的值 */
T ask(int v, int pos) {
return ask_path(root[v], pos);
}
/* 复制一个版本(不修改)并返回版本号 */
void copy(int v) {
root.push_back(root[v]);
}
};

静态区间第kk小问题#

前置:权值线段树

给定 nn 个整数构成的序列 aa,多次询问,将对于指定的闭区间 [l,r][l, r] 查询其区间内的第 kk 小值。

假设我们对整个数组查询第kk小,会怎么做?排序一遍,然后从小到大就是了。如果我们只需单次查询,那就给定了区间[l,r][l,r]后,同样的,对原先数组排序一遍,然后从小到大遍历排序后的全数组,对于每个元素检查原始位置是否在[l,r][l,r]中,直到找到[l,r][l,r]中的第kk小。

但是如果需要多次查询就比较麻烦了。我们单次查询的复杂度已经到了O(nlogn)O(n\log n),进行qq次查询,复杂度会到O(qnlogn)O(q\cdot n\log n),是比较坏的。因此,有一个想法就是,我们只做一次[1,n][1,n]的查询,能不能通过这一次的查询,预处理构造出一个数据结构,让我们能满足比较快的对[l,r][l,r]的查询?我们考虑记录一下nn次插入的每次插入后的版本。很明显我们可以对这nn次插入后的版本二分查询[l,r][l,r]中是否已经插入了kk个元素,然后对恰好插入了kk个元素的那个版本的线段树,查询[l,r][l,r]当前的最大值,也就是第kk小元素。

这个思路的问题是,我们要建立nn个线段树,包爆空间的。

如何解决?可以考虑复用数据结构。也就是说,考虑到每次创建一个新版本的线段树,和前一个版本的区别只在于插入了一个新的节点,也就是说线段树上有且仅有一个路径发生了更改。那我们创建新树的时候,只需要创建一个新的根节点,然后创建这个路径即可,其他的部分,我们直接指针指回上一个版本的节点即可。这就可以很大节省空间。

也就是说,要建立一个可持久化的权值线段树,维护每个版本的区间的权值。对于从小到大的每个数xx和它原先的位置pospos,我们把它的位置pospos插入这个权值线段树。

这题也要加离散化会快一点,但是我用了个很快的快读能过。

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5;
int n,q;
/* 可持久化权值线段树 */
template <typename T>
class CountSegmentTree{
private:
struct Node {
int cnt; // 节点权值
T l, r; // [l,r]
int lson, rson; // 左右儿子
};
struct Version{
int root; // 根节点编号
T revise_val; // 这个版本与前一个版本相比修改的数
int revise_cnt; // 修改的权值数
};
vector<Node> node; // 节点列表
vector<Version> version; // 版本信息存储
int create(int val,T l,T r,int lson,int rson) {
node.push_back({val,l,r,lson,rson});
return node.size()-1;
}
int clone(int u){
node.push_back(node[u]);
return node.size()-1;
}
/* 建树,区间[l,r] */
int build(T l,T r) {
if (l==r)
return create(0,l,r,-1,-1);
else {
T mid=(l+r)/2;
return create(0,l,r,build(l,mid),build(mid+1,r));
}
}
/* 对val值的所有路径节点修改x */
void revise(T val,int x){
int u=version.back().root; // 默认从最后一个版本开始修改
int *fa=nullptr;
while (true){
int v=clone(u);
if (fa)
*fa=v;
else
version.push_back(Version{v,val,x});
node[v].cnt+=x;
if (node[u].l==node[u].r)
return ;
T mid=(node[u].l+node[u].r)/2;
u=(val<=mid)?node[u].lson:node[u].rson;
fa=(val<=mid)?(&node[v].lson):(&node[v].rson);
}
}
/* 查询区间权值 */
int ask_range(int u,const T l,const T r){
if (l<=node[u].l&&node[u].r<=r)
return node[u].cnt;
T mid=(node[u].l+node[u].r)/2;
int ret=0;
if (l<=mid)
ret+=ask_range(node[u].lson,l,r);
if (r>mid)
ret+=ask_range(node[u].rson,l,r);
return ret;
}
public:
CountSegmentTree(T l, T r){
// 传入开的桶的范围,[L,R]
node.reserve((r-l+5)*32);
version.push_back(Version{build(l,r),0});
}
/* 插入值val,返回版本号 */
int insert(T val){
revise(val,1);
return version.size()-1;
}
/* 删除值val,返回版本号 */
void remove(T val){
revise(val,-1);
return version.size()-1;
}
/* 查询版本[L,R]的数的个数 */
int ask_cnt(int ver, T l, T r){
return ask_range(version[ver].root,l,r);
}
/* 查询版本[L,R]修改权值的数 */
T ask_version_diff(int ver){
return version[ver].revise_val;
}
};
namespace fastio
{
const int bufl=1<<16;
const double base1[16]={1,1e-1,1e-2,1e-3,1e-4,1e-5,1e-6,1e-7,1e-8,1e-9,1e-10,1e-11,1e-12,1e-13,1e-14,1e-15};
const double base2[16]={1,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10,1e11,1e12,1e13,1e14,1e15};
struct IN{
FILE *IT;char ibuf[bufl],*is=ibuf,*it=ibuf;
IN(){IT=stdin;}IN(char *a){IT=fopen(a,"r");}
inline char getChar(){if(is==it){it=(is=ibuf)+fread(ibuf,1,bufl,IT);if(is==it)return EOF;}return *is++;}
template<typename Temp>inline void getInt(Temp &a){a=0;int b=0,c=getChar();while(c<48||c>57)b^=(c==45),c=getChar();while(c>=48&&c<=57)a=(a<<1)+(a<<3)+c-48,c=getChar();if(b)a=-a;}
IN& operator>>(char &a){a=getChar();return *this;}
IN& operator>>(char *a){do{*a=getChar();}while(*a<=32);while(*a>32)*++a=getChar();*a=0;return *this;}
IN& operator>>(string &a){char b=getChar();while(b<=32)b=getChar();while(b>32)a+=b,b=getChar();return *this;}
IN& operator>>(int &a){getInt(a);return *this;}
IN& operator>>(long long &a){getInt(a);return *this;}
};
struct OUT{
FILE *IT;char obuf[bufl],*os=obuf,*ot=obuf+bufl;int Eps;long double Acc;
OUT(){IT=stdout,Eps=6,Acc=1e-6;}OUT(char *a){IT=fopen(a,"w"),Eps=6,Acc=1e-6;}
inline void ChangEps(int x=6){Eps=x;}
inline void flush(){fwrite(obuf,1,os-obuf,IT);os=obuf;}
inline void putChar(int a){*os++=a;if(os==ot)flush();}
template<typename Temp>inline void putInt(Temp a){if(a<0){putChar(45);a=-a;}if(a<10){putChar(a+48);return;}putInt(a/10);putChar(a%10+48);}
OUT& operator<<(char a){putChar(a);return *this;}
OUT& operator<<(char *a){while(*a>32)putChar(*a++);return *this;}
OUT& operator<<(string a){for(auto c:a)putChar(c);return *this;}
OUT& operator<<(int a){putInt(a);return *this;}
OUT& operator<<(long long a){putInt(a);return *this;}
~OUT(){flush();}
};
}
using fastio::IN;
using fastio::OUT;
IN fin;
OUT fout;
int main(){
// freopen("P3834_9.in","r",stdin);
// freopen("P3834_9.txt","w",stdout);
fin>>n>>q;
typedef pair<int,int> P;
vector<int> x(n+1);
vector<P> a(n);
for (int i=0;i<n;i++){
fin>>a[i].first;
a[i].second=i+1;
x[i+1]=a[i].first;
}
sort(a.begin(),a.end());
CountSegmentTree<int> tree(1,n);
for (P p: a)
tree.insert(p.second);
int L,R,k;
int l,r,mid;
while (q--){
fin>>L>>R>>k;
l=0,r=n;
while (r-l>1){
mid=(l+r)>>1;
if (tree.ask_cnt(mid,L,R)>=k)
r=mid;
else
l=mid;
}
fout<<x[tree.ask_version_diff(r)]<<'\n';
}
return 0;
}

可持久化并查集#

给定 nn 个集合,第 ii 个集合内初始状态下只有一个数,为 ii

mm 次操作。操作分为 33 种:

  • 1 a b 合并 a,ba,b 所在集合;
  • 2 k 回到第 kk 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态;
  • 3 a b 询问 a,ba,b 是否属于同一集合,如果是则输出 11,否则输出 00

普通的并查集进行修改后,主要的区别就是fa数组。那就考虑对fa数组进行可持久化,朴素的思路是建可持久化线段树。但是并查集通常带了路径压缩,没法撤销,因此要使用按深度合并的并查集,保证每次都是log\log的。

因此直接用可持久化线段树或者说可持久化数组。单次修改,需要维护两个版本,一个是fafa的变化,一个是rankrank的变化。

#include <bits/stdc++.h>
using namespace std;
//#undef LOCAL_DEBUG
#ifdef LOCAL_DEBUG
#include "debug.h"
#else
#define debug(...) (void)0
#define debug_array(arr, len) (void)0
#endif
template <typename T>
class PersistentArray {
private:
struct Node {
T val;
int l, r;
int lson, rson;
Node(T val = 0, int l = 0, int r = 0, int lson = -1, int rson = -1)
: val(val), l(l), r(r), lson(lson), rson(rson) {}
};
vector<Node> nodes;
vector<int> root;
vector<T> data;
int create(T val, int l, int r, int lson, int rson) {
nodes.push_back({val, l, r, lson, rson});
return nodes.size() - 1;
}
int build(int l, int r) {
if (l == r) {
return create(data[l], l, r, -1, -1);
} else {
int mid = (l + r) / 2;
int left = build(l, mid);
int right = build(mid + 1, r);
return create(0, l, r, left, right);
}
}
int revise_path(int u, int pos, const T val) {
if (nodes[u].l == nodes[u].r) {
int idx = create(val, nodes[u].l, nodes[u].r, -1, -1);
return idx;
} else {
int lson = nodes[u].lson, rson = nodes[u].rson;
int mid = (nodes[u].l + nodes[u].r) / 2;
if (pos <= mid) {
lson = revise_path(nodes[u].lson, pos, val);
} else {
rson = revise_path(nodes[u].rson, pos, val);
}
return create(nodes[u].val, nodes[u].l, nodes[u].r, lson, rson);
}
}
T ask_path(int u, int pos) {
if (nodes[u].l == nodes[u].r)
return nodes[u].val;
else {
int mid = (nodes[u].l + nodes[u].r) / 2;
if (pos <= mid)
return ask_path(nodes[u].lson, pos);
else
return ask_path(nodes[u].rson, pos);
}
}
public:
PersistentArray(vector<T> initData, int query_cnt = 0) {
data = move(initData);
nodes.reserve(data.size() * 4 + query_cnt * 8 * log2(data.size()));
root.push_back(build(0, data.size() - 1));
}
int revise(int v, int pos, const T val) {
int new_root = revise_path(root[v], pos, val);
root.push_back(new_root);
return root.size() - 1;
}
T ask(int v, int pos) {
return ask_path(root[v], pos);
}
void copy(int v) {
root.push_back(root[v]);
}
};
struct PersistentDSU {
PersistentArray<int> fa; // 持久化数组
PersistentArray<int> rank;
PersistentDSU(int n, int q = 0): fa(init_array(n), q), rank(vector<int>(n,0), q) {
}
int find(int ver,int x){
while (x!=fa.ask(ver,x))
x=fa.ask(ver,x);
return x;
}
void merge(int ver,int x,int y){ // 在版本ver的基础上合并x,y
x=find(ver,x),y=find(ver,y);
if (x==y){
rank.copy(ver);
fa.copy(ver);
return ;
}
int rx=rank.ask(ver,x);
int ry=rank.ask(ver,y);
if (rx==ry)
rank.revise(ver,y,ry+1);
else
rank.copy(ver);
if (rx>ry)
fa.revise(ver,y,x);
else
fa.revise(ver,x,y);
}
void copy(int ver){ // 复制一份版本ver
rank.copy(ver);
fa.copy(ver);
}
private:
static vector<int> init_array(int n){
vector<int> data(n);
iota(data.begin(),data.end(),0);
return data;
}
};
int main(){
int n,q,op,k,a,b;
cin>>n>>q;
PersistentDSU dsu(n+1, q);
for (int v=1;v<=q;v++){
cin>>op;
if (op==2){
cin>>k;
dsu.copy(k);
}
else{
cin>>a>>b;
if (op==1){
dsu.merge(v-1,a,b);
}
else if (op==3){
// debug(a,b);
a=dsu.find(v-1,a),b=dsu.find(v-1,b);
cout<<(a==b)<<endl;
dsu.copy(v-1);
}
}
}
return 0;
}

可持久化01Trie树#

acwing 256 最大异或和

要求lprl\leq p\le r,使得(i=pna[i])x(\oplus_{i=p}^n a[i])\oplus x最大。实际上,对于从高到低每一位,肯定是能1就1,不能1就随便选了,反正是0。所以朴素的想法是,我们需要从高到低,假设总共8位,就从第8位开始,看看选哪些p可以使得为1,然后看第7位,看看我们之前选的那些p中,哪些可以使得第7位为1,一直到最后只有1个p,就是答案。

这样做就是01Tried树。

算法笔记:可持久化数据结构
https://fuwari.vercel.app/posts/acm/cp-note-persistent-ds/
作者
wegret
发布于
2024-08-07
许可协议
CC BY-NC-SA 4.0