AcWing 91. 最短Hamilton路径(二进制状态压缩、DP)

题目链接:点击这里
在这里插入图片描述

题意:求起点 0 0 到终点 n 1 n-1 的最短哈密顿路径。从题目样例来看,默认都是无向完全图(但是题目本身并未提及,坑)。也就是说,任意一点都可以直达其它顶点,完全不用担心从一个顶点到另一个顶点没有边,这对题意的理解很重要。

朴素做法,就是枚举n个点的全排列,计算路径长度取最小值,时间复杂度为 O ( n n ! ) O(n*n!)

使用二进制状态压缩DP可以优化到 O ( n 2 2 n ) O(n^2*2^n)

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 后面所有的路径状态都可以由前者替代。

从起点到当中任意一点都有一个最短路径,由上述分析可知,对于这些状态我们仅仅关注两个方面:

  1. 哪个点被用过
  2. 当前停在哪个点

因此,我们可以用 f [ i ] [ j ] f[i][j] 表示从起点 0 0 经过路径 i i 走到顶点 j j 的最短Hamilton路径长度。
其中路径 i i 由一串二进制数表示, i i 的二进制数下哪一位为 1 1 则表示哪个点已经走过了。

状态转移方程: f [ s t a t e ] [ j ] = f [ s t a t e _ k ] [ k ] + w e i g h t [ k ] [ j ] f[state][j] = f[state\_k][k] + weight[k][j]
其中 s t a t e _ k state\_k 就是 s t a t e state 去掉 j j 之后的集合,并且 s t a t e _ k state\_k 要包含 k k

#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

小优化:

  1. 既然 f f 数组定义是从顶点 0 0 出发,那么状态二进制表示的最后一位必然是 1 1 ,也就是说只需要枚举 i i 是奇数的情况,这样可以使得程序的执行速度快了一倍。
  2. 另外,由于路径 i i 只是表示哪些顶点走过,并不能表示遍历的顺序,因此,我们在遍历每个状态时,还需要枚举当前遍历到的顶点。只有路径中仅包含两个顶点时才需要从 0 0 枚举 k k ,否则不需要。
#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;
}
发布了727 篇原创文章 · 获赞 111 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_42815188/article/details/104237249