dsu on tree(树上启发式合并)算法总结+习题

前几天补了一下在camp中没学到的算法,树上启发式合并,一个O(nlogn)处理树上查询问题比较好的算法(其实就是暴力

前置知识

学习这个算法之前需要知道一些树链剖分中的概念,不知道也没问题,下面我会给出

节点的大小:以当前节点为根的子树中的节点个数

重儿子:所有儿子节点中大小最大的那个节点

轻儿子:除重儿子之外的儿子节点

至于节点的深度等常见概念就不解释了。

算法思想

先拿一个经典的题来讲:CF600E Lomsat gelral

题意:一棵树有n个结点,每个结点都是一种颜色,每个颜色有一个编号,求树中每个子树的最多的颜色编号的和。

我们不难想到一个暴力的做法,用一个全局cnt数组记录颜色出现次数,对每个节点,遍历其所有子树,然后统计答案,最后再清空cnt数组(不清空的话会对影响到其他节点),这个时间复杂度是O(n^2),肯定过不去。

然后我们可以考虑一个优化,遍历到最后一个子树时是不用清空的,因为它不会产生对其他节点影响了,根据贪心的思想我们当然要把节点数最多的子树(即重儿子形成的子树)放在最后,之后我们就有了一个看似比较快的算法,先遍历所有的轻儿子节点形成的子树,统计答案但是不保留数据,然后遍历重儿子,统计答案并且保留数据,最后再遍历轻儿子以及父节点,合并重儿子统计过的答案。

看似这个优化不是很重要,但是时间复杂度就变成了O(nlogn)。具体的证明过程我不是很懂,就不多说了,可以参考我最后发的参考链接。

具体代码实现:

#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#define PI atan(1.0)*4
#define E 2.718281828
#define rp(i,s,t) for (i = (s); i <= (t); i++)
#define RP(i,s,t) for (i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
    {
        a=(a<<3)+(a<<1)+c-'0';
        c=getchar();
    }
    return a*b;
}
const int N = 1e5+7;
int cnt[N],color[N];
//cnt数组记录颜色的出现次数,方便记录最多出现次数的颜色编号和
//color数组用来记录每一个点的颜色
int hSon[N],sz[N],visited[N];
//hSon数组用来记录每一个节点的重儿子节点编号
//sz数组存储每一个节点的大小,即子树所包含的节点个数
//visited数组标记节点是否被访问
vector<int> G[N];//用来存储树,也可以用链式向前星来存储
ll ans[N];//记录每一个节点的答案,
ll maxCnt,sum;//存储最大出现次数以及每一次记录的答案
void dfs1(int u,int f){//这一个dfs用来得到每一个节点的重儿子
    sz[u]=1;
    for(auto v:G[u]){
        if(v==f) continue;
        dfs1(v,u);
        sz[u]+=sz[v];
        if(sz[hSon[u]]<sz[v]) hSon[u]=v;//更新当前节点的重儿子信息
    }
}
void add(int u,int f,int k){//这里k如果为1,表示记录当前节点的贡献,为-1用来清空cnt数组
    cnt[color[u]]+=k;
    if(k>0&&cnt[color[u]]>=maxCnt){//更新最大出现次数,并记录答案
        if(cnt[color[u]]>maxCnt) sum=0,maxCnt=cnt[color[u]];
        sum+=color[u]; 
    }
    for(auto v:G[u])
        if(v!=f&&!visited[v]) 
            add(v,u,k);//递归计算子节点
}
void dfs(int u,int f,int keep){//这里keep为1表示当前节点在重儿子的子树中,需要保留答案
    for(auto v:G[u])
        if(v!=f&&v!=hSon[u]) //先遍历轻儿子,但是需要清空cnt数组和答案
            dfs(v,u,0);
    if(hSon[u]) dfs(hSon[u],u,1),visited[hSon[u]]=1;//再遍历重儿子,需要保留答案并且标记
    add(u,f,1);//计算总答案
    ans[u]=sum;
    if(hSon[u]) visited[hSon[u]]=0;//清除标记
    if(!keep) add(u,f,-1),sum=maxCnt=0;//清空cnt数组以及答案
}
int main(){
	int n=read(),i;
    rp(i,1,n) color[i]=read();
    rp(i,1,n-1){
        int x=read(),y=read();
        G[x].push_back(y);//建边
        G[y].push_back(x);
    }
    dfs1(1,0);
    // rp(i,1,n) printf("%d ",hSon[i]);   
	dfs(1,0,1);
    rp(i,1,n) printf("%lld ",ans[i]);
    printf("\n");
    return 0;
}

