浅谈旅行商问题(TSP问题)

POJ3311为例

题目描述:

The Pizazz Pizzeria prides itself in delivering pizzas to its customers as fast as possible. 
Unfortunately, due to cutbacks, they can afford to hire only one driver to do the deliveries. He will wait 
for 1 or more (up to 10) orders to be processed before he starts any deliveries. Needless to say, he 
would like to take the shortest route in delivering these goodies and returning to the pizzeria, even if it 
means passing the same location(s) or the pizzeria more than once on the way. He has commissioned 
you to write a program to help him.
输入:
3
0 1 10 10
1 0 1 2
10 1 0 10
10 2 10 0
0
输出:
8

此题使用一次floyd之后就变成了一道经典的TSP问题.那么下面将浅谈一下TSP问题

对于TSP问题:

指一个旅行商从一个城市触发,经过每一个城市一次且只有一次回到原来的地方,要求经过的距离最短.目前为止TSP问题是一个NP难题,目前并没有多项式时间以内的高效算法,使用动态规划来解决的话,复杂度为 n 2 2 n n^22^n n22n

对于旅行商问题,我们可以使用 d p [ s ] [ u ] dp[s][u] dp[s][u]来表示当前已走过的集合为 s s s并且从 u u u点出发走完所有剩余点并回到出发点的最小距离

那么对于一个点,我们要计算这个点到出发点的距离,显然可以由他的邻接点来进行递推.因为我们的这个点到达终点的路径显然是会经过他的邻接点的.并且这个邻接点的状态方程显然就加入了我们的 v v v这个状态.所以此时我们不难写出以下的转移方程:

d p [ { s } ] [ u ] = m i n ( d p [ { s } ] [ u ] , d p [ { s } ⋃ { v } ] [ v ] + m p [ u ] [ v ] ) dp[\{s\}][u]=min(dp[\{s\}][u],dp[\{s\}\bigcup\{v\}][v]+mp[u][v]) dp[{s}][u]=min(dp[{s}][u],dp[{s}{v}][v]+mp[u][v])

其中mp[u][v]为邻接表中u,v两点直接的距离,为什么直接使用邻接表储存呢.因为TSP问题的复杂度极大,所以实际问题中的点的个数将不会很多,所以邻接表足以,且邻接表更方便使用

那么接下来的问题就是如何存储我们的状态了

对于这个问题,我们将使用状态压缩的一种方式来存储,对于每一种状态,我们都使用一串二进制数来存储,那么对于每一个点假设在我们的点集之中,那就意味着我们的对应的二进制位为1即可

( s > > v ) & 1 (s>>v)\&1 (s>>v)&1 表示从点集s中提取出我们的v点的状态
( s ∣ ( 1 < < v ) ) (s|(1<<v)) (s(1<<v)) 表示将我们的v点的状态加入到我们的点集s中

对于边界问题:

显然我们最终的状态将会是 d p [ ( 1 < < n ) − 1 ] [ 0 ] dp[(1<<n)-1][0] dp[(1<<n)1][0]表示最终全部点都走过并且最终在0点

那么我们最终的答案就是 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]代表刚开始没有走过任何一个点并且从0开始的最短距离

对于我们的TSP问题,接下来我将分别展示递推和记搜两种求解方式

对于递推:

	dp[(1<<n)-1][0]=0;
	for(int s=(1<<n)-2;s>=0;s--) {
		for(int u=0;u<n;u++) {
			for(int v=0;v<n;v++) {
				if((!(s>>u&1)&&!(u==0&&s==0))||mp[u][v]==inf) continue;
				if(!(s>>v&1)) {
					dp[s][u]=min(dp[s][u],dp[s|(1<<v)][v]+mp[u][v]);
				}
			}
		}
	}

也就是枚举每一种状态,然后更新每一种状态中每一个节点的值

对于约束条件部分:

! ( s > > u & 1 ) !(s>>u\&1) !(s>>u&1)表示当前枚举的出发点不在我们的点集当中,那么显然我们此时不应该枚举我们的u节点,但是这个并不是一定的,因为当我们求解我们的dp[0][0]时,最终需要枚举的状态是s==0并且需要从u=0开始出发,所以当我们的s==0并且u==0时,我们需要继续进行递推


对于记忆化搜索,我们有以下代码(感觉记搜比较好理解):

int solve(int s,int u) {//s表示目前走过的点集,u表示目前所在的点的位置
	if(dp[s][u]>=0) return dp[s][u]; 
	if(s==(1<<n)-1&&u==0) return dp[s][u]=0;
	if(s!=(1<<n)-1&&u==0&&s!=0) return dp[s][u]=inf;
	//对于此处剪枝,因为当我们并没有走完全部的点却提前到初始点显然是不合法的
	int ans=inf;
	for(int v=0;v<n;v++) {
		if(!(s>>v&1)&&mp[u][v]!=inf) {
			ans=min(ans,solve(s|(1<<v),v)+mp[u][v]);
		}
	}
	return dp[s][u]=ans;
}
solve(0,0);

update in 2022/12

博主在打题的过程中发现了一个更容易理解和实现的TSP递推解法.emmm,之前介绍的两种方法都是倒推的,这就在理解上有一点点的绕,所以接下来讲介绍一种顺推的方法(虽然这种方法在优化程度上并不如前两种方法,但是更为简便)

dp[1][0]=0;
for(int S=1;S<(1<<r);S++) {
	for(int u=0;u<r;u++) {
		if(!((S>>u)&1)) continue;
		for(int v=0;v<r;v++) {
			if((S>>v)&1) continue;
			dp[S|(1<<v)][v]=min(dp[S|(1<<v)][v],dp[S][u]+mp[u][v]);
		}
	}
}
int ans=inf;
for(int i=0;i<r;i++) {
	ans=min(ans,dp[(1<<n)-1][i]+mp[i][0]);
}

怎么说呢,这个方法就是将之前倒推的方法给正了过来,更符合一般人的脑回路??所以此方法的dp[S][u]方程的思想就是当你达到了S状态,并且此时站在u位置的最小费用.所以到达v位置(也就是u的邻接点),显然可以通过u节点更新而来,比之前的好理解多了.

但是在我们的dp方程中,我们求出的是走完所有节点并且到达最终的点的最小贡献,但是我们最终是需要回到源点的,所以此时加上我们的两点之间的距离(注意即使不通也没有关系,我们只要初始化 i n f inf inf即可)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值