731.毕业旅行问题
731. 毕业旅行问题 - AcWing题库 |
---|
难度:中等 |
时/空限制:3s / 128MB |
总通过数:2738 |
总尝试数:4614 |
来源: 今日头条2019笔试题 |
算法标签 状态压缩DP |
题目内容
小明目前在做一份毕业旅行的规划。
打算从北京出发,分别去若干个城市,然后再回到北京,每个城市之间均乘坐高铁,且每个城市只去一次。
由于经费有限,小明希望能够通过合理的路线安排尽可能的省些路上的花销。
给定一组城市和每对城市之间的火车票的价钱,找到每个城市只访问一次并返回起点的最小车费花销。
注意:北京为 1 号城市。
输入格式
第一行包含一个正整数 n,表示城市个数。
接下来输入一个 n 行 n 列的矩阵,表示城市间的车票价钱。
输出格式
输出一个整数,表示最小车费花销。
数据范围
1<n≤20,包括北京
车票价格均不超过 1000 元。
输入样例:
4
0 2 6 5
2 0 4 4
6 4 0 2
5 4 2 0
输出样例:
13
说明
共 4 个城市,城市 1 和城市 1 的车费为 0,城市 1 和城市 2 之间的车费为 2,城市 1 和城市 3 之间的车费为 6,城市 1 和城市 4 之间的车费为 5,以此类推。
假设任意两个城市之间均有单程票可买,且价格均在 1000 元以内,无需考虑极端情况。
题目解析
城市之间两两互通,是个完全图,但不一定对称
n严格大于1,不超过20
跟hamilton路径是一样的,先求一下路径,再枚举一下从哪个点回到北京就可以了
DP
从两个角度来考虑
1. 状态表示
f[i][j]
,两维,第一维是i,一般是二进制压缩的状态
如果n是5的话,会用一个5位的二进制数来表示11011
0~31可以遍历5位二进制数,每一位是从0到第4位,分别表示0,1,2,3,4这几个城市有没有被遍历过,如果0号位是0,说明0号点没有被遍历过
把城市编号从1n变为0n-1,由于0号城市一定是包含在里面的,所以最后一位一定是1才可以
第0位是1的话,表示第0个城市已经被遍历过了;
- 第1个城市是1,表示被遍历过了;
- 第2个城市是0,表示没有遍历过;
- 依次类推
i就是一个压缩之后的状态
j表示遍历完i当中所有的城市之后,最后位于哪个城市
从两个角度来看
- 表示的是哪个集合,当前已经遍历过的城市的集合是i,(i是一个二进制压缩的状态,每一位01表示每一个城市有没有被遍历过),且最后处于城市j的所有的方案的集合
- 考虑
f[i][j]
存的是哪个属性,这个题要求的是最小花费。属性就是集合中每个方案花费的最小值
只要能把所有的状态求出来,答案是要枚举一下回北京之前最后一个到达的城市是哪个
- 如果是从1回到0
f [ 11111 1 2 ] [ 1 ] + W 1 , 0 f[111111_{2}][1] + W_{1,0} f[1111112][1]+W1,0
表示遍历完所有城市,最后位于1,在从1回到0的所有花费最小值 - 从2号城市回到0的话
f [ 11111 1 2 ] [ 2 ] + W 2 , 0 f[111111_{2}][2] + W_{2,0} f[1111112][2]+W2,0 - 依次类推
所以只需要将所有状态求出来之后,最后枚举一下回北京的城市是哪个,求一个最小值就可以了
2. 状态计算
如何求这个状态
对应集合的划分
假设f[i][j]
表示一个所有情况的集合,如果想要求这个集合当中花费最小值的话,一般是对这个集合进行划分,将它划分成若干的不重不漏的子集,在每个子集当中分别求最小值,再整体取一个min,就可以得到整个集合最小值
划分的时候一般是找最后一个不同点
最后一个点j都是一样的,但是倒数第二个点不知道是谁
可以枚举一下倒数第二个点是哪个点
倒数第二个点可以是从0~n-1号点,某一类不一定存在,如果当前这个集合只遍历过0的话,那么倒数第二个点一定不可能是1,如果这个集合不存在的话,这个集合就是空集,接下来只要求每一个子集的最小值,整体取一个min就可以了
为了不失一般性,看其中某一个子集,比如第k个子集,要求第k个子集的最小值,关键是看第k个子集的含义是什么,就是先从0号点,走过中间的一些点之后,最后走到了第k个点,最后再从第k个点走一条边,走到第j个点,并且走完之后整个路线的走过的城市的集合的二进制表示是i
如何求集合最小值
最后一步的花费都是一样的,都是 W k , j W_{k,j} Wk,j,是不变的
前面的花费可以变,如果想要整个花费最小的话,应该让变化的部分去最小
如何求前面的部分最小
从路线的含义出发去考虑,前面变化的部分表示所有从0最终走到k,并且遍历过的城市的集合是i去掉j的集合,所有这样的路线的最小值
可以带入集合表示,它恰好就是f[i去掉j][k]
,这个式子里面存的就是最小值
整个的最小值就是前面的最小值加上
W
k
,
j
W_{k,j}
Wk,j
f
[
i
去掉
j
]
[
k
]
+
W
k
,
j
f[i去掉j][k]+W_{k,j}
f[i去掉j][k]+Wk,j
DP问题的一般分析思路
- 状态表示不是凭空想出来的,而是通过经验看出来的,之前如果见过类似的状态表示,就可以想出来,如果没见过很难凭空想出来
- 有了状态表示之后,要把这个集合描述清楚,后面推导的时候,跟集合的定义是息息相关的,最后求答案的时候,跟集合的定义也是息息相关的
- 考虑状态的定义的时候要考虑两个方面
- 表示的是哪一堆方案的集合
- 存的这个值是集合的哪个属性
- 最大值
- 最小值
- 数量
- 布尔值
- 状态计算对应的是集合的划分
- 有些问题比较复杂,很难划分成不重不漏的集合,这个集合只要不漏就可以,重复是没有关系的
- 比如求最小值,求一个并集的最小值等于求左边集合的最小值和右边集合的最小值取一个min
- 如果问的是方案数的话,集合之间就不能有重合
时间复杂度等于
状态的数量
2
n
∗
n
2^n*n
2n∗n
状态转移,每一个状态最多划分成n个子集
所以整个问题的计算量
2
n
∗
n
2
2^n*n^2
2n∗n2,大概是4亿
常数优化
-
因为是从0号点出发的,所以所有状态都必然要包含0,所以最后一位必须是1才可以,是0的话就无意义
所以有效状态数只有真实状态数的一半 -
在枚举的时候,有些子集可能是不存在的,比如i是11010,j如果是2的话,可以发现i当中不包含j,遍历完所有的i当中的城市,是不可能处于j的,所以其中有很多状态其实是无效的,如果是无效状态的话,就不需要枚举去计算
也可以少掉很多的计算过程
所以整体来看计算量,应该小于等于2亿,常数也不是很大,可以过
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 1 << N, INF = 0x3f3f3f3f;
int n;
//w表示每对城市之间的距离,f表示状态
int w[N][N],f[M][N];
int main()
{
//先读入城市的数量
scanf("%d", &n);
//读入n*n的矩阵,表示每对城市之间的距离
for (int i = 0; i < n; i ++)
for (int j = 0; j < n; j ++)
scanf("%d", &w[i][j]);
//清空一下数组,把所有状态初始化为正无穷,表示这个状态不存在
memset(f, 0x3f, sizeof f);
//初始时遍历0号点,并且最后处于0号点
f[1][0] = 0; //初始状态,从北京出发,还没有花钱
//枚举一下所有状态
//由于从0号点出发,0号点必然在集合当中,所以状态表示的个位必须是1才可以,遍历的时候可以跳过所有末位不是1的数
for (int i = 1; i < 1 << n; i += 2)
for (int j = 0; j < n; j ++)
//求i的二进制表示的第j位是不是1
if (i >> j & 1)
//枚举一下状态的每个子集
for (int k = 0; k < n; k ++)
//判断一下i当中去掉j之后的点的集合是不是包含k
if ((i - (1 << j)) >> k & 1)
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
int res = INF;
//枚举一下最后从哪个城市回到0号点
for (int i = 1; i < n; i ++)
res = min(res, f[(1 << n) - 1][i] + w[i][0]);
printf("%d\n", res);
return 0;
}
1 << n 就是2的n次
1 << 1 = 1*2
1 << 2 = 1*2*2
二进制中,每左移一位就是乘2