题目:
题意:
一棵树,每一个点有一个颜色,统计以每一个节点为根的子树中出现次数最多的颜色的编号和。
题解:
基本就是dsu的裸题啦,下面讲解见咯
代码:
#include <cstdio>
#include <iostream>
#include <cstring>
#define LL long long
#define N 100005
using namespace std;
int tot,nxt[N*2],point[N],v[N*2],size[N],son[N],cnt[N],a[N],maxx,Son;
LL sum,ans[N];
void addline(int x,int y)
{
++tot; nxt[tot]=point[x]; point[x]=tot; v[tot]=y;
++tot; nxt[tot]=point[y]; point[y]=tot; v[tot]=x;
}
void getson(int x,int fa)
{
size[x]=1;
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa)
{
getson(v[i],x);
if (size[v[i]]>size[son[x]]) son[x]=v[i];
size[x]+=size[v[i]];
}
}
void add(int x,int fa,int vv)
{
cnt[a[x]]+=vv;
if (cnt[a[x]]>maxx) sum=(LL)a[x],maxx=cnt[a[x]];
else if (cnt[a[x]]==maxx) sum+=(LL)a[x];
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa && v[i]!=Son) add(v[i],x,vv);
}
void dfs(int x,int fa,int k)
{
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa && v[i]!=son[x]) dfs(v[i],x,0);
if (son[x])//不是叶子,有重儿子
dfs(son[x],x,1),Son=son[x];
add(x,fa,1);//是叶子||轻儿子就加入贡献,并不会删除贡献
Son=0;
ans[x]=sum;
if (!k) add(x,fa,-1),maxx=sum=0;//轻儿子
}
int main()
{
int n,i;
scanf("%d",&n);
for (i=1;i<=n;i++) scanf("%d",&a[i]);
for (i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
addline(x,y);
}
getson(1,0);
dfs(1,0,0);
for (i=1;i<=n;i++) printf("%lld ",ans[i]);
}
普及向:
从优秀的学长学来的新姿势
什么是dsu on tree
dsu on tree用来解决这样一类问题:统计树上一个节点的子树中具有某种特征的节点数。
例如子树中颜色为x的个数。
这种方法可以做到O(nlogn)的复杂度。
那么dsu到底是个什么玩意呢?其实它的中文译名就是众所周知的并查集…
有的小朋友就会问了,并查集怎么跑到树上去的呢?
恩……其实说白了就是启发式合并:在做一类维护问题的时候,将size较小的合并到较大的size上,从而达到降低时间复杂度的目的。
又是并查集的按秩合并思想应用
一个例子
以上面的问题举个例子。
暴力
void add(int x,int fa,int vv)
{
cnt[a[x]]+=vv;
if (cnt[a[x]]>maxx) sum=(LL)a[x],maxx=cnt[a[x]];
else if (cnt[a[x]]==maxx) sum+=(LL)a[x];
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa) add(v[i],x,vv);
}
void dfs(int x,int fa,int k)
{
sum=0,maxx=0;
add(x,fa,1);
ans[x]=sum;
add(x,fa,-1);
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa) dfs(v[i],x,0);
}
代码简短也会T
在这种做法中,每次统计x节点前,暴力将x的子树的贡献加入,统计结束后,再暴力删除贡献,消除影响。
时间复杂度O(n^2)
但是这样有很多无用的删除操作,能不能减少这种操作呢。
树链剖分
void getson(int x,int fa)
{
size[x]=1;
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa)
{
getson(v[i],x);
if (size[v[i]]>size[son[x]]) son[x]=v[i];
size[x]+=size[v[i]];
}
}
void add(int x,int fa,int vv)
{
cnt[a[x]]+=vv;
if (cnt[a[x]]>maxx) sum=(LL)a[x],maxx=cnt[a[x]];
else if (cnt[a[x]]==maxx) sum+=(LL)a[x];
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa && v[i]!=Son) add(v[i],x,vv);
}
void dfs(int x,int fa,int k)
{
for (int i=point[x];i;i=nxt[i])
if (v[i]!=fa && v[i]!=son[x]) dfs(v[i],x,0);
if (son[x])//不是叶子
dfs(son[x],x,1),Son=son[x];
add(x,fa,1);Son=0;
ans[x]=sum;
if (!k) add(x,fa,-1),maxx=sum=0;//轻儿子
}
在这种做法中,我们先进行树链剖分。
dfs的时候,首先dfs节点x的轻儿子,暴力消去影响,再dfs节点x的下一个轻儿子,一次类推。
然后dfs节点x的重儿子,无需消去影响。
在最后,我们为了统计x,再将x轻儿子的贡献加回来。
看起来很暴力,但是实际上它的时间复杂度是O(nlogn)的,跑得飞快。
证明:只有dfs到轻边时,才会将轻边的子树中合并到上一级的重链,树链剖分将一棵树分割成了不超过logn条重链。
每一个节点最多向上合并logn次,单次修改复杂度O(1)。
所以整体复杂度是O(nlogn)的。
dfs序莫队
一个子树中的节点在dfs序中是连续的,所以可以通过dfs序,将子树问题转化为序列问题,这样就可以跑莫队了。
时间复杂度O(q√n)
dfs序主席树
还可以通过dfs序建出主席树,查询就是差分后的单点查询。
时间复杂度O((n+q)logn) 但是空间复杂度是O(nlogn)的