题目链接:点击这里
题意:求起点 到终点 的最短哈密顿路径。从题目样例来看,默认都是无向完全图(但是题目本身并未提及,坑)。也就是说,任意一点都可以直达其它顶点,完全不用担心从一个顶点到另一个顶点没有边,这对题意的理解很重要。
朴素做法,就是枚举n个点的全排列,计算路径长度取最小值,时间复杂度为 。
使用二进制状态压缩DP可以优化到
0 -> 1 -> 2 -> 3 -> … … -> n-1
0 -> 2 -> 1- > 3 -> … … -> n-1
假设现在已经知道0 -> 1 -> 2 -> 3是15,0 -> 2 -> 1- > 3是20,前者比后者更优,那么,3 -> … -> n-1 后面所有的路径状态都可以由前者替代。
从起点到当中任意一点都有一个最短路径,由上述分析可知,对于这些状态我们仅仅关注两个方面:
- 哪个点被用过
- 当前停在哪个点
因此,我们可以用
表示从起点
经过路径
走到顶点
的最短Hamilton路径长度。
其中路径
由一串二进制数表示,
的二进制数下哪一位为
则表示哪个点已经走过了。
状态转移方程:
。
其中
就是
去掉
之后的集合,并且
要包含
。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 1<<20;
int n;
int f[M][N], weight[N][N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
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; //起点0的状态
for(int i = 0; i < 1<<n; ++i)
{
for(int j = 0; j < n; ++j)
{
if(i >> j & 1) //i的第j位是否为1
{
for(int k = 0; k < n; ++k)
{
if(i - (1 << j) >> k & 1) //state_k就是state去掉j之后的集合,并且state_k要包含k
{
f[i][j] = min(f[i][j], f[i-(1<<j)][k] + weight[k][j]);
}
}
}
}
}
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}
参考链接:https://blog.csdn.net/qq_30277239/article/details/103992712
小优化:
- 既然 数组定义是从顶点 出发,那么状态二进制表示的最后一位必然是 ,也就是说只需要枚举 是奇数的情况,这样可以使得程序的执行速度快了一倍。
- 另外,由于路径 只是表示哪些顶点走过,并不能表示遍历的顺序,因此,我们在遍历每个状态时,还需要枚举当前遍历到的顶点。只有路径中仅包含两个顶点时才需要从 枚举 ,否则不需要。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 1 << 20;
int n;
int f[M][N], weight[N][N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
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; //起点0的状态
for(int i = 0; i < 1<<n; ++i)
{
if(i&1)
{
for(int j = 0;j < n;j++)
{
if(i >> j & 1) //枚举当前点
{
int t = i - (1 << j); //去掉当前点
int s = (t - 1 != 0); //如果只剩下了起点,才需要枚举以k = 0的情况
for(int k = s; k < n; ++k)
{
if(t >> k & 1) //以k为倒数第二个点
{
f[i][j] = min(f[i][j], f[t][k] + weight[k][j]);
}
}
}
}
}
}
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}