旅行商问题(状态压缩的DP)

版权声明:个人博客:www.jingyile.cn 萌新发博文积累经验,欢迎各位大佬指导!!! https://blog.csdn.net/JYL1159131237/article/details/78362733

旅行商问题(TravelingSalesmanProblem,TSP)又译为旅行推销员问题、货郎担问题,简称为TSP问题,是最基本的路线问题,该问题是在寻求单一旅行者由起点出发,通过所有给定的需求点之后,最后再回到原点的最小路径成本。

旅行商问题的提法为:

假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路经的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。(路径的选择目标是要求得的路径路程为所有路径之中的最小值给定一个有向图/无向图,图中的边有长度,求一条最短的哈密尔顿回路,即从某一个点开始,依次遍历所有点,回到原点除起点也是终点外,其他的点在遍历中刚好经过一次将图中经过的边的长度相加,求最小的长度和)


刚看完题目后,可能我们大家都有一个想法,就是搜索!为什么这样想,因为我们可以看到题目的要求:从一点出发,遍历完除该点外的其余所有点,然后回到出发的这一点,所以我们搜索遍历一下所有的点就行长度即可,累加搜索时两个点之间的长度。当我们搜索的时候可以发现,这其实是一个全排列的问题:从出发点出发后,我们可以选择走除出发点外的其余任何点,题目允许这样走,题目没有限制要求,所以走到每个点后我们都可以选择走其余剩下点的其中一个点,所以这变成了一个排列组合问题,有(n-1) !中可能。那我们怎么控制在深搜的时候不走重复的点,其实这很简单,我们设一个数组flag[]就可以避免深搜的时候走重复的点,初始化为0;如果走到i点,就设flag[i]=1,设置判断条件,当flag[i]==1时,说明已经走过,就不走该点,最后走完所有点后用一个全局变量ans判断保存最小的那个值,要注意的是要加上遍历的最后一个点和出发点的距离,因为他要走回去。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int a[100][100];
int ans=0x3f3f3f3f;
int n;
int flag[100]={0};
void dfs(int start,int num,int sum)
{
    if(num==n)
    {
        ans=min(ans,sum+a[start][1]);
        return ;
    }
    for(int i=2;i<=n;i++)
    {
        if(flag[i]==0)
           {
               flag[i]=1;
               dfs(i,num+1,sum+a[start][i]);
               flag[i]=0;
           }
    }
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        scanf("%d",&a[i][j]);
        flag[1]=1;
        dfs(1,1,0);
        printf("%d",ans);
        return 0;
}

分析可知也可以使用动态规划来解决这个问题,但当我们开始写状态转移方程的时候我们发现我们很难表示没走过城市的集合,如dp[i][{1,2,3}]不好直接用一个数组表示,那么状态转移方程也不好写,那么我们该怎么解决这个问题呢?这个时候,神奇的位运算来了~~

与运算 :&   
1&1=1
1&0=0
或运算:|
1|1=1
1|0=1
0|0=0

非:~
按位取反

数字在计算中是用二进制来表示的,一个数对应一个唯一的二进制,不会有相同重复的。

而在题目中每个城市只有两个状态,要么走过,要么没走过,所以我们可以神奇的把一个二进制中的数字所处位数看作是城市的编号,二进制中只有0和1,所以我们就可以用0和1来表示对应城市的位数,1代表走过,0代表没走过,这样就很巧妙地知道剩下的集合状态。(因为二进制是唯一的,不会重复)。然后新问题有来了,怎么走过的城市标记上1,搜索时如果不走某个城市了怎么让它又变为0?这时候位运算排上用场了,可以用位运算实现这连个操作

K位置上值清零,其余位不变   s&(~1<<(k-1))

K位置上值变为1,其余位不变  s|(1<<(k-1))

 1的二进制位为:00000001,如果它要移动到k位置它只需要左移k-1位置就可以,左移后除了k位置上的值为1,其余位全为0,所以相或的那个数的k位置上的0会变为1,其余位置上的值不发生改变;同样道理把1左移k-1后,把它全部按位取反,,则除了1左移后k位置上的值为0,其余全为1,因为1&1=1,1&0=0,所以相与的那个数的其他位置值都不发生改变,而0&1=0,0&0=0,所以这样可以使原来k位置上的1变为0.所以上面那两个公式的正确性可以保证~~
举例如下:

把24的第4个位置上的1变为0:24&(~(1<<(4-1)))
24:00011000   1:00000001
1左移三,则1(左移):00001000 按位取反:11110111
则24&1(左反):00011000&11110111=00010000=16;

     把24的第3个位置上的0变为1:24|(1<<(3-1))
1左移二,则1(左移):00000100
则24|1(左移):00011000|00000100=00011100


#include<bits/stdc++.h>
using namespace std;
int DFS(int v,int S);
int n,Map[45][45],maxi;
int status[25][3000000];
int main()
{
  scanf("%d",&n);
  maxi=(int)pow(2.0,n*1.0);
  for(int i=1;i<=n;i++)
   for(int j=1;j<=n;j++)
    scanf("%d",&Map[i][j]);
  cout<<DFS(1,0)<<endl;
  return 0;
}
int DFS(int v,int S){
  if(S==(maxi-2)) return  Map[v][1];//发现所有的城市都走过后结束
  if(status[v][S]) return status[v][S];//如果当前状态已经求解过直接
  int M=99999999;                              //返回
  for(int i=2;i<=n;i++)//查找没有走过的城市
   if(!(S&(1<<(i-1)))&&v!=i){
    S|=(1<<(i-1));                                  //要走该城市则在对应位置变值
    M=min(M,DFS(i,S)+Map[v][i]);      //为1表示走过
    S&=(~(1<<(i-1)));                             //把刚才走过的城市变回为0,
   }                                                        //给其他情况使用,方便找最优
  return status[v][S]=M;
}


深度优先搜索,思路简单,很容易想到,对于小数据测试,它运行时间不慢,但大数据测试的时候,就会需要很长的运行时间

动态规划在这题中是对深搜进行了剪枝,遇到以前搜索过的就不必再进行重复搜素,可以用一个二维数组来储存这个状态的值,当在遇到时就不必进行搜素。但这题因为是用二进制来压缩的,当你输入的城市个数大于你定义的变量s所能表示的最大位数时,这个程序很容易崩溃,这个要注意。

最后感谢达能大佬PPT的详解。

展开阅读全文

没有更多推荐了,返回首页