【备战蓝桥杯国赛-国赛真题】补给
题目地址:补给
题目描述
样例和数据范围可以点击上方地址查看。
思路
本题的数据范围为20,数据范围很小,读完题有写爆搜的冲动,但是进一步思考过后会发现,爆搜的参数很难定义下来,因为我们访问的每个村庄的次数不限,所以我们写爆搜的时候没有很好的方式来支持我们去搜索的下一个村庄是哪一个。转变思路,寻找正解。
有过算法学习经历的同学应该很清楚,这题实际上是一道旅行商问题,是np-hard的,所以我们的解决方案也就确定了,是动态规划,并且是状态压缩的动态规划,我们定义数组f[1 << 20][20]:其中f[i][j] : i为已经走过的点的集合,j为最后一个到达的点,i是一个数,怎么就是一个集合了呢,有些同学不禁会问,其实i就代表了唯一状态的数,只是我们评定的标准是i的二进制中0和1的情况,0代表着该位没有被选过,1代表着该位被选过,在本题就是某个村庄是否被访问过。这类状态压缩的动态规划计算方式非常固定和简单,状态计算不再讲了。
我们还需要考虑一点,就是直升机一次只能飞不超过d的距离,所以每个城市之间的距离我们必须得知道,如果超过d,即不可到达,我们设置位无穷大INF(infinity),之后我们绝需要求任意两个村庄之间的最短距离,因为题目要求我们的答案尽可能的小,为了满足这一点,我们可以使用floyd,来求出任意两个点之间的最短距离。
最后,代码的dp部分和最后求解答案的部分需要由一定的二进制基础和理解f数组的含义。
代码(C++)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 25;
const double INF = 1e12;
int n, d;
PII p[N];
double dist[N][N];
double f[1 << 20][20]; // f[i][j] : i为已经走过的点的集合,j为最后一个到达的点
double get(PII a, PII b) {
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
void floyd() {
for(int k = 0; k < n; k ++) {
for(int i = 0; i < n; i ++) {
for(int j = 0; j < n; j ++) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
int main() {
scanf("%d%d", &n, &d);
for(int i = 0; i < n; i ++) scanf("%d%d", &p[i].x, &p[i].y);
for(int i = 0; i < n; i ++) {
for(int j = 0; j < n; j ++) {
double d_ = get(p[i], p[j]);
if(d_ <= d) dist[i][j] = d_;
else dist[i][j] = INF;
}
}
floyd();
for(int i = 0; i < 1 << n; i ++)
for(int j = 0; j < n; j ++)
f[i][j] = INF;
f[1][0] = 0;
for(int i = 0; i < (1 << n); i ++) {
for(int j = 0; j < n; j ++) {
if(i >> j & 1) {
for(int k = 0; k < n; k ++) {
if(k != j && i >> k & 1) {
f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + dist[k][j]);
}
}
}
}
}
double res = INF;
for(int i = 0; i < n; i ++) {
res = min(res, f[(1 << n) - 1][i] + dist[i][0]);
}
printf("%.2lf\n", res);
return 0;
}