其实这个算法的真正的做法是维护dfs序来做,这样可以优化很大的常数。

经典习题

题目链接:CF570D Tree Requests

题目大意:

给定一个以1为根的n个节点的树,每个点上有一个字母(a-z),

每个点的深度定义为该节点到1号节点路径上的点数.

每次询问 a,b.查询以a为根的子树内深度为b的节点上的字母重新排列之后是否能构成回文串.

题解:

重排之后构成回文串的条件是:每个字母出现次数都为偶数或者只有一个字母出现次数为奇数。

因此我们可以直接维护cnt数组来统计子树中在某个深度上每个字符出现次数,然后再检查是否符合条件就行了。

至于查询操作,我们可以离线处理,然后把它挂在节点上,每当统计到该节点时,更新答案就行了。

时间复杂度:O(26nlogn)

代码实现:

#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#define PI atan(1.0)*4
#define E 2.718281828
#define rp(i,s,t) for (i = (s); i <= (t); i++)
#define RP(i,s,t) for (i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define pb push_back
#define pii pair<int,int>
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
    {
        a=(a<<3)+(a<<1)+c-'0';
        c=getchar();
    }
    return a*b;
}
const int N = 5e5+7;
vector<int> G[N];
char s[N];
int deep[N],sz[N],hSon[N];
int cnt[N][30],ans[N],color[N];
int visited[N];
vector<pii> Q[N];
void dfs(int u,int f){
    deep[u]=deep[f]+1;
    sz[u]=1;
    for(auto v:G[u]){
        if(v==f) continue;
        dfs(v,u);
        sz[u]+=sz[v];
        if(sz[hSon[u]]<sz[v]) hSon[u]=v;
    }
}
void calc(int u,int f,int k){
    cnt[deep[u]][color[u]]+=k;
    for(auto v:G[u])
        if(v!=f&&!visited[v])
            calc(v,u,k);
}
void dfs1(int u,int f,int keep){
    for(auto v:G[u])
        if(v!=hSon[u]&&v!=f)
            dfs1(v,u,0);
    if(hSon[u]) dfs1(hSon[u],u,1),visited[hSon[u]]=1;
    calc(u,f,1);
    for(int i=0;i<Q[u].size();i++){
        int id=Q[u][i].first;
        int d=Q[u][i].second;
        int num=0;
        for(int i=1;i<=26;i++)
            if(cnt[d][i]&1)
                num++;
        ans[id]=num>1?0:1;
    }
    if(hSon[u]) visited[hSon[u]]=0;
    if(keep==0) calc(u,f,-1);
}
int main(){
	int n=read(),m=read(),i;
    rp(i,2,n){
        int x=read();
        G[x].push_back(i);
        G[i].push_back(x);
    }
    scanf("%s",s+1);
    int len=strlen(s+1);
    rp(i,1,len) color[i]=s[i]-'a'+1;
    // rp(i,1,len) printf("%d ",color[i]);
    dfs(1,0);
    // rp(i,1,n) printf("%d %d\n",deep[i],sz[i]);
    rp(i,1,m){
        int x=read(),y=read();
        Q[x].pb(make_pair(i,y));
    }
    dfs1(1,0,1);
    rp(i,1,m){
        if(ans[i]) printf("Yes\n");
        else printf("No\n");
    }
	return 0;
}

题目链接:CF208E Blood Cousins

题目大意:给你一片森林,每次询问一个点与多少个点拥有共同的K级祖先

题解:

我们首先考虑在一棵树上处理的情况,把问题转换一下,其实让你求的就是以某个节点为根的子树中的深度为k的节点个数

因此我们可以用倍增法先求出每一个节点的K级祖先,然后再在第k级祖先上处理维护答案就行了。

