AcWing 1125 牛的旅行

题目描述:

农民John的农场里有很多牧区,有的路径连接一些特定的牧区。

一片所有连通的牧区称为一个牧场。

但是就目前而言,你能看到至少有两个牧区不连通。

现在,John想在农场里添加一条路径(注意,恰好一条)。

一个牧场的直径就是牧场中最远的两个牧区的距离(本题中所提到的所有距离指的都是最短的距离)。

考虑如下的两个牧场,每一个牧区都有自己的坐标:

1.png

图 1 是有 5 个牧区的牧场,牧区用“*”表示,路径用直线表示。

图 1 所示的牧场的直径大约是 12.07106, 最远的两个牧区是 A 和 E,它们之间的最短路径是 A-B-E。

图 2 是另一个牧场。

这两个牧场都在John的农场上。

John将会在两个牧场中各选一个牧区,然后用一条路径连起来,使得连通后这个新的更大的牧场有最小的直径。

注意,如果两条路径中途相交,我们不认为它们是连通的。

只有两条路径在同一个牧区相交,我们才认为它们是连通的。

现在请你编程找出一条连接两个不同牧场的路径,使得连上这条路径后,所有牧场(生成的新牧场和原有牧场)中直径最大的牧场的直径尽可能小。

输出这个直径最小可能值。

输入格式

第 1 行:一个整数 N, 表示牧区数;

第 2 到 N+1 行:每行两个整数 X,Y, 表示 N 个牧区的坐标。每个牧区的坐标都是不一样的。

第 N+2 行到第 2*N+1 行:每行包括 N 个数字 ( 0或1 ) 表示一个对称邻接矩阵。

例如,题目描述中的两个牧场的矩阵描述如下:

  A B C D E F G H 
A 0 1 0 0 0 0 0 0 
B 1 0 1 1 1 0 0 0 
C 0 1 0 0 1 0 0 0 
D 0 1 0 0 1 0 0 0 
E 0 1 1 1 0 0 0 0 
F 0 0 0 0 0 0 1 0 
G 0 0 0 0 0 1 0 1 
H 0 0 0 0 0 0 1 0

输入数据中至少包括两个不连通的牧区。

输出格式

只有一行,包括一个实数,表示所求答案。

数字保留六位小数。

数据范围

1≤N≤150,
0≤X,Y≤10^5

输入样例:

8
10 10
15 10
20 10
15 15
20 15
30 15
25 10
30 10
01000000
10111000
01001000
01001000
01110000
00000010
00000101
00000010

输出样例:

22.071068

分析:

首先,详细的介绍下Floyd算法。众所周知,Floyd算法一般用于求解多源汇最短路问题,即可以求出图中任意两点间的最短距离。Floyd的时间复杂度是O(n^3),用到的思想是动态规划,其状态转移方程为f[i][j] = min(f[i][j],f[i][k] + f[k][j]),看起来这个结论很显然,枚举中间的节点k来尝试去更新i到j之间的最短距离。因为Floyd算法实现的简洁,加上思想也没有dijkstra这类算法复杂,大部分人在最初学习Floyd算法时便潜意识的将它视为一个简单算法了,但是等深入学习了动态规划后,发现Floyd算法的结论似乎没有大家认为的那么显然。在很多介绍Floyd的算法书中,都会说到:Floyd的三层循环一定要把k的遍历放在最外层,却没有深入的探讨其中的原因。因为如果用最初的想法去理解Floyd算法,就是枚举中间点k来更新i到j之间的最短距离,实现的话也应该是将k放在最内层啊,为什么一定要将k放在最外层呢?

