dsu on tree学习笔记
一些废话
膜拜托神@Tony102
介绍
这是一个优雅的暴力,好写。
用于解决和子树相关静态问题的好东西。
看似暴力,其实优雅。
当然和并查集的关系并不大。
一般过程
首先dfs1一遍找出重儿子,然后dfs2统计答案。
这里有两种写法,可以根据情况选择。一种是递归的,一种是直接枚举的。
个人认为枚举会快一些。
递归
inline void dfs1(int st,int fa){
sz[st]=1,f[st]=fa;
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
return;
}
inline void add_val(int st,int ban){
//这里是一个计算统计答案的地方
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]||ed==ban) continue;
add_val(ed,ban);
}
return;
}
inline void del_val(int st){
//这里是消除影响的地方
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]) continue;
del_val(ed);
}
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]||ed==son[st]) continue;
dfs2(ed),del_val(ed);
}
if(son[st]) dfs2(son[st]);
add_val(st,son[st]);
ans[st]=sum;
return;
}
枚举子树
inline void dfs1(int st,int f){
sz[st]=1,dfn[++tot]=st,L[st]=tot,fa[st]=f;
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa[st]) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
R[st]=tot;
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa[st]||ed==son[st]) continue;
dfs2(ed);
for(int j=L[ed];j<=R[ed];j++) //消除影响
}
if(son[st]) dfs2(son[st]);
//如果有需要的话这里要加上点st的影响
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa[st]||ed==son[st]) continue;
//这里统计答案
}
return;
}
具体实现还是要看题目,但是一般不会有太大变动。
一些题目
CF600E
Sol
模板题。
统计答案时判断当前颜色出现次数是否大于||等于最多次数,分别讨论即可。
Code
inline void dfs1(int st,int fa){
sz[st]=1,f[st]=fa;
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
return;
}
inline void add_val(int st,int ban){
++cnt[c[st]];
if(cnt[c[st]]>mx) mx=cnt[c[st]],sum=c[st];
else if(cnt[c[st]]==mx) sum+=c[st];//加贡献
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]||ed==ban) continue;
add_val(ed,ban);
}
return;
}
inline void del_val(int st){
--cnt[c[st]];//消除影响
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]) continue;
del_val(ed);
}
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]||ed==son[st]) continue;
dfs2(ed),del_val(ed),sum=mx=0;
}
if(son[st]) dfs2(son[st]);
add_val(st,son[st]);
ans[st]=sum;//记录答案
return;
}
CF357D
Sol
利用树状数组统计有关颜色的信息即可。
注意树状数组不可以有0,而且题目中\(k>0\),所以颜色数等于0时就不需要算进答案里。
统计的时候判断一下0。
Code
namespace BIT{
int c[N];
inline void addt(int pos,int val){
for(int i=pos;i<=n;i+=(i&(-i))) c[i]+=val;
}
inline int sumt(int pos){
int res=0;
for(int i=pos;i;i-=(i&(-i))) res+=c[i];
return res;
}
};
using BIT::addt;
using BIT::sumt;
inline void dfs1(int st,int fa){
sz[st]=1,f[st]=fa;
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
return;
}
inline void add_val(int st,int ban){
if(cnt[c[st]]>0) addt(cnt[c[st]],-1);
addt(++cnt[c[st]],1);
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]||ed==ban) continue;
add_val(ed,ban);
}
return;
}
inline void del_val(int st){
addt(cnt[c[st]],-1);
if(cnt[c[st]]>1) addt(cnt[c[st]]-1,1);
--cnt[c[st]];
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]) continue;
del_val(ed);
}
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==f[st]||ed==son[st]) continue;
dfs2(ed),del_val(ed);
}
if(son[st]) dfs2(son[st]);
add_val(st,son[st]);
for(int i=0;i<(int)qs[st].size();i++){
int id=qs[st][i].first,k=qs[st][i].second;
ans[id]=sumt(n)-sumt(k-1);
}
return;
}
CF208E
Sol
用0号结点把森林连接成一棵树。
把\(K\)级祖先转化为\(K\)级儿子,再转化成和深度有关的问题。
统计子树中深度为\(i\)的点即可。
注意:可能一个节点的\(K\)级祖先并不存在,所以在最开始要判断,否则会要统计深度为负数的点的个数。
Code
inline void dfs1(int st,int fa){
sz[st]=1,dep[st]=dep[fa]+1;
for(int i=1;i<=t;i++) f[st][i]=f[f[st][i-1]][i-1];
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
return;
}
inline void add_val(int st,int ban){
++cnt[dep[st]];//记录深度为dep的点的出现次数
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==ban) continue;
add_val(ed,ban);
}
return;
}
inline void del_val(int st){
--cnt[dep[st]];
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
del_val(ed);
}
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==son[st]) continue;
dfs2(ed),del_val(ed);
}
if(son[st]) dfs2(son[st]);
add_val(st,son[st]);
for(int i=0;i<(int)qs[st].size();i++){
int id=qs[st][i].first,k=qs[st][i].second;
ans[id]=cnt[k+dep[st]]-1;//减去原先这个点本身
}
//qs里存的是与这个点有关的询问
return;
}
CF1009F
Sol
依然是转化为和深度有关的问题。
和统计颜色一样的套路。
也可以把每个点的深度看成这个点的颜色。
Code
inline void dfs1(int st,int fa){
sz[st]=1,dep[st]=dep[fa]+1,dfn[++tot]=st,L[st]=tot;
for(int i=head[st];i;i=e[i].nx) {
int ed=e[i].ed;
if(ed==fa) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
R[st]=tot;
return;
}
inline void ADD(int st){
++cnt[dep[st]];
if(cnt[dep[st]]>mx) mx=cnt[dep[st]],DEP=dep[st];
else if(cnt[dep[st]]==mx) DEP=min(DEP,dep[st]);
return;
}
inline void dfs2(int st,int fa){
for(int i=head[st];i;i=e[i].nx){
int ed=e[i].ed;
if(ed==fa||ed==son[st]) continue;
dfs2(ed,st);
for(int j=L[ed];j<=R[ed];j++) --cnt[dep[dfn[j]]];
mx=0,DEP=0;
}
if(son[st]) dfs2(son[st],st);
for(int i=head[st];i;i=e[i].nx){
int ed=e[i].ed;
if(ed==fa||ed==son[st]) continue;
for(int j=L[ed];j<=R[ed];j++) ADD(dfn[j]);
}
ADD(st);
ans[st]=DEP-dep[st];
return;
}
CF246E
Sol
依旧是和深度有关的问题。
这次节点有了两个属性,一个是深度,一个是名字,怎么办呢?
好办,对每一个深度开一个set,名字丢到set里就行。
Code
inline void dfs1(int st,int fa){
sz[st]=1,dep[st]=dep[fa]+1;
for(int i=1;i<=t;i++) f[st][i]=f[f[st][i-1]][i-1];
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
return;
}
inline void add_val(int st,int ban){
++cnt[dep[st]];
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==ban) continue;
add_val(ed,ban);
}
return;
}
inline void del_val(int st){
--cnt[dep[st]];
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
del_val(ed);
}
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==son[st]) continue;
dfs2(ed),del_val(ed);
}
if(son[st]) dfs2(son[st]);
add_val(st,son[st]);
for(int i=0;i<(int)qs[st].size();i++){
int id=qs[st][i].first,k=qs[st][i].second;
ans[id]=cnt[k+dep[st]]-1;//cnt是一个set
}
return;
}
CF570D
Sol
异或每个深度所有节点的字母之后判断是否在二进制下最多只有1个1。
Code
inline int mpopcnt(ll x){
int res=0;
while(x) x-=(x&(-x)),++res;
return res;
}
inline void dfs1(int st){
sz[st]=1,dep[st]=dep[fa[st]]+1,dfn[++tot]=st,L[st]=tot;
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
dfs1(ed);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
R[st]=tot;
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==son[st]) continue;
dfs2(ed);
for(int j=L[ed];j<=R[ed];j++) cnt[dep[dfn[j]]]=0;
}
if(son[st]) dfs2(son[st]);
cnt[dep[st]]^=(1<<(s[st-1]-'a'));
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==son[st]) continue;
for(int j=L[ed];j<=R[ed];j++) cnt[dep[dfn[j]]]^=(1<<(s[dfn[j]-1]-'a'));
}
for(int i=0;i<(int)qs[st].size();i++){
int id=qs[st][i].first,k=qs[st][i].second;
ans[id]=(mpopcnt(cnt[k])<=1?1:0);
}
return;
}
The Grass Type
给你一棵树,求\(a_i\cdot a_j=a_{LCA(i,j)}\)的无序对\((i,j)\)的数量。
Sol
map存数的个数。
统计完儿子的答案再统计自己的答案。
如果是子树内有1,那能和根节点构成符合条件的无序对的数量为1的个数。
Code
inline void dfs1(int st,int f){
sz[st]=1,dfn[++tot]=st,L[st]=tot,fa[st]=f;
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa[st]) continue;
dfs1(ed,st);
sz[st]+=sz[ed];
if(sz[ed]>sz[son[st]]) son[st]=ed;
}
R[st]=tot;
return;
}
inline void dfs2(int st){
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa[st]||ed==son[st]) continue;
dfs2(ed);
mp.clear();
}
if(son[st]) dfs2(son[st]);
for(int i=0;i<(int)e[st].size();i++){
int ed=e[st][i];
if(ed==fa[st]||ed==son[st]) continue;
for(int j=L[ed];j<=R[ed];j++)
if(w[st]%w[dfn[j]]==0) ans+=mp[w[st]/w[dfn[j]]];
for(int j=L[ed];j<=R[ed];j++) ++mp[w[dfn[j]]];
}
ans+=mp[1];
++mp[w[st]];
return;
}
坑
小trick
- 询问一般用vector离线存在临时根节点处
- 熟练使用set,map等STL容器能让你更快的解题
- 枚举一般比递归快而且省事
- 有什么想起来了再加