操作树学习笔记

First Post:

Last Update:

Word Count:
2.9k

Read Time:
14 min

引入

有些题目有这样的特点:

  1. 允许离线
  2. 需要回退历史版本

这种情况下,我们可以离线所有操作,建立操作树,每个版本对应一个节点,其父亲就是其来源的版本。对于修改操作,回溯时回退即可。

当然,如果强制在线的话还是上可持久化数据结构吧。

例题

[HBCPC2024] Enchanted

第一次见到这个 trick 是在这个题。

不考虑操作树的话,剩下的和 Mark and Professor Koro 还是很像的。

对于这种两个相同的数合并成 +1 的数的操作,我们可以把它看成二进制意义下的加法,然后进行维护。

这道题值域很小,所以可以直接把二进制用整数表示。

考虑建立操作树,操作四就解决了。

操作三是单点修改,比较简单。

操作一就是表示成二进制以后求和找最高位。

操作二就是表示成二进制以后求和,然后找第 位向上的连续位数。

单点修区间和,树状数组即可。

复杂度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include"bits/stdc++.h"
#define re register
#define int long long
using namespace std;
const int maxn=1e6+10,mod=1e9+7;
int n,m,seed,P,Q;
int a[maxn],ans[maxn];
struct Query{
int op,x,y,k;
}q[maxn];
struct BIT{
#define lb(x) (x&(-x))
int tr[maxn];
inline void add(int x,int val){while(x<=n) tr[x]+=val,x+=lb(x);}
inline int query(int x){int res=0;while(x) res+=tr[x],x-=lb(x);return res;}
#undef lb(x)
}b;
vector<int> g[maxn];
inline int rnd(){
seed=(7*seed+13)%19260817;
return seed;
}
inline void dfs(int u){
int op=q[u].op,x=q[u].x,y=q[u].y,k=q[u].k;
if(op==1){
int res=b.query(y)-b.query(x-1);
ans[u]=floor(__lg(res))+1;
}
if(op==2){
int res=b.query(y)-b.query(x-1);
res>>=(k-1);
int bs=(1ll<<(k+1));
while(res&1) ans[u]=(ans[u]+bs)%mod,bs<<=1,res>>=1;
}
if(op==3){
b.add(x,(1ll<<(q[u].y-1))-(1ll<<(a[x]-1)));
swap(a[x],q[u].y);
}
for(auto v:g[u]) dfs(v);
if(op==3){
b.add(x,(1ll<<(q[u].y-1))-(1ll<<(a[x]-1)));
swap(a[x],q[u].y);
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>seed>>P>>Q;
for(re int i=1;i<=n;++i) a[i]=rnd()%Q+1,b.add(i,1ll<<(a[i]-1));
for(re int i=1,op,x,y,k;i<=m;++i){
op=rnd()%P+1;
if(op==1){
x=rnd()%n+1,y=rnd()%n+1;
if(x>y) swap(x,y);
}
if(op==2){
x=rnd()%n+1,y=rnd()%n+1,k=rnd()%Q+1;
if(x>y) swap(x,y);
}
if(op==3) x=rnd()%n+1,y=rnd()%Q+1;
if(op==4) x=rnd()%i;
q[i]={op,x,y,k};
op==4?g[x].push_back(i):g[i-1].push_back(i);
}
dfs(0);
for(re int i=1;i<=m;++i) if(q[i].op==1||q[i].op==2) cout<<ans[i]<<'\n';
return 0;
}

「TOCO Round 1」History

考虑建立操作树,操作三就解决了。

操作一是单点修改,比较简单。

考虑操作二如何实现。

看到相同深度想到 bfs 序,那么这些点事实上就是 的 级祖先的子树内和 在相同深度的点。

不难发现这些点的 bfs 序是连续的。

然后考虑怎么得到 级祖先的子树内和 在相同深度的点。

可以发现,这些点的 dfs 序也是连续的。

所以我们把所有和 在相同深度的点拿出来,然后二分找到 dfs 序在 的点,那么操作二就变成了区间查询。

别忘了把 级祖先的子树内和 在相同深度的点扣掉,因为这些点并不合法。

复杂度

有些细节,例如 可以不合法,而且还能为 0,都需要特判。

upd:这里写的麻烦了,没必要开 vector 把所有深度的所有点存下来,可能当时写的时候还没太懂。

我们求出每个深度对应的 bfs 序区间,然后把 bfs 序和 dfs 序对应起来。

这样在查某个深度的时候,只需要在 bfs序的区间上二分即可。

具体实现可以看[Cnoi2019] 雪松果树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include"bits/stdc++.h"
#define re register
#define int long long
#define lb(x) (x&(-x))
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
const int maxn=1e5+10;
int n,m,cnt,tim;
int a[maxn];
int head[maxn],ans[maxn];
int fa[maxn][18];
int c[maxn];
vector<int> g[maxn],Dep[maxn];
struct edge{
int to,nxt;
}e[maxn<<1];
struct Query{
int op,x,y;
}q[maxn];
struct node{
int dfn,bfn,siz,dep,seq;
}t[maxn];
struct BIT{
int tr[maxn];
inline void add(int x,int val){++x;while(x<=n+1) tr[x]+=val,x+=lb(x);}
inline int query(int x){++x;int res=0;while(x) res+=tr[x],x-=lb(x);return res;}
}b;
inline void add(int u,int v){
e[++cnt]={v,head[u]};
head[u]=cnt;
}
inline void bfs(int s){
queue<pii> q;
q.push({s,1});
while(!q.empty()){
int u=q.front().fi,dep=q.front().se;
q.pop();
t[u].bfn=++tim,Dep[dep].push_back(t[u].dfn),t[u].dep=dep;
for(re int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(!t[v].bfn) q.push({v,dep+1});
}
}
}
inline void dfs1(int u,int ft){
t[u].siz=1;fa[u][0]=ft;
t[u].dfn=++tim;t[tim].seq=u;
for(re int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==ft) continue;
dfs1(v,u);
t[u].siz+=t[v].siz;
}
}
inline void init(){
for(re int j=1;j<=17;++j){
for(re int u=1;u<=n;++u){
fa[u][j]=fa[fa[u][j-1]][j-1];
}
}
}
inline int find(int u,int dep){
for(re int i=17;i>=0;--i) if(dep>=(1ll<<i)) u=fa[u][i],dep-=(1ll<<i);
return u;
}
inline void dfs(int u){
int op=q[u].op,x=q[u].x,y=q[u].y;
if(op==1) a[x]?b.add(t[x].bfn,-1):b.add(t[x].bfn,1),a[x]^=1;
if(op==2&&!(y&1)){
if(y==0) ans[u]=a[x];
else{
int fa=find(x,y/2),dep=t[x].dep;
if(fa){
int l=Dep[dep][lower_bound(Dep[dep].begin(),Dep[dep].end(),t[fa].dfn)-Dep[dep].begin()];
int r=Dep[dep][upper_bound(Dep[dep].begin(),Dep[dep].end(),t[fa].dfn+t[fa].siz-1)-Dep[dep].begin()-1];
l=t[l].seq,r=t[r].seq;
ans[u]+=(b.query(t[r].bfn)-b.query(t[l].bfn-1));
}
if(fa) fa=find(x,y/2-1);
if(fa){
int l=Dep[dep][lower_bound(Dep[dep].begin(),Dep[dep].end(),t[fa].dfn)-Dep[dep].begin()];
int r=Dep[dep][upper_bound(Dep[dep].begin(),Dep[dep].end(),t[fa].dfn+t[fa].siz-1)-Dep[dep].begin()-1];
l=t[l].seq,r=t[r].seq;
ans[u]-=(b.query(t[r].bfn)-b.query(t[l].bfn-1));
}
}
}
for(auto v:g[u]) dfs(v);
if(op==1) a[x]?b.add(t[x].bfn,-1):b.add(t[x].bfn,1),a[x]^=1;
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;
for(re int i=1,u,v;i<n;++i) cin>>u>>v,add(u,v),add(v,u);
cin>>m;
for(re int i=1;i<=m;++i){
cin>>q[i].op>>q[i].x;
if(q[i].op==2) cin>>q[i].y;
q[i].op==3?g[q[i].x].push_back(i):g[i-1].push_back(i);
}
dfs1(1,0);tim=0;bfs(1);init();
dfs(0);
for(re int i=1;i<=m;++i) if(q[i].op==2) cout<<ans[i]<<'\n';
return 0;
}