一个DP问题的解决需要考虑状态表示、状态转移方程,以及边界状态和状态转移的顺序,每一步都是需要严格的推理的,并不是看着一个状态转移方程似乎合理就认为这种解法是对的。假设我们不知道Floyd算法,现在要用动态规划的思想去解决最短路问题。状态表示f[i][j]表示i到j的最短距离,那么我们一般有两种思路去推出状态转移方程:减而治之与分而治之。减而治之的思想就是将规模为n的原问题分解为规模为n-1的子问题和一个平凡的子问题,应用到最短路问题上就是枚举i到j的最短路径上经过的最后一个顶点,设k是与j相邻且能够到达j的顶点,则f[i][j] = min(f[i][j],f[i][k] + f[k][j]),虽然状态转移方程与Floyd的一致,但是k的含义却不相同,这里k与j间是存在直接相连的边的,f[k][j]就等于k到j的边权,问题就转化为了求i到k之间最短路问题。这种做法看起来也是正确的,但是在做状态转移时我们要先确保f[i][k]已经是最小的了,而并没有好的办法按照所谓规模从小到大去求解最短路,也就是状态转移的方向很复杂。分而治之的思想就是将原问题分解为两个规模相当的子问题,就类似于最初理解的Floyd算法,找个任意的中间k,去更新i到j的最短距离,将原问题分解为f[i][k]和f[k][j]这两个子问题。在线性DP中,分而治之的思想比较经典的就是石子合并问题,在n堆排成一条直线的石子中,先合并1到k堆,再合并k + 1到n堆,最后将剩余的两大堆石子合并,然后在实现时需要根据len从小到大去枚举,从而正确的实现了状态的转移。图论问题中我们不能很好的去区分问题的规模,石子合并问题可以按照len从小打到大去转移状态,那么最短路问题中的len又是什么呢?不能按照规模从小到大去转移状态,Floyd算法的状态转移方程f[i][j] = min(f[i][j],f[i][k] + f[k][j])就不能认为是对的。

推出状态转移方程前需要进行状态的划分,比如枚举i到j路径上倒数第二个点的时候就是根据那个顶点的不同对状态进行划分。Floyd算法则是根据i到j的最短路径是否经过k来划分状态的,i到j有一条最短路径,这条路径要么经过k要么不经过k,这种划分不重不漏。既然一条路径经不经过k是两种不同的状态,那么我们需要在状态表示中增加一维来表示这条路径是否经过了k,很容易想到可以用f[st][i][j]表示从i到j并且经过的路径状态是st的最短路径长度,但是其实我们没有采取这种思路去表示状态。第k个点选与不选,其实类似于01背包问题中第k个物品装不装进背包。当时我们是用f[i][j]表示在前i个物品中选出总体积不超过j的物品的最大价值的,这里可以采取类似的表示,用f[k][i][j]表示i到j经过的顶点编号不超过k的最短路径,这就是Floyd算法中真正的状态表示。显然,状态转移方程为f[k][i][j] = min(f[i-1][i][j],f[k-1][i][k] + f[k-1][k][j]),如果没有经过第k个顶点,f[k][i][j] = f[k-1][i][j],如果经过了第k个顶点,则该路径一定可以划分为i到k的路径和k到j的路径这两部分,并且这两部分路径经过的顶点一定没有相同的,否则路径长度就不是最短的了,f[k][i][j]表示i到j的路径中经过的顶点编号都不超过k,划分为i到k和k到j两部分后,这两部分中间肯定也不会有k顶点,否则就变成了i到k在到k,或者是k到k再到j这种明显不是最短路径的表述了,从而推出i到k经过的顶点编号都小于k,k到j经过的顶点编号也都小于k,于是i到j经过k点时f[k][i][j] = f[k-1][i][k] + f[k-1][k][j],这就得到了上面的状态转移方程。

