树的直径&&题解 【[APIO2010]巡逻】

虽然有很多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的正确性就证明完毕。
代码:

void bfs(int x,int ck){
    memset(dis,0,sizeof(dis));
    memset(vis,0,sizeof(vis));
    queue<int >q;
    q.push(x);
    vis[x]=1;
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i=head[now];i;i=e[i].next){
            int y=e[i].to;
            if(!vis[y]){
                vis[y]=1;
                dis[y]=dis[now]+e[i].w;
                q.push(y);
                if(ck)f[y]=now;//记录父节点,修改时会用
            }
        }
    }
    for(int i=1;i<=n;i++){
        if(ans<dis[i])ans=dis[i],point=i;
    }
}

两遍BFS求树的直径的优点很明显,但是缺点也很突出,就是无法处理负权值。
所以我们在处理过第一次的直径后将权值赋为了-1,就是这个原因。
下面就是这道题的AC代码了

#include<bits/stdc++.h>
using namespace std;
template<typename type>
void scan(type &x){
    type f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f;
}
const int N=1e5+7;
struct node{
    int to,w,next;
}e[N*2];
int head[N],cnt;
void add(int a,int b,int c){
    e[++cnt].to=b;
    e[cnt].next=head[a];
    head[a]=cnt;
    e[cnt].w=1;
}
int dis[N],vis[N],l1,l2,n,k,point,f[N],d[N],v[N];
int ans=0;
void bfs(int x,int ck){
    memset(dis,0,sizeof(dis));
    memset(vis,0,sizeof(vis));
    queue<int >q;
    q.push(x);
    vis[x]=1;
    while(!q.empty()){
        int now=q.front();
        q.pop();
        for(int i=head[now];i;i=e[i].next){
            int y=e[i].to;
            if(!vis[y]){
                vis[y]=1;
                dis[y]=dis[now]+e[i].w;
                q.push(y);
                if(ck)f[y]=now;//记录父节点,修改时会用
            }
        }
    }
    for(int i=1;i<=n;i++){
        if(ans<dis[i])ans=dis[i],point=i;
    }
}
void change(int a){//修改边权值
    while(f[a]){
        int fa=f[a];
        for(int i=head[fa];i;i=e[i].next){
                // e[i].w=-1;
                if(e[i].to==a){
                //    e[i].w=e[i^1].w=-1;
                   e[i].w=-1;
                   break; 
                }
        }
        for(int i=head[a];i;i=e[i].next){
            if(e[i].to==fa){
                e[i].w=-1;
                break;
            }
        }
        a=fa;
    }
}
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);
    }

}

int main(){
   scan(n);scan(k);
    // scanf("%d%d",&n,&k);
    for(int i=1;i<n;i++){
        int a;int b;
       scan(a);scan(b);
        // scanf("%d%d",&a,&b);
        add(a,b,1);
        add(b,a,1);
    }
    if(k==1){
        bfs(1,0);
        bfs(point,0);
        printf("%d",2*n-dis[point]-1);
    }else{
        bfs(1,0);
        bfs(point,1);
        l1=dis[point];
        change(point);
        memset(dis,0,sizeof(dis));
        memset(vis,0,sizeof(vis));
        ans=0;
        dp(1);
        // printf("%d\n",l1);
        // printf("%d\n",ans);
        printf("%d",2*n-ans-l1);
    }
    return 0;
}

希望管理大大通过一下^_^ 最后安利一波博客,希望大家关注一波。

猜你喜欢

转载自www.cnblogs.com/xishirujin/p/11103878.html
今日推荐