动态规划方法解旅行商问题(TSP Traveling Salesperson Problem)

本文通过具体例子详细介绍了如何使用动态规划解决旅行商问题(TSP)。从邻接矩阵和状态矩阵D的概念出发,阐述了动态规划的状态转移规律,并提供了求解最优路径的公式。最后,给出了Java代码实现的概述。

网上很多相关文章介绍的时候都缺乏例子,以至于太抽象不够直观。本文用具体例子来介绍动态规划法界tsp,力求让初学者可以轻松理解。伪代码来自《Foundations of Algorithms(written by Neapolitan, Richard)》

TSP

tsp是为了在一个图中求得一个最短的路径,可以实现从一个起点出发,经过所有节点,最终回到起点。

首先理清一些概念

a.对于下图,我们用v1,v2,v3,……v6表示每个节点。n表示节点个数,n = 6。 因为求的是一个回路,所以从任意一点出发都没有区别。所以我们以v1做起点。

b.用二维邻接矩阵W表示各个节点之间的距离。即W[i][j] = vi到vj的距离


c.比较难理解也是比较重点的是用于存储动态规划状态的矩阵D。D的每一行表示一个节点,每一列表示一个子集。这个子集是指包含除起点v1外的所有节点集合V ={v2, v3, v4, v5, v6}(注意,这里的V不是所有点的集合,是除v1外所有点的集合)的任一子集

-------------------------------------------------------------------------------------------------------

{}{v2}{v2,v3}..(一共有2^5个子集,这里不全写了).{v2,v3,v4,v5,v6}

v1

v2

v3

...

v6

--------------------------------------------------------------------------------------------------------

 {Ø}{v2}{v2, v3}省略,一共是2^5个子集{v2, v3, v4, v5, v6}
v1   …… 
v2   …… 
v3     
v4     
v5     
v6     

D[v2][{v3}]表示,v2经过v3,最终回到起点v1的路程。一般地,当用A表示V ={v2, v3, v4, v5, v6}的任一子集,对于D[vi][A],

D[vi][A] = 从vi出发,经过A中所有节点,再回到起点v1,所经历的最短路程。

当A为空集,就是指vi直接回v1的距离。也就是D[vi][Ø] = W[i][1]

动态规划的思路

那么,我们要求的最优路径,也就是D[v1][V}]

D[v1][{v2, v3, v4, v5, v6}] = W[v1][vi] + min(D[vi][V - vi])(vi = v2,v3,v4,v5,v6)

动态规划的思想,就是是研究状态转移的规律,通过从最初的状态,转移到另一个状态,逐渐转移到解决问题的状态。下面说说本题的状态

对于D来说,我们已知的就是D[vi][Ø] = W[vi][v1],以及,当A只有一个元素时,D[vi][A]的值。因为它就是vi -> vj(vj∈A) -> v1,也就是W[vi][vj] + W[vj][v1]。

但是当A中的元素多于一个,比如 A = {vj, vk},D[vi][A]的值是vi -> vj -> vk -> v1与 vi -> vk -> vj ->v1中更小的那个值。

比如D[v2][{v3, v4}] = min(9+7+7, 5+7+16) = 23

对于任一A⊆V,我们不能直接穷举它所代表的所有路径,然后求一个最小值,赋给D[vi][A],因为这样就变成了暴力法了。而动态规划的思想是,从A = Ø开始,求得A中包含一个元素时D的所有结果,A中包含两个元素时D的所有结果,一直到A包含了任一n-2个元素的所有D的结果(n是包含起点的所有节点个数,本题n = 6)。

为什么是n-2个呢? 对于D[vi][A]来说,当A包含vi本身的时候,对本题来说是没有意义的,比如,D[v2][{v2}]这个值,没有意义。

所以,当我们知道D[v2][{v3, v4, v5, v6}]的时候,我们也就知道了如果从v1出发,先到v2,所需要的最短回路是多少了。当我们求得了所有的D[vi][V - vi]时,本题就解出来了。

对于任一非空的A,都有D[vi][A] = min(W[vi][vj] + D[vj][A-vj]) 其中vj属于A。

比如,D[v2][{v3}] = W[v2][v3] + D[v3][Ø],又因为D[vi][Ø] = W[vi][1],所以D[v2][{v3} = ]W[v2][v3] + W[v3][v1]

在程序中,我们就逐渐增多A中元素的个数,直到n - 2为止。

当经历第一遍循环,所有D[vi][{vj}]的值就都知道了。然后对于含有两个元素的A,比如D[v2][{v3,v4}],就有

D[v2][{v3,v4}] = min(W[v2][v3] + D[v3][{v4}],W[v2][v4] + D[v4][{v3}])

当执行完n-2次这个循环,D中的所有值,除了D[v1][A]就都知道了。我们最终要求的是D[v1][V],它是

D[v1][{v2,v3,v4,v5,v6}] = min(W[v1][v2] + D[v2][{v3,v4,v5,v6}],

W[v1][v3] + D[v3][{v2,v4,v5,v6}],

W[v1[v4] + D[v4][{v2,v3,v5,v6}]],

W[v1][v5] + D[v5][{v2,v3,v4,v6}],

W[v1][v6] + D[v6[{v2,v3,v4,v5}]])



java代码

其中DP函数用于求解,generateOpttour用于输出路径。getSubsets用于得到节点的所有子集。用hashmap储存子集与该子集在D中的列号。