观察f[k][i][j] = min(f[i-1][i][j],f[k-1][i][k] + f[k-1][k][j])发现第k层的状态只用到了第k-1层的状态,那么是否可以使用滚动数组实现呢?在几种背包问题中,如果只用到了上一层左边的状态,我们就要倒着转移第i层的状态;如果只用到了上一层右边的状态,我们需要按顺序去转移第i层的状态,从而防止需要用到的状态被覆盖,但是这里我们似乎看不出[i][j]与[i][k]和[k][j]之间的大小关系,是否就不能使用滚动数组实现了呢?实际上,f[k][i][k]与f[k-1][i][k]是恒等的,因为i到k最短路径中间肯定不会经过k点,同理f[k][k][j]与f[k-1][k][j]也是恒等的,这说明了第k层状态第二三维带k的位置与第k-1层同样位置的状态始终是相等的,因此不会发生需要使用的状态被覆盖的情况,所以可以使用滚动数组,去掉第一维,得到f[i][j] = min(f[i][j],f[i][k] + f[k][j]),这才是Floyd状态转移方程真正的推导过程,k本来表示第一维状态,所以在状态转移时要放在最外层。在DP问题的状态表示中,我们常常用f[i]表示前i个物品的状态,Floyd正是使用了这种思想巧妙地表示出了经不经过第k个顶点,隐式的给图的顶点确定了一个线性顺序,从而方便了我们进行状态转移。

现在开始讲解本题的求解思路。题意大概是说一个连通块中相距最远的两点间的距离被称为这个连通块的直径,我们需要给两个不同的连通块间连条线,从而使得连成的新的连通块的直径最小。有个地方比较绕,题目给的可能并不止两个不同的连通块,我们需要选两个连通块连上线,然后使得生成的新的连通块与其它连通块中最大的直径最小。一个顶点就是一个牧区,若干个顶点构成的连通块被称为一个牧场。

首先有一点需要明确,给两个原本不连通的牧场连上一条线,生成的新牧场的直径一定不会比之前两个牧场的直径小,因为原牧场距离最远的两点的距离并未被改变。我们可以暴力的枚举要连接的两个牧场的两点,连线后如果牧场的直径变大了一定会结果连上的那条线,所以提前求出各个牧场中每个点到其他所有点最小距离的最大值即可,也就是求同一个牧场中离这个点最远点的距离,任意两点间的距离就使用Floyd算法求。具体在实现时,我们只需要任选两个不连通的点,连线求生成的新牧场群中最大的直径,然后在各种可能中求出一种连线方式使得牧场群中最大的直径最小。

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 152;
const double INF = 1e20;
typedef pair<int,int> PII;
PII q[N];
char g[N][N];
double d[N][N],maxd[N];
double get_dist(PII a,PII b){
    int x = a.first - b.first,y = a.second - b.second;
    return sqrt(x * x + y * y);
}
int main(){
    int n;
    cin>>n;
    for(int i = 0;i < n;i++)    cin>>q[i].first>>q[i].second;
    for(int i = 0;i < n;i++)    cin>>g[i];
    for(int i = 0;i < n;i++){
        for(int j = 0;j < n;j++){
            if(i == j)  d[i][j] = 0;
            else if(g[i][j] == '1') d[i][j] = get_dist(q[i],q[j]);
            else    d[i][j] = INF;
        }
    }
    for(int k = 0;k < n;k++)
        for(int i = 0;i < n;i++)
            for(int j = 0;j < n;j++)
                d[i][j] = min(d[i][j],d[i][k] + d[k][j]);
    double res1 = 0,res2 = INF;
    for(int i = 0;i < n;i++){
        for(int j = 0;j < n;j++){//求i到离i最远点的距离
            if(d[i][j] < INF / 2)   maxd[i] = max(maxd[i],d[i][j]);
        }
        res1 = max(res1,maxd[i]);
    }
    for(int i = 0;i < n;i++){
        for(int j = 0;j < n;j++){//连线操作,更新新牧场直径
            if(d[i][j] > INF / 2)   res2 = min(res2,maxd[i]+maxd[j] + get_dist(q[i],q[j]));
        }
    }
    printf("%.6lf\n",max(res1,res2));
    return 0;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值