代码实现:

#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#define PI atan(1.0)*4
#define E 2.718281828
#define rp(i,s,t) for (register int i = (s); i <= (t); i++)
#define RP(i,s,t) for (register int i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define pii pair<int,int>
#define mp make_pair
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
    {
        a=(a<<3)+(a<<1)+c-'0';
        c=getchar();
    }
    return a*b;
}
const int N = 1e5+7;
int cnt[N],deep[N],sz[N],visited[N],hSon[N];
vector<int> G[N];
int ans[N],root[N];
vector<pii> Q[N];
int F[N][25];
void dfs(int u,int f){
    deep[u]=deep[f]+1;
    sz[u]=1;
    F[u][0]=f;
    rp(i,1,20) F[u][i]=F[F[u][i-1]][i-1];
    for(auto v:G[u]){
        if(v==f) continue;
        dfs(v,u);
        sz[u]+=sz[v];
        if(sz[hSon[u]]<sz[v]) hSon[u]=v;
    }
}
void calc(int u,int f,int k){
    cnt[deep[u]]+=k;
    for(auto v:G[u])
        if(!visited[v]&&v!=f)
            calc(v,u,k);
}
void dfs(int u,int f,int keep){
    for(auto v:G[u])
        if(v!=f&&v!=hSon[u])
            dfs(v,u,0);
    if(hSon[u]) dfs(hSon[u],u,1),visited[hSon[u]]=1;
    calc(u,f,1);
    for(int i=0;i<Q[u].size();i++){
        int id=Q[u][i].first;
        int d=Q[u][i].second;
        ans[id]=cnt[d]-1;
    }
    if(hSon[u]) visited[hSon[u]]=0;
    if(keep==0) calc(u,f,-1);
}
int main(){
	int n=read();
    int rootCnt=0;
    rp(i,1,n){
        int x=read();
        if(!x) root[++rootCnt]=i;
        else {
            G[i].push_back(x);
            G[x].push_back(i);
        }
    }
    rp(i,1,rootCnt) dfs(root[i],0);
    // rp(i,1,n) printf("%d %d\n",sz[i],deep[i]);
    int m=read();
    rp(i,1,m){
        int u=read(),p=read(),d=deep[u];
        for(int j=20;~j;j--)
            if(p&(1<<j))
                u=F[u][j];
        // printf("%d\n",u);
        Q[u].push_back(mp(i,d));
    }
    rp(i,1,rootCnt) dfs(root[i],0,0);
    rp(i,1,m) printf("%d\n",ans[i]);
	return 0;
}

题目链接:CF246E Blood Cousins Return

题目描述:

给定一片森林,每次询问一个节点的K-Son共有个多少不同的名字。

一个节点的K-Son即为深度是该节点深度加K的节点。

题解:当时做的时候用的是set来维护的,发现好像不是特别好处理,最后发现用map来维护比较简单。

只需要用map<string,int>来标记某个名字是否出现就行了,比较精巧的用法。

代码实现:

