dp处理旅行商问题

某售货员要到若干个城市去推荐商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过其它每个城市有且仅有一次,最后回到驻地的路线,使总路程(或旅费)最小。

用下面图形来举例,有四个地区:

2.基本思想

  • 解决方法:首先尝试暴力解法,枚举点的全排列,假设有n个点的话,那么共有n!个全排列,一个排列就是一条路径,那么时间复杂度就达到了O(n!),这里我们使用状态压缩dp来处理这个问题,那么时间复杂度将降低到O(2^{n}*n^{2})
  • 图的保存形式:设G=(V,E)是一个有n个顶点的无向带权图,V表示点,E表示边,在程序中采       用邻接矩阵来保存这类图并进行dp处理
  • 状态压缩DP的处理:将问题的状态用二进制位来表示,每个二进制位代表某个状态的存在或    缺失。通过位运算和状态压缩技巧,可以将状态压缩为一个整数,用该整数来表示问题的状态。

3.算法描述

  • 状态压缩是dp的一个小技巧,一般应用于集合问题里面。主要是把对dp状态的处理转换为二进制的位操作,让代码变的简洁和提高算法效率;
  • 首先定义dp的状态,用dp[S][j]表示从起点出发经过集合S里面的所有点最终到达终点j时的最短路径,然后根据dp思想,让集合S从小集合递推到大集合,逐步扩展到整个图,最终得到的下面代码的最终结果就是答案,此时的S表示包含图上所有点的集合,k∈(1,n);    
min(dp[S][k]+dis[k][1])
  • 如何求dp[S][j]?可以从S-j集合递推到S集合,假设有一点k它在S-j集合里面,那么dp[S][j]状态就可以由dp[S-j][k]状态推导出来,状态转移方程如下所示,集合S初始时只包含起点,然后逐步将图中的顶点包含进来,直到最后包含所有点。这个过程用状态转移方程实现;
  dp[S][j]=min(d[S][j],dp[S-j][k]+dis[k][j])
  • 如何操作集合S?用一个二进制数表示集合S,这个二进制上的每一位都代表图上的一个点,该位置上值为1表示该点在集合里面,为0表示该点不在集合里面,例如S=1010,其中1的位置分别处在2、4位(因为地图是从1开始编号,所以这里用逻辑序号表示),表示集合中包含这两个点。

4.算法程序代码及其结果

int n;  //点的个数,也就是城市数量
int dis[21][21]; //保存城市之间的信息,邻接矩阵保存
int dp[1<<20][21]; //状态转移数组
int yasuodp(int n,int **dis){	
	memset(dp,0x3f,sizeof(dp));//初始化为最大值	
	dp[1][1]=0;  //dp初始状态,集合S里面只包含点1,起点和终点都是1,值为0	
	for(int s=1;s<(1<<n);s++)//从小集合0001遍历到整个图1111,共2^n次		
	{	
		for(int i=1;i<=n;i++)//枚举点	
		{	
			if((s>>(i-1))&1)//判断点i是否在集合里面
				for(int j=1;j<=n;j++)//枚举到达i的点j
		             if((s^(1<<(i-1)))>>(j-1)&1)//判断点j是否在集合s-i里面						
						        dp[s][i]=min(dp[s][i],dis[j][i]+dp[s^(1<<(i-1))][j]);			
		}		
	}
	int result=dp[(1<<n)-1][1];	
	for(int i=1;i<=n;i++)		
		result=min(result,dp[(1<<n)-1][i]+dis[i][1]);//遍历求最小值			
			return result;			
}

上面图运行结果如下(文章最后会附上源代码,这个函数只是做个介绍):

说明:

  • s>>(i-1))&1

说明:判断点i是否在S集合里面(集合S里面的序号是从0开始编号的)原理:i因为是表示逻辑序号,所以第i位实际上是表示集合S里面的第(i-1)位,判断点i在不在集合里面,就是判断集合S的第(i-1)位是不是为1,首先使用位运算右移符号‘>>’将集合S中的第(i-1)位移动到第一位[s>>(i-1)],然后与1相与,如果结果为1就表示集合S里面的第(i-1)位为1,表明点i在集合S里面

  • s^(1<<(i-1))

说明:将集合S里面的第(i-1)位赋为0

原理:1<<(i-1)表明将1左移(i-1)位,旨在构造一个二进制数[1...(i-1)个0],然后与集合S相异或,就可以将集合S里面的第(i-1)位赋为0,得到集合(S-i)

  • s^(1<<(i-1))>>(j-1)&1

说明:判断点j是否在S-i集合里面

原理:前两个运算的结合,s^(1<<(i-1))不多解释,就是构造S-i集合,将集合s里面的第(i-1)位赋为0,然后>>(j-1)&1就是判断点j是否在S-i集合里面

5.算法分析

时间复杂度:时间复杂度从O(n!)降低到了O(2^{n}*n^{2}),由代码可知算法主要由3个for循环构成,第1个for循环从1遍历到-1,共有约次,所以复杂度约为O(),在加上后面两个各n次的for循环,所以总复杂度为O(2^{n}*n^{2})

