题目描述
题目分析
首先我们来分析暴力做法,此时最大需要遍历(n=20)20个顶点的排列方式,总共计算的次数为20!,数量级远远大于10^8,显然是不合理的。
此时,我们可以对上述dfs遍历的众多情况进行剪枝,去掉没有用的情况。
假设有4个顶点,1,2,3,4,5,有以下若干路径
1--2--3--4--5 路径总长度为18
1--3--2--4--5 路径总长度为20
......
可以发现比如对于第四个点,在这个点之前经过的其他点的顺序可以是无关紧要的,这里的两条路径都是经过了1,2,3只不过顺序不同导致最终的路径总长度不同,所以对于经过一个特定的点集的路径,只需要知道并存储这种情况之下的最短路径即可。比如说上述,设点集A={1,2,3},令m[A,4]代表已经经过点集A,并且终点落在第四个点的所有路径。m的表示法是最开始暴力思想的体现,即存储所有的路径,但是经过刚才的分析,进行剪枝操作,可以令f[A,4]代表已经经过点集A,并且终点落在第四个点的最小路径长度,这也是本题用到的状态表示。
下面的问题是:如何表示点集A?
可以使用经典的二进制状态压缩操作:不再阐述统一的定义,直接具几个状态表示的实例,我相信就能清楚了:
f[11,2]:11的二进制表示为1011,所以代表已经经过第1,2,4个点并且终点为2的最短路径长度
f[7,2]:7的二进制表示为111,所以代表已经经过第1,2,3个点并且终点为2的最短路径长度
f[17,5]:7的二进制表示为10001,所以代表已经经过第1,5个点并且终点为5的最短路径长度
有了状态表示,状态计算也非常简单:
f[i,j]=min(f[i-{j},k]+weight[k,j]) k=1~n
所以在代码中最外层遍历不同的点集情况,最多为2^20,,第二层遍历不同的终点,20种情况,第三层遍历不同的中间结点,20种情况,总时间最多为4*10^8
注意第一层遍历和第二层遍历需要加一个判断条件:if(i>>j&1)判断点集i中是否包含点j
第二层遍历和第三层遍历也需要加一个判断条件:if(i>>k&1)判断点集i中是否包含点k,为什么不是判断点集i-{j}中是否包含k呢,因为j==k的时候,对整个结果没有影响
代码
#include<iostream>
#include<cstring>
using namespace std;
const int N=20,M=1<<N;
int weight[N][N];
int f[M][N];
int n;
int main()
{
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>weight[i][j];
memset(f,0x3f,sizeof f);
f[1][0]=0;
for(int i=1;i<1<<n;i++)
for(int j=0;j<n;j++)
if(i>>j&1)
for(int k=0;k<n;k++)
if(i>>k&1)
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+weight[k][j]);
cout<<f[(1<<n)-1][n-1];
return 0;
}