虽然有很多Dalao发过很多非常详细的题解,但是本蒟蒻还是(凭着初生牛犊不怕虎的精神)来发一篇题解。
因为最近正在复习原来学习的图论,正巧(一点也不巧)看到了这道题,然后就……点了进来。
其实刚看到这道题的时候,还是有一点懵的(当然,这是一道图论是没得跑的)。 然后,我就从头理了一下思路。
首先:
1、该图上的所有道路都要到达。
2、所有操作都是从一号点开始,一号点结束。
3、要使得巡逻距离较短。
第一步
我们先考虑不加边的情况
从一号点出发,要把每个边遍历一遍再回到1号点,会恰好经过每条边2次,总路线哦不,是经过的路线总长为2(n-1)。
第二步
我们来考虑 k==1 的情况。
建立一条新边,因为每一次新建的边都要。
恰好 经过一次
因为这是一棵树,所以当我们加上一条边时就构成了一个环,而这个环便可以使这个图在遍历时可以减少重复走的边。
如图(不是很非常丑):
因为这个环所带来的重复路径的减少,所以我们在构建环时要找到此时树中最长的链进行构造,这样的话我们就可以满足题上的最短解,这个时候我们来推一下公式:
2(n-1)-L+1
如果你已经想到这里,那么恭喜你,你已经成功地拿到了30分。下面我们来看如何AC这道题。
第三步
第一条路建完以后,我们来考虑第二条路。当第二条新路(u,v)建立之后,又会形成一个环。当连个环不重叠的时候,答案会一直缩小。但是如果有重叠部分,就会有一部分的路不会被巡逻到,所以我们就只好将车重新巡逻这条边,最后返回。这步操作最终又使那条边重复经过了一次。
具体见图:
这样的话,我们就得到了最短的路径。
总结一下具体的步骤:
1、先在原树上找到该树的直径,记为L1,然后将边权改为-1(后面会解释为什么是-1)。
2、在取反边权后再次寻找该树的直径记为L2。
3、计算答案。
公式
由前面的讲解可推知:
ans=2(n-1)-(L1-1)-(L2-1) 即 ans=2n-L1-L2
时间复杂度的话就十分的直观为O(n)。
前面的思路就讲解到这里了,下面我们来讲一讲代码实现。
首先
这道题的一个最直观的考点——树的直径
所以下面就介绍一下树的直径这个考点。
树的直径简单来说就是树中最长的链,下面将有两种O(n)的方法来求树的直径。
背景:假设树以N个节点N-1条边以无向图的形式给出并以邻接表的形式给出。
第一种:树形DP求树的直径
我们用d[x]表示距离,f[x]则表示任意两点之间的距离。
代码实现:
void dp(int x){
v[x]=1;
for(int i=head[x];i;i=e[i].next){
int y=e[i].to;
if(v[y])continue;
dp(y);
ans=max(ans,dis[x]+dis[y]+e[i].w);
dis[x]=max(dis[x],dis[y]+e[i].w);
}
}
用树形dp来求解的话,一个十分明显的优点就是可以处理负权制的问题
第二种:两次BFS求树的直径
通过两次BFS求出直径,更易算出直径上的直径上的具体节点。
步骤 :
①从树上任意一点出发P,找到与其距离最大的点M
②从点M出发,找到与其距离最大的点N
③MN即为树的直径
下面是具体证明(证明过程摘自老师ppt)。
反证法:假设M不是直径的一个端点,AB是树的直径。
① 如果P是直径上的点,如图,PM>PB则AP+PM>AP+PB=AB这与AB是直径矛盾。
②-1 若P不在直径上:P到M路径与A到B路径有公共结点TPT+TM>PT+TB,则TM>TB,故AT+TM>AT+TB=AB,矛盾
②-2 P到M的路径与A到B的路径无公共结点PC+CM>PC+CD+BD,则CM>CD+BD,CM+CD>BD故CM+CD+AD>BD+AD=AB,矛盾。
这样两边BFS的正确性就证明完毕。
代码:
1 void bfs(int x,int ck){ 2 memset(dis,0,sizeof(dis)); 3 memset(vis,0,sizeof(vis)); 4 queue<int >q; 5 q.push(x); 6 vis[x]=1; 7 while(!q.empty()){ 8 int now=q.front(); 9 q.pop(); 10 for(int i=head[now];i;i=e[i].next){ 11 int y=e[i].to; 12 if(!vis[y]){ 13 vis[y]=1; 14 dis[y]=dis[now]+e[i].w; 15 q.push(y); 16 if(ck)f[y]=now;//记录父节点,修改时会用 17 } 18 } 19 } 20 for(int i=1;i<=n;i++){ 21 if(ans<dis[i])ans=dis[i],point=i; 22 } 23 }
两遍BFS求树的直径的优点很明显,但是缺点也很突出,就是无法处理负权值。
所以我们在处理过第一次的直径后将权值赋为了-1,就是这个原因。
下面就是这道题的AC代码了
1 #include<bits/stdc++.h> 2 using namespace std; 3 template<typename type> 4 void scan(type &x){ 5 type f=1;x=0;char s=getchar(); 6 while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();} 7 while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();} 8 x*=f; 9 } 10 const int N=1e5+7; 11 struct node{ 12 int to,w,next; 13 }e[N*2]; 14 int head[N],cnt; 15 void add(int a,int b,int c){ 16 e[++cnt].to=b; 17 e[cnt].next=head[a]; 18 head[a]=cnt; 19 e[cnt].w=1; 20 } 21 int dis[N],vis[N],l1,l2,n,k,point,f[N],d[N],v[N]; 22 int ans=0; 23 void bfs(int x,int ck){ 24 memset(dis,0,sizeof(dis)); 25 memset(vis,0,sizeof(vis)); 26 queue<int >q; 27 q.push(x); 28 vis[x]=1; 29 while(!q.empty()){ 30 int now=q.front(); 31 q.pop(); 32 for(int i=head[now];i;i=e[i].next){ 33 int y=e[i].to; 34 if(!vis[y]){ 35 vis[y]=1; 36 dis[y]=dis[now]+e[i].w; 37 q.push(y); 38 if(ck)f[y]=now;//记录父节点,修改时会用 39 } 40 } 41 } 42 for(int i=1;i<=n;i++){ 43 if(ans<dis[i])ans=dis[i],point=i; 44 } 45 } 46 void change(int a){//修改边权值 47 while(f[a]){ 48 int fa=f[a]; 49 for(int i=head[fa];i;i=e[i].next){ 50 // e[i].w=-1; 51 if(e[i].to==a){ 52 // e[i].w=e[i^1].w=-1; 53 e[i].w=-1; 54 break; 55 } 56 } 57 for(int i=head[a];i;i=e[i].next){ 58 if(e[i].to==fa){ 59 e[i].w=-1; 60 break; 61 } 62 } 63 a=fa; 64 } 65 } 66 void dp(int x){ 67 v[x]=1; 68 for(int i=head[x];i;i=e[i].next){ 69 int y=e[i].to; 70 if(v[y])continue; 71 dp(y); 72 ans=max(ans,dis[x]+dis[y]+e[i].w); 73 dis[x]=max(dis[x],dis[y]+e[i].w); 74 } 75 76 } 77 78 int main(){ 79 scan(n);scan(k); 80 // scanf("%d%d",&n,&k); 81 for(int i=1;i<n;i++){ 82 int a;int b; 83 scan(a);scan(b); 84 // scanf("%d%d",&a,&b); 85 add(a,b,1); 86 add(b,a,1); 87 } 88 if(k==1){ 89 bfs(1,0); 90 bfs(point,0); 91 printf("%d",2*n-dis[point]-1); 92 }else{ 93 bfs(1,0); 94 bfs(point,1); 95 l1=dis[point]; 96 change(point); 97 memset(dis,0,sizeof(dis)); 98 memset(vis,0,sizeof(vis)); 99 ans=0; 100 dp(1); 101 // printf("%d ",l1); 102 // printf("%d ",ans); 103 printf("%d",2*n-ans-l1); 104 } 105 return 0; 106 }