树上启发式合并
superkkkeee · · 算法·理论
树上启发式合并
这次更新了一些例题
题单
问题导入
CF600E
题意
给点一个
定义 重要地位 为:以
有
思路
如果直接暴力会
所以我们要考虑一种新方法:
树上启发式合并
树上启发式合并一般是解决一类不带修的子树查询问题
其本质为利用重链剖分的性质优化子树贡献的计算
我们会先跑轻儿子,然后把贡献累加到重儿子身上,在通过重儿子,具体操作如下:
-
若该点为轻儿子,则算该轻儿子的
ans ,然后清空该儿子cnt 的贡献 -
若该点为重儿子,则算该重儿子的
ans ,但是不用清空cnt 贡献 -
再枚举轻儿子,加入轻儿子的贡献就得到了
x 的答案
Q:为什么不会
A:应为轻儿子的
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
vector<int> p[100001];
int siz[100001];
int son[100001];
int cnt[100001];
int col[100001];
int ans[100001];
int maxx;
int sum;
void df(int x,int fa) //求出重儿子
{
siz[x]=1;
for(int xx:p[x])
{
if(xx==fa) continue;
df(xx,x);
siz[x]+=siz[xx];
if(siz[xx]>siz[son[x]]||son[x]==0) son[x]=xx;
}
}
void add(int x,int fa,int son) //加入贡献并统计答案
{
cnt[col[x]]++;
if(cnt[col[x]]>maxx)
{
maxx=cnt[col[x]];
sum=col[x];
}
else if(cnt[col[x]]==maxx)
{
sum+=col[x];
}
for(int xx:p[x])
{
if(xx==fa||xx==son) continue;
add(xx,x,son);
}
}
void del(int x,int fa) //删除轻儿子的贡献
{
cnt[col[x]]--;
for(int xx:p[x])
{
if(xx==fa) continue;
del(xx,x);
}
}
void ds(int x,int fa,bool flag)
{
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
ds(xx,x,0);
} //先枚举轻儿子并统计答案
if(son[x]) ds(son[x],x,1); //再枚举重儿子
add(x,fa,son[x]); //统计答案
ans[x]=sum;
if(!flag) //删除轻儿子的贡献
{
del(x,fa);
sum=0;
maxx=0;
}
}
对于每一个询问,我们直接输出
最终代码如下:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n;
vector<int> p[100001];
int siz[100001];
int son[100001];
int cnt[100001];
int col[100001];
int ans[100001];
int maxx;
int sum;
void df(int x,int fa)
{
siz[x]=1;
for(int xx:p[x])
{
if(xx==fa) continue;
df(xx,x);
siz[x]+=siz[xx];
if(siz[xx]>siz[son[x]]||son[x]==0) son[x]=xx;
}
}
void add(int x,int fa,int son)
{
cnt[col[x]]++;
if(cnt[col[x]]>maxx)
{
maxx=cnt[col[x]];
sum=col[x];
}
else if(cnt[col[x]]==maxx)
{
sum+=col[x];
}
for(int xx:p[x])
{
if(xx==fa||xx==son) continue;
add(xx,x,son);
}
}
void del(int x,int fa)
{
cnt[col[x]]--;
for(int xx:p[x])
{
if(xx==fa) continue;
del(xx,x);
}
}
void ds(int x,int fa,bool flag)
{
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
ds(xx,x,0);
}
if(son[x]) ds(son[x],x,1);
add(x,fa,son[x]);
ans[x]=sum;
if(!flag)
{
del(x,fa);
sum=0;
maxx=0;
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>col[i];
for(int i=1;i<n;i++)
{
int u,v; cin>>u>>v;
p[u].push_back(v);
p[v].push_back(u);
}
df(1,0);
ds(1,0,0);
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
return 0;
}
习题1
U41492
题意
给点一个
思路
与第一题相同,可以用树上启发式合并,统计答案就修改即可
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
vector<int> p[100001];
int siz[100001];
int son[100001];
int cnt[100001];
int col[100001];
int ans[100001];
int maxx;
int sum;
void df(int x,int fa)
{
siz[x]=1;
for(int xx:p[x])
{
if(xx==fa) continue;
df(xx,x);
siz[x]+=siz[xx];
if(siz[xx]>siz[son[x]]||son[x]==0) son[x]=xx;
}
}
void add(int x,int fa,int son)
{
cnt[col[x]]++;
if(cnt[col[x]]==1) sum++;
for(int xx:p[x])
{
if(xx==fa||xx==son) continue;
add(xx,x,son);
}
}
void del(int x,int fa)
{
cnt[col[x]]--;
for(int xx:p[x])
{
if(xx==fa) continue;
del(xx,x);
}
}
void ds(int x,int fa,bool flag)
{
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
ds(xx,x,0);
}
if(son[x]) ds(son[x],x,1);
add(x,fa,son[x]);
ans[x]=sum;
if(!flag)
{
del(x,fa);
sum=0;
maxx=0;
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<n;i++)
{
int u,v; cin>>u>>v;
p[u].push_back(v);
p[v].push_back(u);
}
for(int i=1;i<=n;i++) cin>>col[i];
df(1,0);
ds(1,0,0);
cin>>m;
while(m--)
{
int x; cin>>x;
cout<<ans[x]<<'\n';
}
return 0;
}
习题2
CF1709E
闲话
感觉这题应该评紫
题面
给定一颗有
每次操作可以修改一个点权,求最少多少次修改可以使不存在任意一条路径上点权的异或和为
思路
我们可以先对这棵树做一个前缀和
:::align{center} :::
如果有一条非法路径(
用
若我们向修改点权使得这条式子不成立,我们自然就把
改完以后,我们就可以把
对于维护,我们可以使用树上启发式合并,
对于每个点,我们都单开一个
对于一个节点
-
如果没有发现非法路径,则将儿子
y 的set_y 合并到set_x (启发式合并) -
若发现了,则将
ans + 1 ,并且将set_x 清空
寻找非法路径也很简单:
根据上面的柿子
我们只要在枚举的
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n;
int a[200001];
int sum[200001];
vector<int> p[200001];
set<int> s[200001];
int ans;
void dfs(int x,int fa)
{
s[x].insert(sum[x]);
bool flag=0;
for(int xx:p[x])
{
if(xx==fa) continue;
sum[xx]=sum[x]^a[xx];
dfs(xx,x);
if(s[x].size()<s[xx].size()) swap(s[x],s[xx]);
for(int xxx:s[xx])
{
if(s[x].find(xxx^a[x])!=s[x].end())
{
flag=1; break;
}
}
for(int xxx:s[xx]) s[x].insert(xxx);
}
if(flag)
{
ans++; set<int>().swap(s[x]);
}
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<n;i++)
{
int u,v; cin>>u>>v;
p[u].push_back(v);
p[v].push_back(u);
}
dfs(1,0);
cout<<ans;
return 0;
}
加上
习题3
CF208E
题面
有一颗以
有
思路
暴力显然是不现实的,我们需要做一下转化
首先,我们可以通过倍增求出
转化:本题可以转化为
我们自然要对询问做改变:
我们可以把
我们不妨设
显然我们更新时要用到 树上启发式合并
我们来复习一下其过程
-
先枚举轻儿子,统计答案,但清空
cnt 的贡献 -
枚举重儿子,统计答案,
cnt 直接继承到当前节点 -
枚举轻儿子,统计
cnt
添加和删除可以写一个函数,引入一个参数
当我们跑到一个挂有查询的节点是,当前节点的答案就为
因为离线,所以最后结果输出
Q:如何求
A:现将
时间复杂度应为
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int> p[100001];
int dep[100001];
int son[100001];
int siz[100001];
int bz[100001][21];
int cnt[100001];
int ans[100001];
struct node
{
int x,id;
};
vector<node> pp[100001];
void df(int x,int fa)
{
dep[x]=dep[fa]+1;
siz[x]=1;
bz[x][0]=fa;
for(int i=1;i<=20;i++) bz[x][i]=bz[bz[x][i-1]][i-1];
for(int xx:p[x])
{
if(xx==fa) continue;
df(xx,x);
siz[x]+=siz[xx];
if(siz[son[x]]<siz[xx]||son[x]==-1) son[x]=xx;
}
}
void add(int x,int fa,int k)
{
cnt[dep[x]]+=k;
for(int xx:p[x])
{
if(xx==fa) continue;
add(xx,x,k);
}
}
void ds(int x,int fa,bool flag)
{
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
ds(xx,x,1);
}
if(son[x]!=-1) ds(son[x],x,0);
cnt[dep[x]]++;
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
add(xx,x,1);
}
for(node xx:pp[x]) ans[xx.id]=cnt[dep[x]+xx.x]-1;
if(flag) add(x,fa,-1);
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
int x; cin>>x;
p[x].push_back(i);
son[i]=-1;
}
son[0]=-1;
df(0,0);
cin>>m;
for(int q=1;q<=m;q++)
{
int x,k; cin>>x>>k;
int fa=x;
for(int i=0;(1<<i)<=k;i++)
{
if(((1<<i)&k)==(1<<i)) fa=bz[fa][i];
}
if(fa==0) ans[q]=0;
else pp[fa].push_back({k,q});
}
ds(0,0,0);
for(int i=1;i<=m;i++) cout<<ans[i]<<" ";
return 0;
}
习题4
CF570D
题面
有一颗
有
那么我们的问题就转化为求
因为询问是按照深度分层,所以我们的
那我们的问题就转化为静态无修改树求
步骤和习题3
差不多,add 的操作也一样
那我们就展示代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int> p[500001];
int a[500001];
int cnt[500001][31];
int dep[500001];
int siz[500001];
int son[500001];
int ans[500001];
struct node
{
int x,id;
};
vector<node> pp[500001];
void df(int x,int fa)
{
dep[x]=dep[fa]+1;
siz[x]=0;
for(int xx:p[x])
{
if(xx==fa) continue;
df(xx,x);
siz[x]+=siz[xx];
if(siz[son[x]]<siz[xx]||son[x]==0) son[x]=xx;
}
}
void add(int x,int fa,int k)
{
cnt[dep[x]][a[x]]+=k;
for(int xx:p[x])
{
if(xx==fa) continue;
add(xx,x,k);
}
}
void ds(int x,int fa,bool flag)
{
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
ds(xx,x,1);
}
if(son[x]) ds(son[x],x,0);
cnt[dep[x]][a[x]]++;
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
add(xx,x,1);
}
for(node xx:pp[x])
{
int f=xx.x;
int id=xx.id;
bool q=1;
int c=0;
for(int i=0;i<26;i++)
{
//cout<<"Qqqqqqqq "<<f<<" "<<i<<" "<<cnt[f][i]<<endl;
if(cnt[f][i]%2==1) c++;
if(c>1)
{
q=0; break;
}
}
ans[id]=q;
}
if(flag) add(x,fa,-1);
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=2;i<=n;i++)
{
int x; cin>>x;
p[x].push_back(i);
}
for(int i=1;i<=n;i++)
{
char x; cin>>x;
a[i]=x-'a';
}
df(1,0);
for(int i=1;i<=m;i++)
{
int x,y; cin>>x>>y;
if(dep[x]>y)
{
ans[i]=1; continue;
}
if(dep[x]==y)
{
ans[i]=1; continue;
}
pp[x].push_back({y,i});
}
ds(1,0,0);
for(int i=1;i<=m;i++)
{
if(ans[i]) cout<<"Yes";
else cout<<"No";
cout<<'\n';
}
return 0;
}
/*
6 1
1 1 1 3 3
zacccd
3 3
5 1
1 1 2 3
cbcab
1 3
*/
习题5
CF375D
题面
有一颗
每次询问求以
颜色
思路
注意到
设最大颜色为
这个
统计答案就是求
但每一次查询时间复杂度是
我们可以使用线段树来维护?
但每一次修改时
(下面是正解)
那我们可以重新定义
修改
维护
答案
树上启发式合并的复杂的是
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
int c[100001];
vector<int> p[100001];
int siz[100001];
int son[100001];
struct node
{
int x,id;
};
vector<node> pp[100001];
int cnt[100001];
int cntt[100001];
int ans[100001];
void df(int x,int fa)
{
siz[x]=1;
for(int xx:p[x])
{
if(xx==fa) continue;
df(xx,x);
siz[x]+=siz[xx];
if(siz[son[x]]<siz[xx]||son[x]==0) son[x]=xx;
}
}
void add(int x,int k)
{
if(k==1)
{
cnt[c[x]]++;
cntt[cnt[c[x]]]++;
}
else
{
cntt[cnt[c[x]]]--;
cnt[c[x]]--;
}
}
void addd(int x,int fa,int k)
{
add(x,k);
for(int xx:p[x])
{
if(xx==fa) continue;
addd(xx,x,k);
}
}
void ds(int x,int fa,bool flag)
{
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
ds(xx,x,1);
}
if(son[x]) ds(son[x],x,0);
add(x,1);
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
addd(xx,x,1);
}
for(node xx:pp[x])
{
int k=xx.x;
int id=xx.id;
ans[id]=cntt[k];
}
if(flag) addd(x,fa,-1);
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1;i<n;i++)
{
int u,v; cin>>u>>v;
p[u].push_back(v);
p[v].push_back(u);
}
for(int i=1;i<=m;i++)
{
int x,k; cin>>x>>k;
pp[x].push_back({k,i});
}
df(1,0);
ds(1,0,0);
for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
return 0;
}
习题6
CF246E
题面
给点一颗
每次询问求
思路
这道题不难想到树上启发式合并,但如何维护贡献是个问题
如果把
我们可以换一个思路,如果我们把出现的名字都加入一个数组里,然后去重,剩下的数量就是答案了
想到这里,我们可以用 set 来维护
设 set,
我们把询问挂在节点上,每次询问的答案就是 size()
注意:
那么代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int> p[200001];
int dep[200001];
int siz[200001];
int son[200001];
string c[200001];
set<string> s[200001];
struct node
{
int x,id;
};
vector<node> pp[200001];
int ans[200001];
void df(int x,int fa)
{
siz[x]=1;
dep[x]=dep[fa]+1;
for(int xx:p[x])
{
if(xx==fa) continue;
df(xx,x);
siz[x]+=siz[xx];
if(siz[son[x]]<siz[xx]||son[x]==-1) son[x]=xx;
}
}
void add(int x,int fa)
{
s[dep[x]].insert(c[x]);
for(int xx:p[x])
{
if(xx==fa) continue;
add(xx,x);
}
}
void del(int x,int fa)
{
//set<string>().swap(s[dep[x]]);
s[dep[x]].clear();
for(int xx:p[x])
{
if(xx==fa) continue;
del(xx,x);
}
}
void ds(int x,int fa,bool flag)
{
for(int xx:p[x])
{
if(xx==fa||xx==son[x]) continue;
ds(xx,x,1);
}
if(son[x]!=-1) ds(son[x],x,0);
s[dep[x]].insert(c[x]);
for(int xx:p[x])
{
if(x==fa||xx==son[x]) continue;
add(xx,x);
}
for(node xx:pp[x])
{
int k=xx.x;
int id=xx.id;
//if(dep[x]+k>n+1) continue;
ans[id]=s[dep[x]+k].size();
}
if(flag) del(x,fa);
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
int x; cin>>c[i]>>x;
p[x].push_back(i);
}
for(int i=0;i<=n;i++) son[i]=-1;
df(0,0);
cin>>m;
for(int i=1;i<=m;i++)
{
int x,k; cin>>x>>k;
pp[x].push_back({k,i});
}
ds(0,0,0);
for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
return 0;
}
习题7
CF741D
本题较精彩,请耐心观看
题面
有一颗
定义“好”的路径为:路径上的字母通过排列可以组成回文数
求以
思路
根据回文的性质,回文数最多只会有一个字母出现奇数次
那么一个数是否是回文数就只关心字母出现次数的奇偶
我们对于每一颗子树都开一个 cnt 数组记录每一个字母出现次数,但时间复杂度不能接受
注意到奇偶数的性质:奇数+奇数=偶数+偶数=偶数,奇数+偶数=奇数
这个性质是不是似曾相识呢?
没错,就是异或!
我们设偶数为
注意到字母范围很小,那我们对于每一颗子树做一个类似状压做法
如果要快速求路径
我们可以先定义一个数组
那么
如果
那么对于节点
那我们找合法的
对于合法路径
那我们求最大贡献肯定要是
那我们就可以记录奇偶性为
设
那
-
若以
x 为端点,则贡献为cnt_{dis_x}-dep_x -
若路径经过
x ,则加上递归取max -
若路径不经过
x ,即路径在x 的某个子树内,则贡献就为子树的最大贡献
那
第
void add(int x,int fa,int k)
{
if(cnt[dis[x]])
{
ans[k]=max(ans[k],cnt[dis[x]]+dep[x]-2*dep[k]);
}
for(int i=0;i<=21;i++)
{
if(cnt[dis[x]^(1<<i)])
{
ans[k]=max(ans[k],cnt[dis[x]^(1<<i)]+dep[x]-2*dep[k]);
}
}
for(node xx:p[x])
{
int v=xx.v;
if(v==fa) continue;
add(v,x,k);
}
}
若直接统计
那统计
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
struct node
{
int v,w;
};
vector<node> p[2000001];
int dis[2000001];
int siz[2000001];
int dep[2000001];
int son[2000001];
int cnt[20000001];
int ans[2000001];
void df(int x,int fa)
{
siz[x]=1;
dep[x]=dep[fa]+1;
for(node xx:p[x])
{
int v=xx.v;
int w=xx.w;
if(v==fa) continue;
dis[v]=dis[x]^w;
df(v,x);
siz[x]+=siz[v];
if(siz[son[x]]<siz[v]||son[x]==0) son[x]=v;
}
}
void add(int x,int fa,int k)
{
if(cnt[dis[x]])
{
ans[k]=max(ans[k],cnt[dis[x]]+dep[x]-2*dep[k]);
}
for(int i=0;i<=21;i++)
{
if(cnt[dis[x]^(1<<i)])
{
ans[k]=max(ans[k],cnt[dis[x]^(1<<i)]+dep[x]-2*dep[k]);
}
}
for(node xx:p[x])
{
int v=xx.v;
if(v==fa) continue;
add(v,x,k);
}
}
void del(int x,int fa)
{
cnt[dis[x]]=0;
for(node xx:p[x])
{
int v=xx.v;
if(v==fa) continue;
del(v,x);
}
}
void addd(int x,int fa)
{
cnt[dis[x]]=max(cnt[dis[x]],dep[x]);
for(node xx:p[x])
{
int v=xx.v;
if(v==fa) continue;
addd(v,x);
}
}
void ds(int x,int fa,bool flag)
{
for(node xx:p[x])
{
int v=xx.v;
if(v==fa||v==son[x]) continue;
ds(v,x,1);
ans[x]=max(ans[x],ans[v]);
}
if(son[x])
{
ds(son[x],x,0);
ans[x]=max(ans[x],ans[son[x]]);
}
if(cnt[dis[x]]) ans[x]=max(ans[x],cnt[dis[x]]-dep[x]);
for(int i=0;i<=21;i++)
{
if(cnt[dis[x]^(1<<i)])
{
ans[x]=max(ans[x],cnt[dis[x]^(1<<i)]-dep[x]);
}
}
cnt[dis[x]]=max(cnt[dis[x]],dep[x]);
for(node xx:p[x])
{
int v=xx.v;
if(v==fa||v==son[x]) continue;
add(v,x,x);
addd(v,x);
}
if(flag) del(x,fa);
}
signed main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>n;
for(int i=2;i<=n;i++)
{
int x;
char ox; cin>>x>>ox;
int w=ox-'a';
p[x].push_back({i,(1ll<<w)});
}
df(1,0);
ds(1,0,0);
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
return 0;
}
总结
本题的转化十分巧妙,将字母出现的奇偶性转化为二进制,并且使用异或来合并
在统计贡献是也要想全面,不能漏情况
总的来说这题是一道十分巧妙的好题,适合读者思考研究