Persistent Bookcase *2200

建操作树后暴力 bitset 维护即可。

对一行取反可以先把一个全为 1 的 bitset 处理出来,然后异或这个 bitset 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include"bits/stdc++.h"
#define re register
#define int long long
using namespace std;
const int maxn=1e3+10,maxq=1e5+10;
int n,m,Q,now;
int ans[maxq];
bitset<maxn> a[maxn],A;
vector<int> g[maxq];
struct query{
int op,x,y;
}q[maxq];
inline void dfs(int u){
int op=q[u].op,x=q[u].x,y=q[u].y;
bool sta=0;
if(op==1){
sta=a[x][y];
if(!a[x][y]) ++now;
a[x][y]=1;
}
if(op==2){
sta=a[x][y];
if(a[x][y]) --now;
a[x][y]=0;
}
if(op==3){
int res=a[x].count();
a[x]^=A;
now+=a[x].count()-res;
}
ans[u]=now;
for(auto v:g[u]) dfs(v);
if(op==1){
if(!sta) --now;
a[x][y]=sta;
}
if(op==2){
if(sta) ++now;
a[x][y]=sta;
}
if(op==3){
int res=a[x].count();
a[x]^=A;
now+=a[x].count()-res;
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>Q;
for(re int i=1;i<=m;++i) A[i]=1;
for(re int i=1,op,x,y;i<=Q;++i){
cin>>op;
if(op==1||op==2) cin>>x>>y;
else cin>>x;
q[i]={op,x,y};
op==4?g[x].push_back(i):g[i-1].push_back(i);
}
dfs(0);
for(re int i=1;i<=Q;++i) cout<<ans[i]<<'\n';
return 0;
}

可持久化并查集

没有强制在线,所以考虑操作树。

维护个可撤销并查集就行了。

没写代码。

TTM - To the moon

没有强制在线,所以考虑操作树。

然后线段树维护就行了。

然而当初做这道题是为了学主席树如何区间修改(标记永久化)。

[Ynoi2014] 等这场战争结束之后

如果没有回撤,那么这是[HNOI2012] 永无乡,并查集+线段树合并即可。

那我暴力线段树合并+分裂行不行?不行,因为每次撤销都可以卡成 次分裂,那就寄了。

那我 LCT 上维护 set 行不行?不行,理由同上。

我们真的需要 LCT 维护这个东西吗?事实上不用,因为我们只关心连通性,所以可撤销并查集就足够了。

那 kth 怎么解决?线段树用不了了,那就值域分块!

我们对每个联通块维护所有整块的信息,合并时暴力合并。

查询也很简单,因为相当于查全局 kth,直接枚举每个整块,然后找到落在哪个块时就枚举这个块里的值。

枚举值的时候要判断这个值对应的点是否在联通块内,所以要给值和点对应一个关系(下面提到了)。

时间复杂度 ,空间复杂度 。

块长取 时最优。

时间复杂度 ,空间复杂度

代码倒是没有很难写,主要是卡空间卡的要死。

卡空间的一些技巧:

  1. 块长调大点
  2. 能开 short 的数组开 short
  3. 别用 vector 存图,用链式前向星
  4. 能不存的就别存,例如每个块的起始终点之类的

还有一些需要注意的点:

  1. 离散化时,相同的数不能视为一个,因为在查询时,我们查到一个整块的里面时,需要知道每个值对应的下标是否在当前连通块里,所以编号要注意。
  2. 别写 UB 或你弄不懂到底会怎么运行的代码。例如我参考的题解里写了一句 a[i]+=cnt[a[i]]++;,这东西你真能弄清吗。

因为一直卡不过去所以最后参考了题解,代码也跟题解差不多了,而且写的也很丑,所以不放代码了。

参考文章:

操作树学习笔记 - MYCui