[APIO2010] 算法竞赛竞赛经典 巡逻

原题链接

题目描述

在一个地区有 n 个村庄,编号为1,2,…,n。

有 n-1 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其他任一个村庄。

每条道路的长度均为1个单位。

为保证该地区的安全,巡警车每天都要到所有的道路上巡逻。

警察局设在编号为1的村庄里,每天巡警车总是从警局出发,最终又回到警局。

为了减少总的巡逻距离,该地区准备在这些村庄之间建立 K 条新的道路,每条新道路可以连接任意两个村庄。

两条新道路可以在同一个村庄会合或结束,甚至新道路可以是一个环。

因为资金有限,所以 K 只能为1或2。

同时,为了不浪费资金,每天巡警车必须经过新建的道路正好一次。

编写一个程序,在给定村庄间道路信息和需要新建的道路数的情况下,计算出最佳的新建道路的方案,使得总的巡逻距离最小。

输入格式

第一行包含两个整数 n 和 K。

接下来 n-1 行每行两个整数 a 和 b,表示村庄 a 和 b 之间有一条道路。

输出格式

输出一个整数,表示新建了 K 条道路后能达到的最小巡逻距离。

数据范围

\(3 \le n \le 100000\),
\(1 \le K \le 2\),
\(1 \le a,b \le n\)

输入样例:

8 1
1 2
3 1
3 4
5 3
7 5
8 5
5 6

输出样例:

11

解题报告

题意理解

有一颗\(n-1\)个节点的,警察局设在根节点,两个节点之间的距离固定\(1\),现在要求所有的节点都要至少访问一遍,可以访问多次.

猪八戒警察是个不愿意浪费多余时间的人死胖子,时间要花在睡觉吃饭上面,所以他只想要走最短的路.为什么胖,就是因为懒,贪吃

要致富先修路,于是上面派发了巨款修路,这笔巨款,居然可以添加一条路,或者两条路.巨款好多啊,全被猪八戒买东西吃掉了

不过有要求,那就是新修建的路,只准走一次,因为豆腐渣工程,不安全.


解题思路

题目分析

一道题目中,条件性质都是一座座宝藏,必须深入挖掘.才能成为yxc老师一般的人生赢家

  1. 题目的核心是什么?

偷懒走最少的路

  1. 换句话表示是什么?

不要走最长的路

  1. 什么是最长的路?

树的直径

  1. 树的直径是什么?

一棵树上,最远的两个点,他们的距离.

  1. 添加路径有什么效果?

使得两个点的最短距离变成\(1\).

  1. 当只能增加一条新的路径的时候,我们怎么最大化利润?

找到树的直径的两个端点,在他们中间添加一条新路径.

样例分析

巡逻1.png

这是我们样例1的模型.

让我们来求一下这棵树的树的直径

巡逻2.png

假如说我们将这条直径的两个端点连接的话,我们最多可以优惠多少路径长度呢?

巡逻3.png

性质是什么,就是从一个特殊例子,变化成为一个通解公式


性质总结

因此我们通过上面的每一步,得到了如下的这些重要性质.

  1. 刚开始我们一共要走的长度是:

\[ 路径总长度=2*(n-1)\\\\ n是这棵树上的节点个数 \]

因为对于每一个而言,我们必须要走两次. (你可以认为就像是DFS搜索一样)

第一次访问这个点,也就是进入这个点.

第二次访问这个点,也就是离开这个点.


  1. 假如说我们之前的树,它的树的直径长度\(L\),那么经过我们第一轮路径增加过后.

\[ 路径总长度-L+1 \\\\ 也就是2*(n-1)-L+1 \]

因为我们不必再走一遍树的直径了,所以我们减少了\(L\)的长度.

但是我们增加了一条边,所以我们增加了\(1\)的长度.

总而言之,言而总之,这就是我们对于增加一条边的性质总结.


重点思考

这道题目,不仅仅要让我们增加一条边,还有增加第二条边的可能.

假如说我们没有第一条边的建立,那么现在我们增加第二条边,其实所有步骤都是和上面建立第一条边是一样的.

  1. 第一条边建立后的影响

我们知道,树之所以是树,是因为它不会出现环.