package project3;


import java.util.ArrayList;
//import java.util.ArrayList;
import java.util.HashMap;


public class TSPDP {
  private int[][] graph;
  HashMap<Integer, ArrayList<Integer>> idtoset = new HashMap<Integer, ArrayList<Integer>>();
  //get subset by id
  HashMap< ArrayList<Integer>, Integer> settoid = new HashMap< ArrayList<Integer>, Integer>();
  //get id by subset
  public TSPDP(int[][] graph){
    this.graph = graph;
  }
  /**
  * Solve Traveling Salesperson Probl  em by Dynamic Programming
  * @return the min length
  * */
  public int DP(){
    int n = graph.length;
    int[] vertex = new int[n-1];
    int vertexid = 1;
    for(int i = 0; i < vertex.length; i++)  {
      vertex[i] = vertexid;
      vertexid++;
    }
    getsubsets(vertex);
    int[][] D = new int[n][settoid.size()];//To record the distance
    int[][] P = new int[n][settoid.size()];//To track the path

    for(int i = 1; i < n; i++){
      D[i][0] = this.graph[i][0];
    }
    for(int k = 1; k <= n - 2; k++){
      for(int id = 0; id < idtoset.size(); id++){
        ArrayList<Integer> subset = idtoset.get(id);
        if(subset.size() != k)
          continue;
        for(int i = 1; i < n; i++){
          if(subset.contains(i))
            continue;
          int min = 10000;
          int value = 0;
          for(int j : subset){
            ArrayList<Integer> Aminusj = remove(subset, j);
            int idj = settoid.get(Aminusj);
            try{
              value = this.graph[i][j] + D[j][idj];
            }catch(Exception e){
              System.out.print("Error!___");
              System.out.println("i: " + String.valueOf(i) + " j: "+ String.valueOf(j));
              int size = this.graph[i].length;
              System.out.println(" graph.length: "+ String.valueOf(size));
              System.out.println(" D.length: "+ String.valueOf(D.length));
            }
            if(value < min && value != 0){
min = value;
              P[i][id] = j;
            } 
          }
          if(min < 9999);
            D[i][id] = min;
          }
       }
    }
    ArrayList<Integer> Vminusv0 = new ArrayList<Integer>();
    for(int i = 0; i < vertex.length; i++){
      Vminusv0.add(vertex[i]);
    }
    int vminusv0id = settoid.get(Vminusv0);
    int min = Integer.MAX_VALUE;
    for(int j : Vminusv0){
      ArrayList<Integer> Vminusv0vj = remove(Vminusv0,j);
      int idj = settoid.get(Vminusv0vj);

      int value = (this.graph[0][j]!=0 && D[j][idj]!=0) ? this.graph[0][j] + D[j][idj]:0;


      if(value < min && value != 0){
        min = value;
        P[0][vminusv0id] = j;
      }
    }
    if(min < 99999);
    D[0][vminusv0id] = min;

    generateOpttour(P, Vminusv0);
    return D[0][vminusv0id];
  }

  private void printDP(int[][] D){
    for(int i = 0; i < D.length; i++){
      for(int j = 0; j < D[i].length; j++){
        System.out.print(D[i][j]);
        System.out.print(" ");
      }
      System.out.println();
    }
  }
  /**
  * Generate optimal tour by P, and print it
  * @param P
  * @param list containing all vertexes   except V0
  * */
  public void generateOpttour(int[][] P, ArrayList<Integer> V){
    String path = "1->";
    ArrayList<Integer> Set = V;
    int start = 0;
    while(!Set.isEmpty()){
      int id = settoid.get(Set);
      String vertex = String.valueOf(P[start][id]+1);
      path += vertex + "->";
      Set = remove(Set, P[start][id]);
      start = P[start][id];
    }
    path += "1";
    System.out.println(path);
  }
  /**
  * Get all subsets of a input set. And number subsets
  * All results will be recorded in member variables
  * @param set input set
  * */
  private void getsubsets(int[] set){
    idtoset.clear();
    settoid.clear();
    int max = 1 << set.length; //how many sub sets  
    int id = 0;
    for(int i=0; i<max; i++){  
      int index = 0;  
      int k = i;  
      ArrayList<Integer> s = new ArrayList<Integer>();  
      while(k > 0){  
        if((k&1) > 0){  
          s.add(set[index]);  
        }  
        k>>=1;  
        index++;  
      }  
      idtoset.put(id, s);
      settoid.put(s, id);
      id++; 
    }   
  }  
  /**
  * Remove an input value in a list
  * @param src source list
  * @param n the value to be removed
  * @return list after removing n
  * */
  private ArrayList<Integer> remove(ArrayList<Integer> src, int n){
    ArrayList<Integer> dest = new ArrayList<Integer>();
    int j = 0;
    for(int i = 0; i < src.size(); i++){
      int vertex = src.get(i);
      if(vertex == n)
        continue;
      dest.add(vertex);
    }
    return dest;
  }

  public static void main(String[] args){

    TSPDP test = new TSPDP(new int[][]{{0,16,16,7,13,6},
        {16,0,9,5,19,7},
        {16,9,0, 7,9,6},
        {7,5, 7,0,7,7},
        {13,19,9,7,0,13},
        {6,7,6,7,13,0}});
    int dis = test.DP();
    System.out.println(dis);
  }
}


评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值