dsu on tree:关于一类无修改询问子树可合并问题
开始学长讲课的时候听懂了但是后来忘掉了....最近又重新学了一遍
所谓\(dsu\ on\ tree\)就是处理本文标题:无修改询问子树可合并问题。
\(dsu\)是并查集,\(dsu\ on\ tree\)是树上启发式合并,基于树剖(轻重链剖分)。
无修改好理解,询问子树也好理解,啥是可合并啊?
举个简单的例子,集合的\(gcd\)就是可以合并的,就是两个集合\(gcd\)的\(gcd\);桶也是能合并的,对应位置相加就好了,诸如此类。
怎么处理呢?
用一个例题来讲吧(网上几乎所有博客都是这两个例题,我也就不例外了
CF 600 E.Lomsat gelral
我们考虑这样的一个暴力:对于第\(i\)棵子树,暴力扫整棵子树然后更新答案,时间复杂度是\(O(\sum size)\),\(n^2\)的。
显然,很多点都被重复扫了很多遍,可不可以优化这个过程?
我们把整棵树轻重链剖分一下。
假设我们每次\(dfs?\)这棵子树后就会把当前点的答案算出来。
暴力的时候我们就是维护了一个桶,现在我们要:承接重儿子的桶然后暴力扫轻儿子。
每个点肯定都属于一条重链,如果不是叶子就一定有重儿子,重儿子更新答案后的桶我们不删,留着。
然后暴力扫这个点的所有轻儿子来得到这棵子树的桶,之后把轻儿子的桶都删掉。
这样的话显然是对的,时间复杂度怎么保证?
考虑:除了正常的递归之外,每个点会被当做某个点的轻儿子的子孙暴力扫多少遍?
不难发现,只有在这个点的祖先中,存在一对父子关系使得儿子是父亲的轻儿子这个点才会被暴力扫一次。
而一旦出现了这种关系就表示那对父子关系中的儿子是一条重链的链头。
所以一个点的被更新次数就是这个点到根的重链链头数(减一,因为除了根节点
最多只有\(log\)对这样的关系,为什么?
重链剖分其实是保证了这个事情的。
因为每次存在这种关系那么这个轻儿子的\(size\)至多是其父亲\(size\)的一半。
假设这个点到根的链上存在\(num\)对这样的关系,每存在这样一对关系就表示其父亲对\(size\)至少\(\times 2\)。
所以整棵树的\(size\)至少是\(2^{num}\),显然\(num\)最多就\(log\)。
故此总时间复杂度是\(O(nlogn)\)。
代码:
#include <bits/stdc++.h>
#define N 1000010
using namespace std;
typedef long long ll;
int to[N<<1],nxt[N<<1],head[N],tot;
int col[N],Stack[N],son[N],size[N];
int mx=0; ll sum=0,ans[N];
char *p1,*p2,buf[100000];
#define nc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,100000,stdin),p1==p2)?EOF:*p1++)
int rd() {int x=0,f=1; char c=nc(); while(c<48) {if(c=='-') f=-1; c=nc();} while(c>47) x=(((x<<2)+x)<<1)+(c^48),c=nc(); return x*f;}
inline void add(int x,int y) {to[++tot]=y; nxt[tot]=head[x]; head[x]=tot;}
// 求出重儿子。
void dfs1(int p,int fa)
{
size[p]=1;
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa)
{
dfs1(to[i],p);
size[p]+=size[to[i]];
if(size[to[i]]>size[son[p]]) son[p]=to[i];
}
}
void add(int p,int fa,int val)
{
// 暴力遍历以p为根的子树,直接统计出答案。
Stack[col[p]]+=val;
if(Stack[col[p]]>=mx)
{
if(Stack[col[p]]>mx) mx=Stack[col[p]],sum=0;
sum+=col[p];
}
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa) add(to[i],p,val);
}
void dfs2(int p,int fa,int opt)
{
// 因为重儿子的信息不删除,所以可能对处理轻儿子的答案时有影响,故此我们先处理轻儿子再处理重儿子。
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p]) dfs2(to[i],p,0);
if(son[p]) dfs2(son[p],p,1);
// add函数是暴力遍历一颗子树。我们就暴力的把轻儿子整体的信息扫出来。
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p]) add(to[i],p,1);
// 把当前点的信息加上,然后统计当前点答案。
Stack[col[p]]++;
if(Stack[col[p]]>=mx)
{
if(Stack[col[p]]>mx) mx=Stack[col[p]],sum=0;
sum+=col[p];
}
// 记录答案。
ans[p]=sum;
// 如果当前点是轻儿子那么我们需要把当前子树全部清空。
if(!opt) add(p,fa,-1),sum=mx=0;
}
int main()
{
int n=rd();
for(int i=1;i<=n;i++) col[i]=rd();
for(int i=1;i<n;i++) {int x=rd(),y=rd(); add(x,y),add(y,x);}
dfs1(1,1); dfs2(1,1,1);
for(int i=1;i<=n;i++) printf("%lld ",ans[i]); puts("");
return 0;
}
通过这个题我们能大致看到这个算法的精髓。
就是当那些信息可以合并的时候,
我就只保留重儿子留下来的,反正可以通过合并得到所以轻儿子的暴力处理再合并就好了呗。
接下来我们看看被讲烂的第二题:
CF 741 D.Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
这个题要求的东西还是挺奇葩的。
其实就是求一个简单路径,使得这上面只有一种字符出现了奇数次。
发现这个字符只有\(a\)到\(v\),一共就\(22\)个....这提示就有点大了,我们状压一下。
如果第\(i\)为\(1\)就表示这个字符出现了奇数次,反之出现了偶数次
设\(val[x]\)表示点\(x\)到根的路径的字符状态。
我们发现一个很重要的事情:就是如果两个点\(x\)和\(y\)满足:\(val[x]\oplus val[y]\)只有一位是\(1\),那么他们俩之间的简单路径中也满足只有一位是\(1\)。
因为被算重的部分是\(lca(x,y)?\)到根的路径,被算了两边所以不影响答案。
故此我们就维护\(st[S]\)表示状态为\(S\)的点到根路径的最大深度。
毋庸置疑这鬼东西显然能合并啊,暴力就好了嘛
更新答案的话,不仅要统计经过当前点的,还要和儿子们的答案取个\(max\)。
这里要注意一下,因为是这个合并是个\(max\)所以轻儿子删除的时候需要赋值成极小值。
所以我们\(dfs?\)时候的遍历循序不能一个一个合并,应该是先轻儿子再重儿子。这样重儿子往上传输的答案不会在轻儿子删除的时候覆盖掉。
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define N 500010
using namespace std;
int to[N<<1],head[N],nxt[N<<1],tot;
int st[1<<23],val[N],son[N],dep[N],ans[N],mdlans,size[N];
inline void add(int x,int y) {to[++tot]=y; nxt[tot]=head[x]; head[x]=tot;}
void dfs1(int p,int fa)
{
dep[p]=dep[fa]+1; size[p]=1; val[p]^=val[fa];
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa)
{
dfs1(to[i],p);
size[p]+=size[to[i]];
if(size[to[i]]>size[son[p]]) son[p]=to[i];
}
}
inline void fix(int p) {st[val[p]]=max(st[val[p]],dep[p]);}
void update(int p,int fa,int opt)
{
if(opt) fix(p);
else st[val[p]]=-inf;
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa) update(to[i],p,opt);
}
inline void pushup(int p)
{
mdlans=max(mdlans,st[val[p]]+dep[p]);
for(int i=0;i<23;i++) mdlans=max(mdlans,st[val[p]^(1<<i)]+dep[p]);
}
void get_ans(int p,int fa)
{
pushup(p);
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa) get_ans(to[i],p);
}
void dfs2(int p,int fa,int opt)
{
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p]) dfs2(to[i],p,0);
if(son[p]) dfs2(son[p],p,1),ans[p]=ans[son[p]];
for(int i=head[p];i;i=nxt[i]) if(to[i]!=fa&&to[i]!=son[p])
{
get_ans(to[i],p);
ans[p]=max(ans[p],ans[to[i]]);
update(to[i],p,1);
}
pushup(p);
fix(p);
ans[p]=max(ans[p],mdlans-(dep[p]*2)),mdlans=0;
if(!opt) update(p,fa,0);
}
int main()
{
char s[10]; int x;
int n; cin >> n ;
for(int i=2;i<=n;i++) scanf("%d%s",&x,s+1),val[i]=1<<(s[1]-'a'),add(x,i),add(i,x);
memset(st,0xef,sizeof st);
// update(1,1,0);
dfs1(1,1); dfs2(1,1,1);
for(int i=1;i<=n;i++) printf("%d ",ans[i]); puts("");
return 0;
}
好,这两道例题讲完了。
相信各位对\(dsu\ on\ tree?\)有了一个大致的理解。
这东西有时候可以替代点分治。因为好写啊...
它的使用条件有些苛刻,但是一旦满足了这些条件还是比较水的。
而有些问题并不是直接告诉你询问子树,需要进行一些奇奇怪怪的转化,然后才能用这东西解决。
附上两道\(bz?\)的练习题吧:
加油哦~
原文地址:https://www.cnblogs.com/ShuraK/p/10731810.html