#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#define PI atan(1.0)*4
#define E 2.718281828
#define rp(i,s,t) for (register int i = (s); i <= (t); i++)
#define RP(i,s,t) for (register int i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define pii pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
    {
        a=(a<<3)+(a<<1)+c-'0';
        c=getchar();
    }
    return a*b;
}
const int N = 1e5+7;
vector<int> G[N],root;
int sz[N],deep[N],hSon[N];
int visited[N],cnt[N],ans[N];
string s[N];
map<string,int> ss[N];
vector<pii> Q[N];
void dfs(int u,int f){
    sz[u]=1;
    deep[u]=deep[f]+1;
    for(auto v:G[u]){
        if(v==f) continue;
        dfs(v,u);
        sz[u]+=sz[v];
        if(sz[hSon[u]]<sz[v]) hSon[u]=v;
    }
}
void calc(int u,int f,int k){
    if(!ss[deep[u]][s[u]]) cnt[deep[u]]++;
    ss[deep[u]][s[u]]+=k;
    if(!ss[deep[u]][s[u]]) cnt[deep[u]]--;
    for(auto v:G[u])
        if(!visited[v]&&v!=f)
            calc(v,u,k);
}
void dfs(int u,int f,int keep){
    for(auto v:G[u])
        if(v!=f&&v!=hSon[u])
            dfs(v,u,0);
    if(hSon[u]) dfs(hSon[u],u,1),visited[hSon[u]]=1;
    calc(u,f,1);
    for(int i=0;i<Q[u].size();i++){
        int id=Q[u][i].first;
        int d=Q[u][i].second;
        // for(auto nn:ss) cout<<nn<<" ";
        // cout<<endl;
        ans[id]=cnt[d];
    }
    if(hSon[u]) visited[hSon[u]]=0;
    if(keep==0) calc(u,f,-1);
}
int main(){
    int n=read();
    int rootCnt=0;
    rp(i,1,n){
        int x;
        cin>>s[i]>>x;
        if(!x) root.pb(i);
        else{
            G[x].pb(i);
            G[i].pb(x);
        }
    }
    rp(i,0,root.size()-1) dfs(root[i],0);
    int m=read();
    rp(i,1,m){
        int x=read(),y=read();
        // printf("%d\n",deep[x]+y);
        Q[x].pb(mp(i,deep[x]+y));
    }
    rp(i,0,root.size()-1) dfs(root[i],0,0);
    rp(i,1,m) printf("%d\n",ans[i]);
}

题目链接:CF1009F Dominant Indices

题目大意:

给定一棵以 1 为根,n 个节点的树。设 d(u,x)为 u 子树中到 u 距离为 x 的节点数。

对于每个点,求一个最小的 k,使得 d(u,k) 最大。

题解:比较简单的一道题,直接按照题意维护一下节点数,记录最小深度,然后更新就行了。

代码实现:

#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#define PI atan(1.0)*4
#define E 2.718281828
#define rp(i,s,t) for (register int i = (s); i <= (t); i++)
#define RP(i,s,t) for (register int i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define pii pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
    {
        a=(a<<3)+(a<<1)+c-'0';
        c=getchar();
    }
    return a*b;
}
const int INF = 0x3f3f3f3f;
const int N = 1e6+7;
vector<int> G[N];
int deep[N],sz[N],hSon[N];
int visited[N],ans[N];
int minDeep,maxCnt;
int cnt[N];
void dfs(int u,int f){
    deep[u]=deep[f]+1;
    sz[u]=1;
    for(auto v:G[u]){
        if(v==f) continue;
        dfs(v,u);
        sz[u]+=sz[v];
        if(sz[hSon[u]]<sz[v]) hSon[u]=v;
    }
}
void calc(int u,int f,int k){
    cnt[deep[u]]+=k;
    if(k>0&&cnt[deep[u]]>=maxCnt){
        if(cnt[deep[u]]>maxCnt) maxCnt=cnt[deep[u]],minDeep=deep[u];
        else if(cnt[deep[u]]==maxCnt&&deep[u]<minDeep){
            minDeep=deep[u];
        }
    }
    for(auto v:G[u])
        if(v!=f&&!visited[v])
            calc(v,u,k);
}
void dfs(int u,int f,int keep){
    for(auto v:G[u])
        if(v!=f&&v!=hSon[u])
            dfs(v,u,0);
    if(hSon[u]) dfs(hSon[u],u,1),visited[hSon[u]]=1;
    calc(u,f,1);
    ans[u]=minDeep-deep[u];
    if(hSon[u]) visited[hSon[u]]=0;
    if(keep==0) calc(u,f,-1),minDeep=INF,maxCnt=0;
}
int main(){
    int n=read();
    rp(i,1,n-1){
        int x=read(),y=read();
        G[x].pb(y);
        G[y].pb(x);
    }
    dfs(1,0);
    dfs(1,0,1);
    rp(i,1,n) printf("%d\n",ans[i]);
    return 0;
}

题目链接:CF375D Tree and Queries

题目大意:给出一棵 n 个结点的树,每个结点有一个颜色 c i 。 询问 q 次,每次询问以 v 结点为根的子树中,出现次数 ≥k 的颜色有多少种。树的根节点是1。

