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

概念

简而言之:是处理复杂集合问题的 DP。每个状态 dp[i][j] 表示的不是一个有意义的数值,例如花费、价值、长度等,而是代表了集合的数量

旅行商问题

Traveling Saleman Problem, TSP:有 n 个城市,已知任何两个城市之间的距离(或者费用),一个旅行商从某城市出发,经过每一个城市并且只经过一次,最后回到出发城市,输出最短(或者路费最少)的线路

小规模的 TSP 问题可以用状态压缩 DP 求解,复杂度是:O(2^{n}n^{2}),能解决规模 n<=15 的问题

思路:假设最短的 TSP 路径是:Path = (v0\rightarrow v1\rightarrow v2\rightarrow v3\rightarrow v0)

           那么 Path = (v0\rightarrow v1)+(v1\rightarrow v2\rightarrow v3\rightarrow v0)

           所以,问题转变为:求经过所有城市的最短回路 \rightarrow 从某个城市到起点的最短路径

DP 状态设计如下:假设已经访问过的城市集合是 S,当前所在城市是 v,用 dp[S][v] 表示从 v 出发访问剩余的所有城市最后回到起点的路径费用总和的最小值。

状态转移方程:dp[V][0]=0  //V 是最后一个城市

                         dp[S][v]=min\left \{ \right dp[S\cup \left\{ \right u\}][u]+dist(v,u) \mid u\notin S\}

城市集合 S 如何表示?这就用到压缩状态 DP 的技巧;把路径“压缩”到二进制数中。定义: int dp[1<<MAXN][MAXN];

MAXN 是城市数量,当 MAXN=15 时,1<<MAXN=2^15=32768, 0~32768 内的每个数的二进制表示就是一个可能的路径,二进制数中的 1 表示选中一个城市,0 表示不选中。例如:S=000 0000 0000 0101(2),末尾的 101 表示已访问过的城市2、0。在下面代码中,“dp[s | 1<<u][u]”,其中的 s | 1<<u,表示在已访问过的城市集合 S 中加入一个新访问的城市 u

下面是部分示意代码(c++):

例2

TSP 的变形

本题用状态压缩 DP 求解,算法复杂度是 O(3^{n}n^{2}),当 n=10 时,正好通过 OJ 测试

  1. 路径的表示 

    在普通 TSP 中,一个城市只有两种情况,即访问和不访问,用 1 和 0 表示。这个题有 3 种情况,也就是 不访问、访问 1 次、访问 2 次,所以需要用到三进制

    当 n=10 时有 3^{10} 种组合(路径数量),对每个路径用三进制表示。例如:第 14 中路径,它的三进制是 112_{3},表示的是第 3 个城市走 1 次,第 2 个城市走 1 次,第 1 个城市走 2 次

    在程序中用 tri[i][j],表示第 i 个路径,其第 j 位的值是城市的状态,例如:tri[14][3]=1, tri[14][2]=1, tri[14][1]=2

  2. 状态和状态转移 

         定义状态 dp[i][j],当前所在城市是 i,dp[i][j] 表示从 i 出发访问剩余的所有城市最后回到起点的路径 j 的费用总和的最小值

状态转移:dp[j][i] = min\left \{ dp[j][i]\right ,dp[k][l]+graph[k][j]\}

代码(Java)

public class Main
{
   final int INF = 0x3f3f3f3f;
   int n,m;
   int[] bit = {0,1,3,9,27,81,243,729,2187,6561,19683,59049};   //三进制每一位的权值,与二进制的0、1、2、4、8等对照
   int[][] tri = new int[60000][11];
   int[][] dp = new int[11][60000];
   int[][] graph = new int[11][11];   //存图

   private void make_trb(){   //初始化
       for(int i=0; i<59050; i++){   //共 3^10=59050 种状态
           int t = i;
           for(int j=1; j<=10; j++){
               tri[i][j] = t%3;
               t/=3;
           }
       }
   }

   private int comp_dp(){
       int ans = INF;
       for(int i=0; i<11; i++)
           Arrays.fill(dp[i], INF);
       for(int i=0; i<=n; i++)
           dp[i][bit[i]] = 0;   //bit[i] 是第 i 个城市,起点是任意的
       for(int i=0; i<bit[n+1]; i++){   //此循环中,k、j都表示城市点,i、l是路径状态
           boolean flag = true;   //所有的城市都遍历1次以上
           for(int j=1; j<=n; j++){   //选一个终点,即城市 j
               if(tri[i][j]==0){   //判断终点位是否为0,详见注解第4点
                   flag = false;   //还没有经过所有点
                   continue;
               }
               if(i==j)
                   continue;
               for(int k=1; k<=n; k++){
                   int l = i - bit[j];   //i 状态的第 j 位置 0,l 为没有走过j城市前的i状态(用于递归),即去掉城市j,因要将j作为此路径终点
                   if(tri[i][k]==0)   //判断k是否在i中
                       continue;
                   dp[j][i] = Math.min(dp[j][i], dp[k][l]+graph[k][j]);
               }
           }
           if(flag)   //在所有可行路径中找最小费用
               for(int j=1; j<=n; j++)
                   ans = Math.min(ans,dp[j][i]);
       }
       return ans;
   }

    public static void main(String args[]){
        Main m = new Main();
        Scanner sc = new Scanner(System.in);
        m.make_trb();
        while (!sc.hasNext("#")){   //输入以"#"字符串结束
            m.n = sc.nextInt();
            m.m = sc.nextInt();
            for(int i=0; i<11; i++)
                Arrays.fill(m.graph[i],m.INF);
            while(m.m--!=0){
                int a = sc.nextInt(), b = sc.nextInt(), c = sc.nextInt();
                if(c<m.graph[a][b])
                    m.graph[a][b] = m.graph[b][a] = c;
            }
            int ans = m.comp_dp();
            if(ans==m.INF)
                System.out.println("-1");
            else System.out.println(ans);
        }
        sc.close();
    }
}

对以上代码的注解:

  • tri[i][j]:路径 i 中表示一个城市走过的次数
  • dp[j][i]: 表示在路径状态 i 下(即走了多少点,以及每个点走了多少次),最后走的是 j 点的最小花费
  • 对于一个已有状态 dp[j][s], 枚举每一个点 k,如果这个点是 k 不是 j ,而k,j 有边,且 k 被访问次数不超过 2 次,那么下一步就可访问 k 点
  • 如何保证走过了所有城市?就是所选路径 i 中的每个数位均大于0(即对应的数组 tri[i] 中每一元素均大于0,则 i 是一个可行路径状态) 
  • 输入时数组 graph 中的 a、b 是从1 开始的

 

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值