可知当n=8时O(n!)的复杂度已经超过了O(2^{n}*n^{2})的复杂度,并且在n=20时,O(n!)的复杂度已经达到了O(2^{n}*n^{2})复杂度的上亿倍。

优点:

  • 算法效率高:状态压缩DP可以大幅度减少计算时间,尤其在处理指数级别的状态空间时,相对于传统的动态规划算法,它可以显著提高算法的效率。
  • 时间复杂度低:通过状态压缩技巧,可以避免重复计算相同的子问题。这可以进一步加速算法的执行速度,大幅减少算法的计算时间。

缺点

  •  实现复杂:状态压缩DP相对于传统的动态规划算法来说,实现难度较高。需要处理位运算相关的操作,这可能增加了代码的复杂性和难度。
  • 不适用于所有问题:状态压缩DP适用于满足子问题的无后效性和最优子结构性质的问题。

改进方法:

  •  动态规划剪枝:使用动态规划剪枝技术来减少不必要的计算,提前终止某些分支的计算,以减少计算量。
  • 邻近搜索策略:通过邻近搜索策略指导状态压缩DP的计算过程,优先考虑与当前状态相邻的状态,以有针对性地搜索最优解。

6.算法运行过程

dp的初始状态除了dp[1][1]=0外,其他值都初始化为最大值,又可知集合s是由集合s-i推导出来的,所以如果集合s-i里面不包含起点1的话,那么dp的值是肯定为最大值的。如下所示:

当你的状态处在不包含起点时,是肯定求得最大不会有所改变的,保持原最大值。

最后当求得所有dp最优状态时,根据代码

  int result=dp[(1<<n)-1][1];     
     for(int i=1;i<=n;i++)         
         result=min(result,dp[(1<<n)-1][i]+dis[i][1]; 

  求出最终结果,因为求得的dp[s][i]表示从起点出发经过集合S里面的所有点最终到达终点i时的最短路径,所以最后要加上dis[i][1],表示最终返回起点

7.解决题目的源代码程序

#include <bits/stdc++.h>
using namespace std;
int dis[21][21], dp[1 << 20][21];
int main() {
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++)
			scanf("%d", &dis[i][j]);
	}
	memset(dp, 0x3f, sizeof(dp));
	dp[1][1] = 0;
	for (int s = 1; s < (1 << n); s++) {
		for (int i = 1; i <= n; i++) {
			if ((s >> (i - 1)) & 1)
				for (int j = 1; j <= n; j++)
					if ((s ^ (1 << (i - 1))) >> (j - 1) & 1)
						dp[s][i] = min(dp[s][i], dis[j][i] + dp[s ^ (1 << (i - 1))][j]);
		}
	}
	int result = dp[(1 << n) - 1][1];
	for (int i = 1; i <= n; i++) {
		result = min(result, dp[(1 << n) - 1][i] + dis[i][1]);
	}
	printf("%d", result);
}

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
旅行商问题是一个经典的组合优化问题,它的目标是找到一条路径,使得一个旅行商能够访问一组城市,同时最小化总距离或总成本。动态规划是解决旅行商问题的一种常见方法。下面是一个Python实现的动态规划TSP算法: ```python import sys from itertools import permutations def tsp_dp(cities): # 计算城市之间的距离矩阵 n = len(cities) dist = [[0] * n for i in range(n)] for i in range(n): for j in range(n): dist[i][j] = distance(cities[i], cities[j]) # 初始化动态规划dp = [[-1] * (1 << n) for i in range(n)] for i in range(n): dp[i][1 << i] = 0 # 动态规划 for mask in range(1, 1 << n): for i in range(n): if mask & (1 << i) == 0: continue for j in range(n): if i == j or mask & (1 << j) == 0: continue prev_mask = mask ^ (1 << i) if dp[i][mask] == -1: continue if dp[j][prev_mask] == -1: dp[j][prev_mask] = dp[i][mask] + dist[i][j] else: dp[j][prev_mask] = min(dp[j][prev_mask], dp[i][mask] + dist[i][j]) # 找到最小旅行距离 min_dist = sys.maxsize for i in range(n): if dp[i][(1 << n) - 1] == -1: continue min_dist = min(min_dist, dp[i][(1 << n) - 1] + dist[i][0]) return min_dist def distance(city1, city2): return ((city1[0] - city2[0]) ** 2 + (city1[1] - city2[1]) ** 2) ** 0.5 # 示例 cities = [(0, 0), (1, 2), (3, 4), (5, 6)] print(tsp_dp(cities)) ``` 该算法使用动态规划来计算从每个城市开始的最小旅行距离。它首先计算城市之间的距离矩阵,然后初始化一个动态规划表。每个单元格表示从一个城市出发,访问特定子集的城市的最小距离。然后,它使用一个循环来填充动态规划表,直到所有子集都被处理。最后,找到最小旅行距离并返回。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值