题解:我的解法是维护一个cnt数组记录颜色出现次数,再维护一个num数组记录出现次数的次数,然后暴力枚举更新答案,本来以为会爆,但是竟然过了,开心!!!!。

更新:这个题数据比较弱,树上莫队也能过,正解应该是用num[k]来记录出现次数大于等于k的个数,其实就是每次更新出现次数的次数时,之前的出现次数的次数不删去,然后再更新当前出现次数的次数,类似于维护了一个后缀和。

用线段树或者树状数组维护也许,不过时间复杂度就变成了O(n*logn*logn),两个log。

代码实现:

#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#define PI atan(1.0)*4
#define E 2.718281828
#define rp(i,s,t) for (register int i = (s); i <= (t); i++)
#define RP(i,s,t) for (register int i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define pii pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
    {
        a=(a<<3)+(a<<1)+c-'0';
        c=getchar();
    }
    return a*b;
}
const int N = 1e5+7;
int cnt[N],color[N];
int num[N]; 
int visited[N];
vector<int> G[N];
int hSon[N],sz[N];
vector<pii> Q[N];
int ans[N];
void dfs(int u,int f){
    sz[u]=1;
    for(auto v:G[u]){
        if(v==f) continue;
        dfs(v,u);
        sz[u]+=sz[v];
        if(sz[hSon[u]]<sz[v]) hSon[u]=v;
    }
}
void calc(int u,int f,int k){//num[k]表示出现次数大于等于k的个数
    if(k==-1) num[cnt[color[u]]]+=k;
    cnt[color[u]]+=k;
    if(k==1) num[cnt[color[u]]]+=k;
    for(auto v:G[u])
        if(!visited[v]&&v!=f)
            calc(v,u,k);
}
void dfs(int u,int f,int keep){
    for(auto v:G[u])
        if(v!=f&&v!=hSon[u])
            dfs(v,u,0);
    if(hSon[u]) dfs(hSon[u],u,1),visited[hSon[u]]=1;
    calc(u,f,1);
    // printf("%d\n",maxNum);
    for(int i=0;i<Q[u].size();i++){
        int id=Q[u][i].first;
        int k=Q[u][i].second;
        ans[id]=num[k];
    }
    if(hSon[u]) visited[hSon[u]]=0;
    if(keep==0) calc(u,f,-1);
}
int main(){
    int n=read(),q=read();
    rp(i,1,n) color[i]=read();
    rp(i,1,n-1){
        int x=read(),y=read();
        G[x].pb(y);
        G[y].pb(x);
    }
    dfs(1,0);
    rp(i,1,q){
        int u=read(),k=read();
        Q[u].pb(mp(i,k));
    }
    dfs(1,0,1);
    rp(i,1,q) printf("%d\n",ans[i]);
    return 0;
}

题目链接:wannafly Day2 E 阔力梯的树

题目大意:给你一个n个节点的树,求每个节点的"结实程度"

一个节点的结实程度定义为以该节点为根的子树里所有节点的编号从小到大排列后,相邻编号的平方和。

假设一个节点的子树中所有节点编号排序后构成的序列为a1,a2,a3.....ak,那么答案为\sum_{i=1}^{k-1}(a_{i+1}-a_{i})^{2}

题解:这个题是我补这类算法的初衷题,最后也总算是A了,完成我的计划之一。

第一想法肯定是用一个set来维护子树中的所有节点,最后再暴力遍历一下,更新答案,不要想肯定会超时,我已经试过了

O(n^2logn)的算法,1e5的数据,咋可能让你过类。

然后我们考虑优化,不难想到我们可以在维护子树节点时顺便统计答案,因为一个节点如果插入在最前面或最后面,对答案的贡献都和它后一个或者前一个数有关,如果插入在中间,那么对答案的贡献与两边都有关,维护一下就行了,最后我们要特判一下set为空的情况就行了。

trick:节点数为1的情况特判

代码实现:

