**~~
**~~
struct edge{ int to,nxt; }e[MAX_E<<1];
int head[MAX_N],tote;
void add_edge(int u,int v){e[++tote].to=v,e[tote].nxt=head[u];head[u]=tote;}
int maxn,depth[MAX_N],dp[MAX_N][log2(MAX_N)+1];
void dfs(int u,int fa)
{
dp[u][0]=fa,depth[u]=depth[fa]+1;
repi(i,1,maxn) dp[u][i]=dp[dp[u][i-1]][i-1];
reps(u)if(v!=fa) dfs(v,u);
}
int LCA(int x,int y)
{
if(depth[x]<depth[y]) swap(x,y);
repd(i,maxn,0)if((1<<i)<=(depth[x]-depth[y])) x=dp[x][i];
if(x==y) return x;
repd(i,maxn,0)if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i];
return dp[x][0];
}
void init(int n)
{
repi(i,0,n) head[i]=0;
tote=0;
maxn=floor(log(n+0.0)/log(2.0));
}
/*
depth[0]=0,dfs(rt,0);
*/
struct edge{ int to,nxt; }e[MAX_E<<1];
int head[MAX_N],tote;
void add_edge(int u,int v){e[++tote].to=v,e[tote].nxt=head[u];head[u]=tote;}
MAX_N)]],first[MAX_N];
int dp[MAX_N<<2][log2(MAX_N<<2)+1];
void init(int n)
{
repi(i,0,n) head[i]=0;
tot=tote=0;
}
void dfs(int u,int fa,int dep)
{
ver[++tot]=u,first[u]=tot,depth[tot]=dep;
reps(u)if(v!=fa) dfs(v,u,dep+1),ver[++tot]=u,depth[tot]=dep;
}
void ST(int n)
{
repi(i,1,n) dp[i][0]=i;
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)-1<=n;i++){
int a=dp[i][j-1],b=dp[i+(1<<(j-1))][j-1];
dp[i][j]=depth[a]<depth[b]?a:b;
}
}
}
int RMQ(int l,int r)
{
int k=log2(r-l+1);
return (depth[dp[l][k]]<depth[dp[r-(1<<k)+1][k]])?dp[l][k]:dp[r-(1<<k)+1][k];
}
int LCA(int x,int y)
{
x=first[x],y=first[y];
if(x>y) swap(x,y);
return ver[RMQ(x,y)];
}
/*
dfs(rt,0,1),ST(tot);
*/
1.对于两棵树,如果第一棵树直径两端点为(u,v),第二棵树直径两端点为(x,y),用一条边将两棵树连接,那么新树的直径一定是u,v,x,y,中的两个点(不论是两棵树中哪两个点连接)
证明:如果新树直径不是原来两棵树中一棵的直径,那么新直径一定经过两棵树的连接边,新直径在原来每棵树中的部分一定是距离连接点最远的点,即一定是原树直径的一个端点。
2.对于一棵树,如果在一个点的上接一个叶子节点,那么最多会改变直径的一个端点
3.若一棵树存在多条直径,那么这些直径交于一点且交点是这些直径的中点(一条边/一个点 注意是按路径长而非边个数)
int st,ed,mx;
void dfs(int u,int fa,int dep)
{
if(mx<dep) mx=dep,st=u;
reps(u)if(v!=fa) dfs(v,u,dep+1);
}
/*
mx=0,dfs(1,0,0);
ed=st;
mx=0,dfs(st,0,0);
*/
int ans,dp[MAX_N];//dp[i]以i为根的子树中到i距离最远的点 ans要初始化
void dfs_dp(int u,int fa)
{
reps(u)if(v!=fa){
dfs_dp(v,u);
ans=max(ans,dp[u]+dp[v]+e[i].w);
dp[u]=max(dp[u],dp[v]+e[i].w);
}
}
// dfs_dp(1,0)
例题
1.P3304 求给定树有多少条边一定是直径边
解法1: 利用性质3.若一棵树存在多条直径,那么这些直径交于一点且交点是这些直径的中点(一条边/一个点 注意是按路径长而非边个数)。找到中心位置。
(1)若为一条边,则改变必选,另外就是边的两点。要看以这两点(拆掉中心边)为根的子树中,到达叶节点最远的路径中的必经边。记录ml数组,该点到以该点为根的子树叶结点的最远距离。递归处理,对当前节点,看可以通过几个节点到达最远,若为1,边必选,向下继续找,>1说明后续不存在必经边。
(2)若为一个点,找到该点连接的所有点,记录通过对应点能走的最远距离,取前3远的 fi se th,若一共就两个点/se>th,就对fi,se对应的点进行上面的递归处理,若fi>se se=th就单独对fi处理。其他情况说明不存在必经边。
int st,ed;
int pre[MAX_N];
ll mx,ml[MAX_N];
void dfs(int u,int fa,ll dep)
{
pre[u]=fa,ml[u]=0;
if(mx<dep) mx=dep,st=u;
reps(u)if(v!=fa) dfs(v,u,dep+e[i].w),ml[u]=max(ml[u],ml[v]+e[i].w);
}
int ans=0;
void solve(int u,int fa)
{
dfs(u,fa,0);
while(true){
ll mx=0,cnt=0,to;
reps(u)if(v!=fa&&ml[v]+e[i].w>=mx){
if(mx==ml[v]+e[i].w) cnt++;
else mx=ml[v]+e[i].w,cnt=1,to=v;
}
if(cnt!=1) break;
ans++,fa=u,u=to;
}
}
void init(int n)
{
repi(i,0,n) head[i]=0;
tote=0;
}
int main()
{
int n; si(n); init(n);
repi(i,1,n-1){
int u,v; si(u),si(v);
ll w; sl(w);
add_edge(u,v,w),add_edge(v,u,w);
}
mx=0,dfs(1,0,0);
ed=st;
mx=0,dfs(st,0,0);
printf("%lld\n",mx);
int mid=st;
while(ml[pre[mid]]*2<=mx) mid=pre[mid];
if(ml[mid]*2==mx){
dfs(mid,0,0);
int cnt=0;
vector<P> rec;
reps(mid) rec.pb(P(ml[v]+e[i].w,v));
sort(all(rec));
int len=rec.size();
if(len==2||rec[len-2].fi>rec[len-3].fi) solve(rec[len-1].se,mid),solve(rec[len-2].se,mid);
else if(rec[len-1].fi>rec[len-2].fi&&rec[len-2].fi==rec[len-3].fi) solve(rec[len-1].se,mid);
}
else{
ans++;
int a=mid,b=pre[mid];
solve(a,b),solve(b,a);
}
printf("%d\n",ans);
return 0;
}
解法2:对每条边u,v,求出以u,v为根的子树(断开这条边),求出以v为根子树到v向下,向上最远的距离及路径条数(两次树形dp)。若距离和积等于路径长度,路径之积等于直径路径数,则该边为必经边
struct node{
ll w,cnt;//最远距离 边数
node(){w=cnt=0;}
node(ll _w,ll _cnt){ w=_w,cnt=_cnt; }
void comb(ll _w,ll _cnt)
{
if(_w>w) w=_w,cnt=_cnt;
else if(_w==w) cnt+=_cnt;
}
}dp1[MAX_N],dp2[MAX_N],mx;
ll w[MAX_N];
void dfs_1(int u,int fa)//处理出向下最远距离,路径数
{
dp1[u].w=0,dp1[u].cnt=1;
reps(u)if(v!=fa){
w[v]=e[i].w,dfs_1(v,u);
mx.comb(dp1[u].w+dp1[v].w+e[i].w,dp1[u].cnt*dp1[v].cnt);
dp1[u].comb(dp1[v].w+e[i].w,dp1[v].cnt);
}
}
int ans=0;
node front[MAX_N],rear[MAX_N];
void dfs_2(int u,int fa)//处理出向上的最远距离,路径数
{
if(fa){
dp2[u].w=dp2[u].cnt=0;
dp2[u].comb(dp2[fa].w+w[u],dp2[fa].cnt);
dp2[u].comb(front[u].w+w[u],front[u].cnt),dp2[u].comb(rear[u].w+w[u],rear[u].cnt);
if(dp1[u].w+dp2[u].w==mx.w&&dp1[u].cnt*dp2[u].cnt==mx.cnt) ans++;
}
vector<int> rec;
node t;
reps(u)if(v!=fa) rec.pb(v),front[v]=t,t.comb(dp1[v].w+e[i].w,dp1[v].cnt);
int len=rec.size(); t.w=t.cnt=0;
repd(i,len-1,0) rear[rec[i]]=t,t.comb(dp1[rec[i]].w+w[rec[i]],dp1[rec[i]].cnt);
reps(u)if(v!=fa) dfs_2(v,u);
}
void init(int n)
{
repi(i,0,n) head[i]=0;
tote=0;
}
int main()
{
int n; si(n); init(n);
repi(i,1,n-1){
int u,v; si(u),si(v);
ll w; sl(w);
add_edge(u,v,w),add_edge(v,u,w);
}
dfs_1(1,0),dp2[1].w=0,dp2[1].cnt=1,dfs_2(1,0);
printf("%lld\n%d\n",mx.w,ans);
return 0;
}
1.在一棵树中,所有点到某点的距离之和,是到重心的距离和最小.
2.把两棵树通过任何一点相连,形成的树的重心都在这两棵树的重心的连线上.
3.给一棵树添加或者删除一个节点,重心位置最多移动一格.
4.以重心为根,任何一棵子树的大小都不超过整棵树大小的一半.
5.删去重心及关联边,所有子树大小小于 ⌊ 总 点 数 2 ⌋ \left \lfloor \frac{总点数}{2} \right \rfloor ⌊2总点数⌋
6. 设一棵树以x为根,那么它的重心要么是x,要么在x所在的重链上。
模板 求树的重心
int siz[MAX_N],w[MAX_N],mxsiz,rt,totsiz;//初始化mxsiz为inf,totsiz为树的节点个数
//int k,rt1,rt2;
void get_barycenter(int u,int fa)
{
siz[u]=1,w[u]=0;
reps(u)if(v!=fa){
get_barycenter(v,u),siz[u]+=siz[v];
w[u]=max(w[u],siz[v]);
}
w[u]=max(w[u],totsiz-siz[u]);
if(mxsiz>w[u]) mxsiz=w[u],rt=u;
/*
if(w[u]
}
模板 求一颗树以任意一个节点为根的子树的重心
int siz[MAX_N],fa[MAX_N];
vector<int> ans[MAX_N];
void dfs(int u,int pre)
{
fa[u]=pre,siz[u]=1;
int son=-1;
reps(u)if(v!=pre){
dfs(v,u),siz[u]+=siz[v];
if(son==-1||siz[v]>siz[son]) son=v;
}
if(son==-1) ans[u].pb(u);
else{
son=ans[son][0];
while((2*(siz[u]-siz[son]))>siz[u]) son=fa[son];
ans[u].pb(son);
if(siz[u]-siz[son]==siz[son]) ans[u].pb(fa[son]);
}
}
/*
dfs(root,0)
*/
例题
1.P5666 求出一颗树删去每条边后,分裂出的两颗子树的重心编号和
设一棵树以x为根,那么它的重心要么是x,要么在x连出的重链上。
删去重心及关联边,所有子树大小小于 ⌊ 总 点 数 2 ⌋ \left \lfloor \frac{总点数}{2} \right \rfloor ⌊2总点数⌋
利用这两个性质,对任何一颗树,要从根找到重心,只需一直沿着重链跳,每次跳的时候验证 节点数-以下一条节点为根的子树的节点数 小于等于节点数/2
最后停留的点及其重儿子,父节点都按性质判一下就可找到所有重心
首先倍增出每个节点向下跳的重儿子
枚举删除每条边,dfs处理
对每条边u-v,以v为根的子树直接倍增跳即可
对以u为根的,先找重儿子,再重新维护倍增的重链后跳着找即可
回溯时恢复原先维护的信息
int dp[MAX_N][19],pre[MAX_N],pre2[MAX_N];
int son[MAX_N][2],son2[MAX_N],siz[MAX_N],siz2[MAX_N];
void dfs(int u,int fa)
{
siz[u]=1,pre[u]=fa;
reps(u)if(v!=fa){
dfs(v,u),siz[u]+=siz[v];
if(siz[v]>siz[son[u][0]]) son[u][1]=son[u][0],son[u][0]=v;
else if(siz[v]>siz[son[u][1]]) son[u][1]=v;
}
dp[u][0]=son[u][0];
repi(i,1,18) dp[u][i]=dp[dp[u][i-1]][i-1];
}
ll ans;
ll check(int u,int sum)
{
return 1ll*u*(max(siz2[son2[u]],sum-siz2[u])<=sum/2);
}
void dfs_2(int u,int fa)
{
reps(u)if(v!=fa){//xxx2为新状态下的每个节点的相应数值
siz2[u]=siz[1]-siz[v],pre2[u]=pre2[v]=0;
son2[u]=(son[u][0]==v)?son[u][1]:son[u][0];
if(siz2[fa]>siz2[son2[u]]) son2[u]=fa;
dp[u][0]=son2[u];
repi(j,1,18) dp[u][j]=dp[dp[u][j-1]][j-1];
int tmp=u;
repd(j,18,0)if(siz2[u]-siz2[dp[tmp][j]]<=siz2[u]/2) tmp=dp[tmp][j];
ans+=check(son2[tmp],siz2[u])+check(tmp,siz2[u])+check(pre2[tmp],siz2[u]);
tmp=v;
repd(j,18,0)if(siz2[v]-siz2[dp[tmp][j]]<=siz2[v]/2) tmp=dp[tmp][j];
ans+=check(son2[tmp],siz2[v])+check(tmp,siz2[v])+check(pre2[tmp],siz2[v]);
pre2[u]=v,dfs_2(v,u);
}
son2[u]=dp[u][0]=son[u][0],pre2[u]=pre[u],siz2[u]=siz[u];
repi(i,1,18) dp[u][i]=dp[dp[u][i-1]][i-1];
}
void init(int n)
{
repi(i,0,n) head[i]=son[i][0]=0;
tote=ans=0;
}
int main()
{
int T; si(T);
while(T--)
{
int n; si(n); init(n);
repi(i,1,n-1){
int u,v; si(u),si(v);
add_edge(u,v),add_edge(v,u);
}
dfs(1,0);
repi(i,1,n) son2[i]=son[i][0],siz2[i]=siz[i],pre2[i]=pre[i];
dfs_2(1,0);
printf("%lld\n",ans);
}
return 0;
}
2.P4582 求给定树有多少联通子树和这个树重心相同(若原树两个重心则联通子树也要两个中心)
看代码注释吧
int siz[MAX_N],w[MAX_N],mxsiz,totsiz;//???mxsiz?inf,totsiz???????
int k,rt1,rt2;
void get_barycenter(int u,int fa)//找原树重心
{
siz[u]=1,w[u]=0;
reps(u)if(v!=fa){
get_barycenter(v,u),siz[u]+=siz[v];
w[u]=max(w[u],siz[v]);
}
w[u]=max(w[u],totsiz-siz[u]);
if(w[u]<mxsiz) mxsiz=w[u],k=1,rt1=u; //判断重心数目
else if(w[u]==mxsiz) k=2,rt2=u;
}
int dp[MAX_N][MAX_N];//dp[i][j] 以i为根的子树一共带j个点的方案数
void dfs(int u,int fa)
{
dp[u][0]=0,dp[u][1]=1,siz[u]=1;
reps(u)if(v!=fa){
dfs(v,u),siz[u]+=siz[v];
repd(k,siz[u],1)repi(j,1,min(siz[v],k-1)) dp[u][k]=(dp[u][k]+dp[u][k-j]*dp[v][j]%mod)%mod;
}//对dp数组的第二维要倒序更新,若正序会是先前的更新对之后的更新造成影响(选用了相同子树的节点)
}
int g[MAX_N][MAX_N];//g[i][j] 一共选i个节点 子树最大大小j的方案数
int solve_1()
{
dfs(rt1,0);
int ans=0,tot=0,mx=0;
reps(rt1){
tot+=siz[v],mx=max(mx,siz[v]);
repd(j,tot,1)repd(u,min(j,siz[v]),1){//第二维也是倒序枚举
if(j==u){ g[j][u]=(g[j][u]+dp[v][u])%mod; continue; }//特判j=u,因为g[j][0]=0
for(int t=1;t<=mx&&t<=j;t++) g[j][max(t,u)]=(g[j][max(t,u)]+g[j-u][t]*dp[v][u]%mod)%mod;//枚举不同最大子树大小,注意判断,因为可能新加的u更大
}
}
repi(j,1,tot)repi(t,1,mx)if(t*2<(j+1)) ans=(ans+g[j][t])%mod;//选j个节点 最大子树大小小于等于j/2的可行(重心定义)
return ans+1;
}
int w2[MAX_N];//w2[i]选了i个节点的方案数
int solve_2()
{
dfs(rt1,0);
int ans=0,tot=0; w2[0]=1;
reps(rt1)if(v!=rt2){
tot+=siz[v];//w2最大只用维护到siz[rt2]-1,因为只用到这里
repd(j,min(tot,siz[rt2]-1),1)repd(u,min(j,siz[v]),1) w2[j]=(w2[j]+w2[j-u]*dp[v][u]%mod)%mod;
}
//rt1为根的选了i个(不算rt1),rt2为根的选了i+1个(算上rt2)
repd(i,siz[rt2]-1,0) ans=(ans+w2[i]*dp[rt2][i+1]%mod)%mod;
return ans;
}
void init(int n)
{
repi(i,0,n) head[i]=w2[i]=0;
repi(i,0,n)repi(j,0,n) dp[i][j]=g[i][j]=0;
tote=0;
}
int main()
{
int T,cnt=1; si(T);
while(T--)
{
int n; si(n); init(n);
repi(i,1,n-1){
int u,v; si(u),si(v);
add_edge(u,v),add_edge(v,u);
}
mxsiz=inf,totsiz=n,get_barycenter(1,0);
int ans=0;
if(k==1) ans=solve_1();
else ans=solve_2();
printf("Case %d: %d\n",cnt++,ans%mod);
}
return 0;
}