题目链接:
题目大意:
给出计算两个城市之间cost的方法,输入n个城市的x,y,z坐标,求出从第一个城市遍历所有城市之后回到该城市的最小cost和
思路:
这个计算cost的方法有点特别,但是这不是题目的重点,不管计算方法如何复杂,任意两点间的cost都可以用一个公式算出来。
这题的重点有两个:1.要遍历所有城市 2.最后回到起点
怎么做呢?暴力回溯肯定会超时,贪心又做不了,那就首先想到dp
根据上面两个重点,dp需要是一个二维数组,一个坐标用来记录遍历的状态(用二进制表示,最右边的二进制数表示0号城市有没有遍历过,右边倒数第二个表示1号城市,以此类推),另一个用来记录当前终点,dp值用来记录当前的cost总和。
接下来思考递归起点,因为要遍历所有城市,然后回到起点,所以一开始要记为所有城市都没有遍历过,所以是dp[0][0] = 0,特别注意这里不能视为起点城市已经被遍历过,因为最后要回到这里,这样算的话就要到这里两次了,不符合二进制记录的规则。
然后是递推关系式,比如已经去过0,1,2,3号城市,现在要计算去过0,1,2,3,4号城市,并且以4号城市为终点的dp值,就用前面那个0,1,2,3的递推。注意它包含了4个有效的dp值,分别是以0,1,2,3为终点的情况,要找出最小的,就要都遍历一遍,找出最小的 (上一层dp值+当前cost) 。(对应下方代码中for(int k=0; k<n; k++)那一层循环)
在递推的过程中,还有可以剪枝的情况,如果没有遍历完所有城市,就回到了起点,这种情况显然不合题意,所以将其剪去(对应第二层for循环下第一个if语句)
最后dp[(1<<n)-1][0]就是要返回的结果啦。(它表示遍历完所有城市,最后终点是0号城市)
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include <cstring>
#include <set>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 17, M = (1 << 17) + 1; //用二进制位表示17个城市是否走过的状态(包括0城市)
int n; //城市数量
int x[N], y[N], z[N]; //储存城市三个坐标的数组
ll dp[M][N];
//dp[i][j]为状态i下从起点出发到j的最小花费,i用来表示走过那些城市,1表示走过,0表示没走过
//需要用i来记录1~n所有城市是否走过(用0~n-1总共n个二进制数来记录0~n-1号城市有没有遍历过)
ll cost(int a, int b){
//用来计算两城市之间的cost的函数,注意它隐含了一个城市到它自己的cost为0
return abs(x[a]-x[b]) + abs(y[a]-y[b]) + max(0, z[b]-z[a]);
}
int main(){
cin >> n;
for(int i=0; i<n; i++){ //输入n组数据(每个城市的坐标)
cin >> x[i] >> y[i] >> z[i];
}
memset(dp, 0x3f, sizeof(dp)); //开始时所有dp值记为INF
dp[0][0] = 0; //不去任何城市,就在0城市原地,cost是0,这是dp的起点(唯一一个0状态),那么dp[1][0]是多少呢,实际上遍历之后可以发现,dp[1][0]也是0,因为它等于dp[0][0]+cost(0,0)
for(int i=1; i<(1 << n); i++){ //遍历所有非0的状态(至少去过一个城市的状态)
for(int j=0; j<n; j++){ //遍历所有城市,查看当前状态下这个城市有没有被遍历过
if(i & 1 && i != (1<<n)-1) continue; //这里可以进行剪枝:没有遍历所有城市之前,提前回到0城市,肯定不合题意
if((i >> j) & 1){ //i的第j位(从0开始计数)是1,说明要更新这个状态下到j的距离最小值(它等于“上一个没有j的状态下,任意一个连通点距离+该点和j的cost”中的最小值)
for(int k = 0; k<n; k++){ //看似是遍历所有点,实际上只遍历了上一个不含j的状态连通的所有点,因为不连通的点dp值是INF,对结果无影响
dp[i][j] = min(dp[i][j], dp[i-(1 << j)][k] + cost(k, j)); //k是起点,j是终点,所以是cost(k, j)
}
}
}
}
printf("%lld\n", dp[(1 << n)-1][0]); //遍历所有城市之后,最后一个才遍历0城市的总cost
}