#pragma GCC optimize(2)
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <map>
#include <set>
#include <stack>
#include <queue>
#define PI atan(1.0) * 4
#define E 2.718281828
#define rp(i, s, t) for (register int i = (s); i <= (t); i++)
#define RP(i, s, t) for (register int i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a, b) memset(a, b, sizeof(a))
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
#define pb push_back
using namespace std;
inline int read()
{
    int a = 0, b = 1;
    char c = getchar();
    while (c < '0' || c > '9')
    {
        if (c == '-')
            b = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9')
    {
        a = (a << 3) + (a << 1) + c - '0';
        c = getchar();
    }
    return a * b;
}
const int N = 1e5 + 7;
vector<int> G[N];
int visited[N];
int hSon[N], sz[N];
ll ans[N];
void dfs(int u, int f)
{
    sz[u] = 1;
    for (auto v : G[u])
    {
        if (v == f)
            continue;
        dfs(v, u);
        sz[u] += sz[v];
        if (sz[hSon[u]] < sz[v])
            hSon[u] = v;
    }
}
set<int> s;
ll sum;
void calc(int u, int f, int k)
{
    if (k == 1)
    {
        if (s.empty())
            s.insert(u);
        else
        {
            if (s.lower_bound(u) == s.end())
            {
                set<int>::iterator it = s.end();
                it--;
                sum += 1ll*(u - *it) * (u - *it);
            }
            else if (s.lower_bound(u) == s.begin())
            {
                set<int>::iterator it = s.begin();
                sum += 1ll*(*it - u) * (*it - u);
            }
            else
            {
                set<int>::iterator it = s.lower_bound(u);
                set<int>::iterator it1 = it;
                it1--;
                sum -= 1ll*(*it - *it1) * (*it - *it1);
                sum += 1ll*(u - *it1) * (u - *it1);
                sum += 1ll*(*it - u) * (*it - u);
            }
            s.insert(u);
        }
    }
    else
        s.erase(u);
    for (auto v : G[u])
        if (!visited[v] && v != f)
            calc(v, u, k);
}
void dfs(int u, int f, int keep)
{
    for (auto v : G[u])
        if (v != f && v != hSon[u])
            dfs(v, u, 0);
    if (hSon[u])
        dfs(hSon[u], u, 1), visited[hSon[u]] = 1;
    // cout<<"ac"<<endl;
    calc(u, f, 1);
    // cout<<"ac"<<endl;
    // printf("%d\n",u);
    ans[u] = sum;
    if (hSon[u])
        visited[hSon[u]] = 0;
    if (keep == 0)
        calc(u, f, -1), sum = 0;
}
int main()
{
    int n = read();
    if(n==1){
        printf("0\n");
        return 0;
    }
    rp(i, 2, n)
    {
        int x = read();
        G[x].pb(i);
        G[i].pb(x);
    }
    dfs(1, 0);
    // rp(i,1,n) printf("%d %d\n",sz[i],hSon[i]);
    dfs(1, 0, 1);
    rp(i, 1, n) printf("%lld\n", ans[i]);
    return 0;
}

最后放一个比较难的题,也是这个算法的发明者专门为这个算法出的题

题目链接:CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

题目大意:

一棵根为1 的树,每条边上有一个字符(a-v共22种)。

一条简单路径被称为Dokhtar-kosh当且仅当路径上的字符经过重新排序后可以变成一个回文串。 求每个子树中最长的Dokhtar-kosh路径的长度。

题解:因为比较难,也比较好,所以我专门写了一篇解题报告:题解

代码实现:

#pragma GCC optimize(2)
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<queue>
#define PI atan(1.0)*4
#define E 2.718281828
#define rp(i,s,t) for (register int i = (s); i <= (t); i++)
#define RP(i,s,t) for (register int i = (t); i >= (s); i--)
#define ll long long
#define ull unsigned long long
#define mst(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define pii pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
inline int read()
{
    int a=0,b=1;
    char c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
            b=-1;
        c=getchar();
    }
    while(c>='0'&&c<='9')
    {
        a=(a<<3)+(a<<1)+c-'0';
        c=getchar();
    }
    return a*b;
}
const int INF = 0x3f3f3f3f;
const int N = 5e5+7;
int tot,L[N],R[N],Id[N];//用来维护dfs序
int head[N],cnt;
struct Edge{
    int to,nxt,w;
}e[N];//链式向前星存储树
int deep[N],sz[N],hSon[N];//分别记录节点的深度,大小,以及重儿子
int ans[N],visited[N];//记录每个点的答案和标记是否访问过
int f[N*10],dis[N];
//dis[i]表示从1(根节点)到 i 点路径上的状态
//f[i]表示在以当前节点为根的子树中状态为 i 的最大深度
void addEdge(int u,int v,int w){//建边
    e[++cnt]=(Edge){v,head[u],w};
    head[u]=cnt;
}
void dfs1(int u,int f){//这一次dfs是预先求出每个节点的深度,重儿子,以及每个节点的dfs序,常规操作
    deep[u]=deep[f]+1;
    sz[u]=1;
    L[u]=++tot;
    Id[tot]=u;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        dis[v]=dis[u]^e[i].w;
        dfs1(v,u);
        sz[u]+=sz[v];
        if(sz[hSon[u]]<sz[v]) hSon[u]=v;
    }
    R[u]=tot;
}
void calc(int u){
    //这里f[dis[u]]表示在以u为根的子树中状态为dis[u]的最大深度
    if(f[dis[u]]) ans[u]=max(ans[u],f[dis[u]]-deep[u]);
    rp(i,0,21) if(f[dis[u]^(1<<i)]) ans[u]=max(ans[u],f[dis[u]^(1<<i)]-deep[u]);
    //这里看着有点不好理解,其实就是利用了异或的性质,我们首先需要考虑以u为起点对答案的贡献
    //因为dis[u]表示从1到u的状态,所以如果符合条件一定满足 x^dis[u]=0||(1<<i) -> x = dis[u] ^ 0||(1<<i); 
    f[dis[u]]=max(f[dis[u]],deep[u]);//更新和维护f数组
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==hSon[u]||visited[v]) continue;
        rp(j,L[v],R[v]){//这里考虑的是以u为中间点的情况,其实和上面情况类似
            int x=Id[j];
            //其实就是看另一边是否出现相同满足条件状态,出现的话更新答案
            if(f[dis[x]]) ans[u]=max(ans[u],f[dis[x]]+deep[x]-2*deep[u]);
            rp(k,0,21) if(f[dis[x]^(1<<k)]) ans[u]=max(ans[u],f[dis[x]^(1<<k)]+deep[x]-2*deep[u]);
        }
        rp(j,L[v],R[v]) f[dis[Id[j]]]=max(f[dis[Id[j]]],deep[Id[j]]);//维护f数组 
    }
}
void dfs2(int u,int keep){//keep为 1 表示处理并保留数据,否则表示处理但不保留数据
    for(int i=head[u];i;i=e[i].nxt){//先对轻儿子进行处理答案,但是不保留数据
        int v=e[i].to;
        if(v==hSon[u]) continue;
        dfs2(v,0);
        ans[u]=max(ans[u],ans[v]);//更新答案,因为有可能最大长度的链不以当前节点为根节点
    }
    if(hSon[u]) dfs2(hSon[u],1),ans[u]=max(ans[u],ans[hSon[u]]),visited[hSon[u]]=1;//对重儿子进行类似处理,但是保留数据并且标记
    calc(u);//更新答案
    if(hSon[u]) visited[hSon[u]]=0;
    if(keep==0) rp(i,L[u],R[u]) f[dis[Id[i]]]=0;//如果为轻儿子,则清空数据
}
int main(){
    int n=read();
    rp(i,2,n){
        int x;char s[10];
        scanf("%d%s",&x,s);
        addEdge(x,i,1ll<<(s[0]-'a'));
    }
    dfs1(1,1);
    dfs2(1,1);
    rp(i,1,n) printf("%d ",ans[i]);
    return 0;
}

总结

这类题型,主要是如果你知道这个算法,并且能够写出暴力的算法,这个题你就能做出来了。

其实大致都是套路,但是前提是你要知道这个套路。

参考链接

树上启发式合并总结

[洛谷日报第65期]树上启发式合并

发明者的原算法链接

终于写完了!!,大晚上的写总结的感觉真爽,感觉脸上充满了月之精华。

发布了342 篇原创文章 · 获赞 220 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_43472263/article/details/104150940