原题链接:
题解:
本题采用暴搜的时间复杂度较高O((n-2)!),因此可以采用状压DP进行优化。
状态压缩的实质就是:将路径的状态用二进制压缩。(也就是状态用二进制表示)
1.本题思路
假设:一共有七个点,用0,1,2,3,4,5,6来表示,那么先假设终点就是5,在这里我们再假设还没有走到5这个点,且走到的终点是4,那么有以下六种情况:
first: 0–>1–>2–>3–>4 距离:21
second: 0–>1–>3–>2–>4 距离:23
third: 0–>2–>1–>3–>4 距离:17
fourth: 0–>2–>3–>1–>4 距离:20
fifth: 0–>3–>1–>2–>4 距离:15
sixth: 0–>3–>2–>1–>4 距离:18
如果此时你是一个商人你会走怎样的路径?显而易见,会走第五种情况对吧?因为每段路程的终点都是4,且每种方案的可供选择的点是0~4,而商人寻求的是走到5这个点的最短距离,而4到5的走法只有一种,所以我们选择第五种方案,可寻找到走到5这个点儿之前,且终点是4的方案的最短距离,此时0~5的最短距离为(15+4走到5的距离).(假设4–>5=8)
同理:假设还没有走到5这个点儿,且走到的终点是3,那么有一下六种情况:
first: 0–>1–>2–>4–>3 距离:27
second: 0–>1–>4–>2–>3 距离:22
third: 0–>2–>1–>4–>3 距离:19
fourth: 0–>2–>4–>1–>3 距离:24
fifth: 0–>4–>1–>2–>3 距离:26
sixth: 0–>4–>2–>1–>3 距离:17
此时我们可以果断的做出决定:走第六种方案!!!,而此时0~5的最短距离为(17+3走到5的距离)(假设3–>5=5)
在以上两大类情况之后我们可以得出当走到5时:
1.以4为终点的情况的最短距离是:15+8=23;
2.以3为终点的情况的最短距离是:17+5=22;
经过深思熟虑之后,商人决定走以3为终点的最短距离,此时更新最短距离为:22。
当然以此类推还会有以1为终点和以2为终点的情况,此时我们可以进行以上操作不断更新到5这个点的最短距离,最终可以得到走到5这个点儿的最短距离,然后再返回最初的假设,再依次假设1,2,3,4是终点,最后再不断更新,最终可以得出我们想要的答案。
2.DP分析:
用二进制来表示要走的所以情况的路径,这里用i来代替
例如走0,1,2,4这三个点,则表示为:10111;
走0,2,3这三个点:1101;
状态表示:f[i][j];
集合:所有从0走到j,走过的所有点的情况是i的所有路径
属性:MIN
状态计算:如1中分析一致,0–>·····–>k–>j中k的所有情况
状态转移方程:f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j])
时间复杂度为O(2^n*n^2),可以接受
疑惑点:
关于为什么 i 和 j 循环的位置不能互换的问题:
因为在进行状态转移时,f[i][j] 要由 f[i^(1 << j)][k] 转移过来,所以一定要保证在计算 f[i][j] 之前一定已经计算过 f[i^(1 << j)][k] 了,这样才能保证答案递推的连续性。在 i 是外层循环中,是按照最外层为 状态依次从小到大 的顺序进行对答案的计算
因为状态 i^(1 << j) 一定是小于状态 i 的,所以符合递推的连续性。在 j 是外层循环中,在计算 f[i][j] 时一定用到 f[i^(1 << j)][k]
而当 k>j 的情况时,此时的 f[i^(1 << j)][k] 并没有被计算过,因为你最外层循环还没循环到k呢。例如: f[101011][3] = f[100011][5] + w[5][3]
按 i 外层循环,一定可以保证 f[100011][5] 在f[101011][3] 之前被计算出来。
而按 j 外层循环,由于 5 > 3 ,f[100011][5] 根本就没有被计算过,所以不可行。
代码:
DP:
#include<bits/stdc++.h>
using namespace std;
const int N = 21, M = 1 << N;
int f[M][N], wgt[N][N];
int n;
int main() {
cin >> n;
for (int i = 0;i < n;i++)
for (int j = 0;j < n;j++) cin >> wgt[i][j];
memset(f, 0x3f, sizeof(f));
f[1][0] = 0;
//f[1][0]的含义,前一个数表示状态,1的二进制表示为00…0001,二进制每位表示有没走过,此时0的位置上为1就表示走过了,且0到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 ((i >> k) & 1) {
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + wgt[j][k]);
}
}
}
}
}
cout << f[(1 << n) - 1][n - 1];
}
模拟退火:
1. 先固定起点终点,然后随机一个序列
2. 直接抽卡(中间随机交换 2点,看是否比原序列优)
3. 算路径直接 O(n)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 21;
int n;
int g[N][N];
int pre[N];
int res;
//计算当前路径长度
int getDistSum()
{
int ans = 0;
for(int i = 1; i < n; i ++) ans += g[pre[i - 1]][pre[i]];
return ans;
}
//随机返回区间[l, r]中的一个数
double rand(double l, double r)
{
return (double)rand() / RAND_MAX * (r - l) + l;
}
void simulated_annealing()
{
random_shuffle(pre + 1, pre + n - 1); //随机打乱路径, 起点和终点保持不变
int old = getDistSum();
for(double T = 1e5; T > 1e-5; T *= 0.986) {
int a = rand(0, n), b = rand(0, n); //随机两个点
if(a == 0 || b == 0 || a == n - 1 || b == n - 1) continue; //起点和终点保持不变
swap(pre[a], pre[b]);
int now = getDistSum();
int dE = now - old; //能量差
if(exp(-dE * 1.0 / T) > rand(0, 1)){ //以一定概率跳至新的解
old = now;
res = min(res, now);
} else {
swap(pre[a], pre[b]);
}
}
}
int main()
{
cin >> n;
for(int i = 0; i < n; i ++)
for(int j = 0; j < n; j ++)
cin >> g[i][j];
res = 1e8;
for(int i = 0; i < n; i ++) pre[i] = i; //标记路径
//模拟退火
for(int i = 0; i < 100; i ++) simulated_annealing();
cout << res << endl;
return 0;
}
暴搜DFS(时间复杂度较高,无法通过,仅作为参考):
#include<bits/stdc++.h>
using namespace std;
const int N = 25, INF = 0x3f3f3f3f;
int dis[N][N], vis[N];
int n, res = INF;
void dfs(int node, int len, int num) {
vis[node] = 1;
if (len >= res) return;
if (node == n - 1) {
if (num == n && len < res) res = len;
else return;
}
else {
for (int i = 0;i < n;i++) {
if (node != i && !vis[i]) {
dfs(i, len + dis[node][i], num + 1);
vis[i] = 0;//多次搜索,搜索后要还原
}
}
}
}
int main() {
cin >> n;
for (int i = 0;i < n;i++)
for (int j = 0;j < n;j++) cin >> dis[i][j];
dfs(0, 0, 1);
cout << res;
}