但是现在一条边的建立,环它出现了.是他,是他,就是他,我们的小英雄一组环

因为出现了环,所以我们现在不能保证一个点,那就是

第二条边构造的B环会不会和第一条边构造的A环,有重叠部分.

  1. 假如说没有重叠部分

如果说没有重叠,那么第一条边和第二条边各自为政,井水不犯河水,我们可以看作是两次第一条边.

只需要把第一次步骤,重复两遍即可.

  1. 如果有重叠部分

我们现在要明确一点,为什么我们添加边,可以使得路径缩小?

因为对于树的直径上的所有点,我们只需要进入一次,并不需要再离开一次了.

  1. 但是重叠部分会导致什么呢?

对于一个节点而言,它是重叠部分上的点.

第一次,它被减少掉了一条边.

第二次,它再次被减少了一条边.

请问它还会被访问吗?

答案是不可能!因为我们一个节点,最多访问两次,而你减少了两次.

所以此时,我们的这些重叠部分上的点,从只需要访问一次,变成了需要访问两次.

因为我们必须去访问这些点,而访问了一次,又必须再离开一次,于是就是访问两次


算法流程
  1. 在最初的树上,求出树的直径\(L1\),然后将这条路径上的所有边权统统取反.
    \[ 1变成-1 \]

  2. 我们再求一次树的直径\(L2\).

  3. 答案就是
    \[ 2 \times (n-1)-(L1-1)-(L2-1) \\\\ = 2 \times (n-1)-L1+1-L2+1 \]

假如说\(L2\)\(L1\)有重叠部分.

那么当我们
\[ -L1+1 \]
的时候,我们就会发现,重叠的部分变成了只需要经过一次.

然后
\[ -L2+1 \]
相当于把重叠部分相加回来了.

此时变成了经过了两次.

假如说没有重叠部分,那么1,-1都不会有影响,反正我们不会经过这些边.


代码解析

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+20;
int n,k;
struct Edge
{
    int ver[2*N],edge[2*N],Next[2*N],head[N],dis[N],pre[N],f[N],v[N],tot,p,i,x,y,z;
    queue<int> q;
    void init()
    {
        memset(head,0,sizeof(head));
        tot=1;
    }
    void add_edge(int a,int b,int c)
    {
        edge[++tot]=b;
        ver[tot]=c;
        Next[tot]=head[a];
        head[a]=tot;
    }
    int bfs(int s)
    {
        int i,x,y;
        memset(dis,0x3f,sizeof(dis));
        q.push(s);
        dis[s]=pre[s]=0;
        while(q.size())
        {
            x=q.front();
            q.pop();
            for(i=head[x]; i; i=Next[i])
                if(dis[edge[i]]==0x3f3f3f3f)
                    dis[edge[i]]=dis[x]+ver[i],pre[edge[i]]=i,q.push(edge[i]);
        }
        for(x=y=1; x<=n; x++)
            if(dis[x]>dis[y])
                y=x;
        return y;
    }
    int diam()
    {
        p=bfs(1);
        p=bfs(p);
        return dis[p];
    }
    void change()
    {
        for(; pre[p]; p=edge[pre[p]^1]) ver[pre[p]]=ver[pre[p]^1]=-1;
    }
    void dp(int x)
    {
        v[x]=1;
        for(int i=head[x]; i; i=Next[i])
            if(!v[edge[i]])
            {
                dp(edge[i]);
                y=max(y,f[edge[i]]+f[x]+ver[i]);
                f[x]=max(f[x],f[edge[i]]+ver[i]);
            }
    }
    void work()
    {
        x=diam(),y=0,z=1;
        if(k==2)
            change(),dp(1),z=2;
        printf("%d\n",2*(n-1)-x-y+z);
    }
} g1;
int main()
{
//  freopen("stdin.in","r",stdin);
    scanf("%d%d",&n,&k);
    g1.init();
    for(int i=1; i<n; i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        g1.add_edge(a,b,1);
        g1.add_edge(b,a,1);
    }
    g1.work();
    return 0;
}
posted @ 2019-07-12 21:53 秦淮岸灯火阑珊 阅读(...) 评论(...) 编辑 收藏
levels of contents