AcWing 状压DP——91. 最短Hamilton路径

原题链接:

91. 最短Hamilton路径 - AcWing题库

题解:

本题采用暴搜的时间复杂度较高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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值