树的直径 学习笔记
树的直径
define:树上最长链
solution:
1.树形dp
状态:d[x],表示x到达以x为根子树的最远距离
转移: ans=max(ans,d[x]+d[y]+edge[i]);d[x]=max(d[x],d[y]+edge[i])
注意 ans的更新 :因为转移顺序是底到根更新,走到yi时,d[x]已经储存了d[yj]的信息,并且,没有储存d[yi],则可用d[x]+d[yi]+e[x,yi]更新;
复杂度O(n)
优势:好写
缺点:难以记录路径
void dp(int x){ v[x]=1; for(int i=head[x];i;i=ver[i]){ int y=ver[i]; if(v[y])continue; dp(y); ans=max(ans,d[x]+d[y]+edge[i]); d[x]=max(d[x],d[y]+edge[i]); } }
2.两边bfs/dfs
过程:first:任选一点x为根,bfs/dfs,更新到x的最远路径,记录到达点p
second:以点p为根,bfs/dfs,更新到p的最远路径,记录到达点q,则从p到q即为树的直径
证明:可以考虑反证法
假设此树的最长路径是从s到t,我们选择的点为u。反证法:假设搜到的点是v。
1、v在这条最长路径上:dis[u,v]>dis[u,v]+dis[v,s],显然矛盾。
2、v不在这条最长路径上:设点p在e(s,t)上:
dis[u,v]>dis[u,p]+dis[p,t];
dis[s,v]=dis[s,p]+dis[p,u]+dis[u,v];
dis[p,u]+dis[u,v]>dis[p,t];
dis[s,v]>dis[s,p]+dis[p,t]=dis[s,t]
dis[s,v]>dis[s,t],矛盾。
复杂度O(n)
优势:方便记录路径(记录路径,可以在第一遍bfs、dfs中记录前继来记录,而dp却显不出这样的的优势)
缺点:代码较长,难写
int bfs(int s){ memset(d,0x3f,sizeof(d)); d[s]=0; pre[s]=0; q.push(s); while(!q.empty()){ int x=q.front(); q.pop(); for(int i=head[x];i;i=nxt[i]){ int y=ver[i]; if(d[y]==0x3f3f3f3f){ d[y]=d[x]+edge[i]; pre[y]=i; q.push(y); } } } int y=1; for(int x=1;x<=n;x++){ if(d[x]>d[y])y=x; } return y; }
主程序中:
int p; p=bfs(1);//任意一点 p=bfs(p); ans=d[p];
例题:
APIO 2010(巡逻)
题面:给定一棵树:要求加k条边(边权为1),使得,从1开始走并回到1,当经过了所有点,走的路径最短;(1<=k<=2)
本实际上 加上一些边使部分边(L长)连成环,则可以少走(L-1)路径长
so:找到树上最长链即树的直径,可以使路径最短
当k=1,可以直接输出2(n-1)-(L-1)
但是当k=2,考虑到可能有重合,我们可以在第一遍找直径时,将此直径所有的点标记为-1,可以避免环重合
因此:因为第一遍我们需要记录直径路径,故使用两边bfs,将直径上的路径处理为-1;之后即可以树形dp一遍
代码如下
#include<cstdio> #include<iostream> #include<cstring> #include<queue> using namespace std; const int MAXX=100005; queue<int>q; int head[MAXX],ver[MAXX*2],edge[MAXX*2],nxt[MAXX*2]; int d[MAXX],pre[MAXX],f[MAXX]; bool v[MAXX]; int n,k,p,tot=1,ans; void add(int x,int y,int z){ ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot; edge[tot]=z; } int bfs(int s){ memset(d,0x3f,sizeof(d)); d[s]=0; pre[s]=0; q.push(s); while(!q.empty()){ int x=q.front(); q.pop(); for(int i=head[x];i;i=nxt[i]){ int y=ver[i]; if(d[y]==0x3f3f3f3f){ pre[y]=i; d[y]=d[x]+edge[i]; q.push(y); } } } int x,y; for( x=y=1;x<=n;x++){ if(d[x]>d[y])y=x; }//找到端点 return y; } void dp(int x){ v[x]=1; for(int i=head[x];i;i=nxt[i]){ int y=ver[i]; if(v[y])continue; dp(y); ans=max(ans,f[x]+f[y]+edge[i]); f[x]=max(f[x],f[y]+edge[i]); } } void change(){ for(;pre[p];p=ver[pre[p]^1])edge[pre[p]]=edge[pre[p]^1]=-1; //取反操作,ver[pre[p]^1],实际上是反边的终点,也就是p的前继 //因为tot=1;++tot,边从2开始记录,2^1=3,即2,3互为反边 } int main(){ cin>>n>>k; for(int i=1;i<=n-1;i++){ int x,y; cin>>x>>y; add(x,y,1); add(y,x,1); } p=bfs(1); p=bfs(p); int l1=d[p]; if(k==1){ cout<<2*(n-1)-(l1-1)<<endl; }else { change(); dp(1); cout<<2*(n-1)-(l1-1)-(ans-1)<<endl; } return 0; }