本題當然可以通過大力討論每棵子樹的size的大小關系,然后用各種數據結構暴力維護。但是我更傾向於用一種更為性質的做法。
首先講一下我在考場上想到的做法(沒寫)。就是考慮換根,在換根的過程中計算每一條邊刪去后得到的兩棵子樹的重心, 由於重心的一些性質,如果我們把以點v的所有兒子為根的子樹的重心求了出來,那么我們要求以點v為根的子樹的重心時,可以保證重心一定在v的重兒子子樹的重心到v的鏈上,那么我們就可以通過鏈上倍增來實現對多棵子樹的重心的合並。在換根時維護每個點子樹size,以及子樹內的重心,換根時倍增求新子樹的重心即可。
代碼:
#include<bits/stdc++.h>
using namespace std;
#define N 300007
#define M 600007
#define ll long long
#define mem(x) memset(x,0,sizeof(x))
int hd[N],pre[M],to[M],num,wa[N],wb[N],sz[N],ts,dep[N];
int anc[N][23],ms[N],n;
ll ans;
void clear()
{
mem(hd),mem(wa),mem(wb),mem(sz);
mem(dep),mem(ms);
num=0;ans=0;
}
int calc(int v)
{
return max(ms[v],ts-sz[v]);
}
void solve(int x,int d,int &a,int &b)
{
if(dep[x]<d)return;
for(int i=20;i>=0;i--)
{
int mid=anc[x][i],y=anc[mid][0];
if(dep[y]>=d&&calc(mid)>=calc(y))
x=mid;
}
int y=anc[x][0];
int dx=calc(x),dy=calc(y);
if(dep[x]==d||dx<dy)
{
if(dx<=ts/2)a=x;
}
else
{
if(dy<=ts/2)
{
a=y;
if(dx==dy)b=x;
}
}
}
/*void solve(int x,int d,int &a,int &b)
{
while(dep[x]>=d)
{
if(calc(x)<=ts/2)
{
if(a)b=x;
else a=x;
}
x=anc[x][0];
}
}*/
int glca(int x,int y)
{
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--)
if(dep[x]-dep[y]>=(1<<i))
x=anc[x][i];
if(x==y)return x;
for(int i=20;i>=0;i--)
if(anc[x][i]!=anc[y][i])
x=anc[x][i],y=anc[y][i];
return anc[x][0];
}
int dmax(int x,int y)
{
return dep[x]>dep[y]?x:y;
}
int dmin(int x,int y)
{
return dep[x]<dep[y]?x:y;
}
void dfs(int v,int f)
{
dep[v]=dep[f]+1;
anc[v][0]=f;
for(int i=1;i<=20;i++)
anc[v][i]=anc[anc[v][i-1]][i-1];
sz[v]=1;
int son=v;
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(u==f)continue;
dfs(u,v);
sz[v]+=sz[u];
if(sz[u]>ms[v])ms[v]=sz[u],son=u;
}
ts=sz[v];
int x=( son==v ? v : dmax(wa[son],wb[son]) );
solve(x,dep[v],wa[v],wb[v]);
}
void dp(int v,int f)
{
int s1,d1,s2,d2;
s1=d1=s2=d2=0;
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(sz[u]>s1)s2=s1,d2=d1,s1=sz[u],d1=u;
else if(sz[u]>s2)s2=sz[u],d2=u;
}
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(u==f)continue;
int x=d1;
if(u==d1)x=d2;
int _ms=ms[v],_wa=wa[v],_wb=wb[v];
sz[v]-=sz[u],sz[u]+=sz[v];
ms[v]=sz[x];
wa[v]=wb[v]=0;
ts=sz[v];
int p=dmax(wa[x],wb[x]);
//if(p==0)printf("WA\n");
if(x==f)
{
int q=dmin(wa[x],wb[x]);
int lca=glca(p,v);
if(q!=0&&q==anc[lca][0])p=q,lca=p;
solve(p,dep[lca],wa[v],wb[v]);
solve(v,dep[lca]+1,wb[v],wa[v]);
}
else
{
solve(p,dep[v],wa[v],wb[v]);
}
/*if(!wa[v]&&!wb[v])
{
printf("%d\n",v);
printf("%d\n",x);
}*/
ans+=wa[v]+wb[v]+wa[u]+wb[u];
dp(u,v);
wa[v]=_wa,wb[v]=_wb;
ms[v]=_ms;
sz[u]-=sz[v],sz[v]+=sz[u];
}
}
void adde(int x,int y)
{
num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
void dewa()
{
for(int i=1;i<=n;i++)
printf("%d: %d %d\n",i,wa[i],wb[i]);
}
int main()
{
//freopen("data.in","r",stdin);
int t;
scanf("%d",&t);
while(t--)
{
clear();
int x,y;
scanf("%d",&n);
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
adde(x,y),adde(y,x);
}
dfs(1,0);
//dewa();
dp(1,0);
printf("%lld\n",ans);
}
return 0;
}
還有一種題解做法,也是利用重心的性質,但是比我的更巧妙,而且實現也更簡單。
就是考慮以某一個點v為根時怎么找它的重心,可以發現從點v出發,到重心的路徑一定是不斷沿着重兒子走,而每一個點的重兒子是唯一的,那么我們用倍增優化沿着重兒子走的過程,設f[i][j]為從點i開始,走\(2^j\)步走到的位置,我們只要找到最后一個滿足\(2*size[v]\geq sum\)的點即可,其中sum是割邊后當前連通塊的大小。同樣像前一種做法一樣換根即可,注意f[i][j]是可以換根維護的。
題解做法:
#include<bits/stdc++.h>
using namespace std;
#define N 300007
#define M 600007
#define ll long long
#define mem(x) memset(x,0,sizeof(x))
int hd[N],pre[M],to[M],num,son[N],fa[N],sz[N];
int ch[N][23],n;
ll ans;
void clear()
{
mem(hd),mem(son);
ans=0,num=0;
}
void adde(int x,int y)
{
num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
void mak_st(int v)
{
for(int i=1;i<=20;i++)
ch[v][i]=ch[ch[v][i-1]][i-1];
}
void dfs(int v,int f)
{
fa[v]=f,sz[v]=1;
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(u==f)continue;
dfs(u,v);
sz[v]+=sz[u];
if(sz[u]>sz[son[v]])son[v]=u;
}
ch[v][0]=son[v];
mak_st(v);
}
void solve(int x,int ts)
{
for(int i=20;i>=0;i--)
{
int y=ch[x][i];
if(y&&2*sz[y]>=ts)x=y;
}
ans+=x;
if(2*sz[x]==ts)ans+=fa[x];
}
void dp(int v,int f)
{
int s1,s2;
s1=s2=0;
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(sz[u]>sz[s1])s2=s1,s1=u;
else if(sz[u]>sz[s2])s2=u;
}
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(u==f)continue;
int x=s1;
if(u==s1)x=s2;
sz[v]-=sz[u];
ch[v][0]=x;
mak_st(v);
solve(v,sz[v]),solve(u,sz[u]);
sz[u]+=sz[v],fa[v]=u;
dp(u,v);
sz[u]-=sz[v],sz[v]+=sz[u];
}
ch[v][0]=son[v];
mak_st(v);
fa[v]=f;
}
int main()
{
//freopen("data.in","r",stdin);
int t;
scanf("%d",&t);
while(t--)
{
clear();
scanf("%d",&n);
int x,y;
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
adde(x,y),adde(y,x);
}
dfs(1,0),dp(1,0);
printf("%lld\n",ans);
}
return 0;
}
比較兩種做法,發現其實都利用的是重兒子的性質,只不過我的做法是從重兒子子樹內往根走,而題解做法是從根往重兒子走。為什么題解做法比我的短了很多呢?
因為我的倍增數組是從下往上倍增,換根時根的改變會導致下面所有子孫結點的倍增數組改變,於是不能在換根過程中維護,所以我的倍增數組一直是以1號點為根的,就多了當前點的重兒子是從以1為根時的父親方向來還是從兒子方向來的討論。而題解是從上往下倍增的,換根時不會導致下面的點發生改變,而能夠自上往下倍增的原因又是題解利用了每個點只會往唯一的重兒子走的性質,那么每個點出發走若干步走到的點是唯一的,就可以倍增了。
我在洛谷的題解中,看到了一篇大力討論子樹size的題解,里面的一些計數技巧還是有點意思的,比如說把計算每條邊割斷后子樹重心標號之和轉化為,對每一個點計算它會在多少條邊被割斷之后成為子樹的重心,大家可以看一看。Mr_Wu 的博客