优化的TSP算法

时间:2011-08-23 09:59:30

标签: algorithm math optimization graph

我对如何改进或提出能够解决约n = 100 to 200个城市的Travelling salesman problem的算法感兴趣。

我给出的维基百科链接列出了各种优化,但是它在相当高的级别上实现了,我不知道如何在代码中实际实现它们。

那里有工业强度解决方案,例如Concorde,但那些对我想要的东西来说太复杂了,充斥着TSP搜索的经典解决方案都是随机算法或经典的回溯或动态编程算法仅适用于大约20个城市。

那么,有没有人知道如何实现一个简单的(简单来说,我的意思是一个实现不需要超过100-200行代码)TSP求解器在合理的时间(几秒)内工作至少100城市?我只对确切的解决方案感兴趣。

您可能会认为输入是随机生成的,因此我不关心专门用于破坏某种算法的输入。

7 个答案:

答案 0 :(得分:51)

200行而没有库是一个严格的约束。高级求解器使用Held-Karp松弛进行分支和绑定,我不确定即使是最基本的版本也能适应200条法线。不过,这是一个大纲。

举行了Karp

将TSP编写为整数程序的一种方法如下(Dantzig,Fulkerson,Johnson)。对于所有边e,常数w e 表示边e的长度,如果边e在旅程中,则变量x e 为1,否则为0。对于顶点的所有子集S,∂(S)表示连接S中的顶点和不在S中的顶点的边。

最小化和边e w e x e
受制于 1.对于所有顶点v,总和边e在∂({v}) x e = 2中 2.对于所有非空的适当的顶点子集S,和边e在∂(S) x e ≥2
3.对于E中的所有边e,{0,1}

中的 e

条件1确保边缘集合是巡视的集合。条件2确保只有一个。 (否则,让S为其中一个游览所访问的顶点集。)通过进行此更改可获得Held-Karp松弛。

<击> 3。对于E中的所有边e,在{0,1}中的x e
3.对于E中的所有边e,0≤x e ≤1

Held-Karp是一个线性程序,但它具有指数数量的约束。解决它的一种方法是引入拉格朗日乘数,然后进行次梯度优化。归结为一个循环,它计算最小生成树,然后更新一些向量,但细节是涉及的。除了“Held-Karp”和“subgradient(descent | optimization)”之外,“1-tree”是另一个有用的搜索词。

(一个较慢的选择是编写一个LP求解器并引入次要约束,因为它们被前一个optima所违反。这意味着编写一个LP求解器和一个min-cut过程,这也是更多的代码,但它可能会更好地扩展到更具异国情调的TSP限制。)

分支和绑定

通过“部分解决方案”,我的意思是将变量部分分配给0或1,其中分配1的边缘肯定在巡视中,并且分配0的边缘肯定是out。使用这些侧面约束评估Held-Karp可以在最佳巡视中给出一个较低的界限,该巡视尊重已经做出的决定(扩展)。

分支和绑定维护一组部分解决方案,其中至少有一个扩展到最佳解决方案。一个变体的伪代码,具有最佳优先回溯的深度优先搜索如下。

let h be an empty minheap of partial solutions, ordered by Held–Karp value
let bestsolsofar = null
let cursol be the partial solution with no variables assigned
loop
    while cursol is not a complete solution and cursol's H–K value is at least as good as the value of bestsolsofar
        choose a branching variable v
        let sol0 be cursol union {v -> 0}
        let sol1 be cursol union {v -> 1}
        evaluate sol0 and sol1
        let cursol be the better of the two; put the other in h
    end while
    if cursol is better than bestsolsofar then
        let bestsolsofar = cursol
        delete all heap nodes worse than cursol
    end if
    if h is empty then stop; we've found the optimal solution
    pop the minimum element of h and store it in cursol
end loop

分支和绑定的想法是有一个部分解决方案的搜索树。解决Held-Karp的问题在于LP的值最多是最佳巡回的长度OPT,但也推测至少是3/4 OPT(实际上,通常更接近OPT)。

我遗漏的伪代码中的一个细节是如何选择分支变量。目标通常是首先做出“硬”决策,因此修复一个值已接近0或1的变量可能并不明智。一种选择是选择最接近0.5,但有许多,其他很多。

修改

Java实现。 198个非空白,非注释行。我忘了单树不能将变量赋值给1,所以我通过找到一个树的度为&gt; 2的顶点并依次删除每个边来进行分支。此程序接受EUC_2D格式的TSPLIB实例,例如来自http://www2.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95/tsp/eil51.tspeil76.tsp以及eil101.tsplin105.tsp

// simple exact TSP solver based on branch-and-bound/Held--Karp
import java.io.*;
import java.util.*;
import java.util.regex.*;

public class TSP {
  // number of cities
  private int n;
  // city locations
  private double[] x;
  private double[] y;
  // cost matrix
  private double[][] cost;
  // matrix of adjusted costs
  private double[][] costWithPi;
  Node bestNode = new Node();

  public static void main(String[] args) throws IOException {
    // read the input in TSPLIB format
    // assume TYPE: TSP, EDGE_WEIGHT_TYPE: EUC_2D
    // no error checking
    TSP tsp = new TSP();
    tsp.readInput(new InputStreamReader(System.in));
    tsp.solve();
  }

  public void readInput(Reader r) throws IOException {
    BufferedReader in = new BufferedReader(r);
    Pattern specification = Pattern.compile("\\s*([A-Z_]+)\\s*(:\\s*([0-9]+))?\\s*");
    Pattern data = Pattern.compile("\\s*([0-9]+)\\s+([-+.0-9Ee]+)\\s+([-+.0-9Ee]+)\\s*");
    String line;
    while ((line = in.readLine()) != null) {
      Matcher m = specification.matcher(line);
      if (!m.matches()) continue;
      String keyword = m.group(1);
      if (keyword.equals("DIMENSION")) {
        n = Integer.parseInt(m.group(3));
        cost = new double[n][n];
      } else if (keyword.equals("NODE_COORD_SECTION")) {
        x = new double[n];
        y = new double[n];
        for (int k = 0; k < n; k++) {
          line = in.readLine();
          m = data.matcher(line);
          m.matches();
          int i = Integer.parseInt(m.group(1)) - 1;
          x[i] = Double.parseDouble(m.group(2));
          y[i] = Double.parseDouble(m.group(3));
        }
        // TSPLIB distances are rounded to the nearest integer to avoid the sum of square roots problem
        for (int i = 0; i < n; i++) {
          for (int j = 0; j < n; j++) {
            double dx = x[i] - x[j];
            double dy = y[i] - y[j];
            cost[i][j] = Math.rint(Math.sqrt(dx * dx + dy * dy));
          }
        }
      }
    }
  }

  public void solve() {
    bestNode.lowerBound = Double.MAX_VALUE;
    Node currentNode = new Node();
    currentNode.excluded = new boolean[n][n];
    costWithPi = new double[n][n];
    computeHeldKarp(currentNode);
    PriorityQueue<Node> pq = new PriorityQueue<Node>(11, new NodeComparator());
    do {
      do {
        boolean isTour = true;
        int i = -1;
        for (int j = 0; j < n; j++) {
          if (currentNode.degree[j] > 2 && (i < 0 || currentNode.degree[j] < currentNode.degree[i])) i = j;
        }
        if (i < 0) {
          if (currentNode.lowerBound < bestNode.lowerBound) {
            bestNode = currentNode;
            System.err.printf("%.0f", bestNode.lowerBound);
          }
          break;
        }
        System.err.printf(".");
        PriorityQueue<Node> children = new PriorityQueue<Node>(11, new NodeComparator());
        children.add(exclude(currentNode, i, currentNode.parent[i]));
        for (int j = 0; j < n; j++) {
          if (currentNode.parent[j] == i) children.add(exclude(currentNode, i, j));
        }
        currentNode = children.poll();
        pq.addAll(children);
      } while (currentNode.lowerBound < bestNode.lowerBound);
      System.err.printf("%n");
      currentNode = pq.poll();
    } while (currentNode != null && currentNode.lowerBound < bestNode.lowerBound);
    // output suitable for gnuplot
    // set style data vector
    System.out.printf("# %.0f%n", bestNode.lowerBound);
    int j = 0;
    do {
      int i = bestNode.parent[j];
      System.out.printf("%f\t%f\t%f\t%f%n", x[j], y[j], x[i] - x[j], y[i] - y[j]);
      j = i;
    } while (j != 0);
  }

  private Node exclude(Node node, int i, int j) {
    Node child = new Node();
    child.excluded = node.excluded.clone();
    child.excluded[i] = node.excluded[i].clone();
    child.excluded[j] = node.excluded[j].clone();
    child.excluded[i][j] = true;
    child.excluded[j][i] = true;
    computeHeldKarp(child);
    return child;
  }

  private void computeHeldKarp(Node node) {
    node.pi = new double[n];
    node.lowerBound = Double.MIN_VALUE;
    node.degree = new int[n];
    node.parent = new int[n];
    double lambda = 0.1;
    while (lambda > 1e-06) {
      double previousLowerBound = node.lowerBound;
      computeOneTree(node);
      if (!(node.lowerBound < bestNode.lowerBound)) return;
      if (!(node.lowerBound < previousLowerBound)) lambda *= 0.9;
      int denom = 0;
      for (int i = 1; i < n; i++) {
        int d = node.degree[i] - 2;
        denom += d * d;
      }
      if (denom == 0) return;
      double t = lambda * node.lowerBound / denom;
      for (int i = 1; i < n; i++) node.pi[i] += t * (node.degree[i] - 2);
    }
  }

  private void computeOneTree(Node node) {
    // compute adjusted costs
    node.lowerBound = 0.0;
    Arrays.fill(node.degree, 0);
    for (int i = 0; i < n; i++) {
      for (int j = 0; j < n; j++) costWithPi[i][j] = node.excluded[i][j] ? Double.MAX_VALUE : cost[i][j] + node.pi[i] + node.pi[j];
    }
    int firstNeighbor;
    int secondNeighbor;
    // find the two cheapest edges from 0
    if (costWithPi[0][2] < costWithPi[0][1]) {
      firstNeighbor = 2;
      secondNeighbor = 1;
    } else {
      firstNeighbor = 1;
      secondNeighbor = 2;
    }
    for (int j = 3; j < n; j++) {
      if (costWithPi[0][j] < costWithPi[0][secondNeighbor]) {
        if (costWithPi[0][j] < costWithPi[0][firstNeighbor]) {
          secondNeighbor = firstNeighbor;
          firstNeighbor = j;
        } else {
          secondNeighbor = j;
        }
      }
    }
    addEdge(node, 0, firstNeighbor);
    Arrays.fill(node.parent, firstNeighbor);
    node.parent[firstNeighbor] = 0;
    // compute the minimum spanning tree on nodes 1..n-1
    double[] minCost = costWithPi[firstNeighbor].clone();
    for (int k = 2; k < n; k++) {
      int i;
      for (i = 1; i < n; i++) {
        if (node.degree[i] == 0) break;
      }
      for (int j = i + 1; j < n; j++) {
        if (node.degree[j] == 0 && minCost[j] < minCost[i]) i = j;
      }
      addEdge(node, node.parent[i], i);
      for (int j = 1; j < n; j++) {
        if (node.degree[j] == 0 && costWithPi[i][j] < minCost[j]) {
          minCost[j] = costWithPi[i][j];
          node.parent[j] = i;
        }
      }
    }
    addEdge(node, 0, secondNeighbor);
    node.parent[0] = secondNeighbor;
    node.lowerBound = Math.rint(node.lowerBound);
  }

  private void addEdge(Node node, int i, int j) {
    double q = node.lowerBound;
    node.lowerBound += costWithPi[i][j];
    node.degree[i]++;
    node.degree[j]++;
  }
}

class Node {
  public boolean[][] excluded;
  // Held--Karp solution
  public double[] pi;
  public double lowerBound;
  public int[] degree;
  public int[] parent;
}

class NodeComparator implements Comparator<Node> {
  public int compare(Node a, Node b) {
    return Double.compare(a.lowerBound, b.lowerBound);
  }
}

答案 1 :(得分:3)

如果你的图形满足三角形不等式,并且你想要在最佳值内保证3/2,我建议使用christofides算法。我在phpclasses.org上用PHP编写了一个实现。

答案 2 :(得分:1)

截至2013年,仅使用Cplex中的确切配方就可以解决100个城市。为每个顶点添加度数方程,但仅在出现时包括避免地形的约束。其中大部分都没有必要。 Cplex有一个例子。

你应该可以解决100个城市。每次找到新的子量时都必须进行迭代。我在这里运行了一个例子,在几分钟和100次迭代之后,我得到了我的结果。

答案 3 :(得分:1)

我从协调库中采用了Held-Karp算法,并在0.15秒内解决了25个城市。这个表现对我来说非常好!您可以从concorde库中提取hold-karp的代码(在ANSI C中编写):http://www.math.uwaterloo.ca/tsp/concorde/downloads/downloads.htm。如果下载的扩展名为gz,则应为tgz。您可能需要重命名它。然后你应该在VC ++中进行少量调整。首先取文件heldkarp h和c(重命名为cpp)和其他约5个文件,进行调整,它应该使用edgelen:euclid_ceiling_edgelen调用CCheldkarp_small(...)。

答案 4 :(得分:0)

TSP是一个NP难题。 (据我们所知),在多项式时间内没有NP难问题的算法,所以你要求一些不存在的东西。

它要么足够快,要在合理的时间内完成,然后就不完全准确,或者确切但不会在你的一生中完成100个城市。

答案 5 :(得分:0)

给出一个愚蠢的答案:我也是。每个人都对这种算法感兴趣,但正如其他人已经说过的那样:我不存在(但是?)存在。 Esp是精确的,200个节点,几秒运行时间的组合,只有200行代码是不可能的。你已经知道NP难了,如果你对渐近行为有一点点印象,你应该知道没有办法实现这个(除非你证明NP = P,甚至我会说那是不可能的)。即使是确切的商业解算器也需要这些实例远远超过几秒钟,而且你可以想象它们有超过200行代码(即使你只考虑它们的内核)。

编辑:维基算法是该领域的“常见嫌疑人”:线性编程和分支定界。他们针对具有数千个节点的实例的解决方案需要花费数年的时间才能解决(他们只是将非常多的CPU并行执行,因此他们可以更快地完成它)。有些甚至使用分支定界问题特定的边界知识,因此它们不是一般方法。

分支和绑定只是枚举所有可能的路径(例如,使用回溯),并在它有一个解决方案后应用,以便在它可以证明结果不比已经找到的解决方案更好时停止开始递归(例如,如果你只是访问过你的2个城市,路径已经超过了200个城市之旅。你可以放弃所有以2个城市组合开始的旅行。在这里,您可以在告诉您的函数中投入非常多的问题特定知识,该路径不会比已经找到的解决方案更好。它越好,您需要查看的路径越少,算法就越快。

线性规划是一种优化方法,因此可以解决线性不等式问题。它适用于多项式时间(单纯形式,但在这里并不重要),但解决方案是真实的。当你有一个额外的约束条件,解决方案必须是整数时,它就会得到NP完全。对于小实例,可以是例如一种方法来解决它,然后查看解决方案的哪个变量违反整数部分并添加加法不等式来改变它(这称为切割平面,名称来自不等式定义(高维)平面的事实,解空间是一个多边形,通过添加额外的不等式,你可以用多边形平面切割一些东西。这个主题非常复杂,当你不想深入研究数学时,即使是简单的单纯形也很难理解。有几本好书,其中一个更好的是来自Chvatal,Linear Programming,但还有几本。

答案 6 :(得分:0)

我有一个理论,但我从来没有时间去追求它:

TSP是一个边界问题(所有点位于周边的单一形状),其中最佳解决方案是具有最短周长的解决方案。

有很多简单的方法可以获得位于最小边界周边的所有点(想象一个大弹性带在大板上的一堆钉子周围伸展。)

我的理论是,如果你开始推进弹性带,使得带的长度在周长上的相邻点之间增加相同的量,并且每个段保持为椭圆弧的形状,拉伸的弹性将在非最佳路径上交叉点之前的最佳路径上的交叉点。请参阅绘图椭圆上的this page on mathopenref.com - 特别是步骤5和6.边界周边的点可以看作下图中椭圆(F1,F2)的焦点。

Step 5.

Step 6.

我不知道的是,如果在添加每个新点之后需要重置“气泡拉伸”过程,或者现有的“气泡”继续增长并且周边的每个新点仅导致本地化“泡沫“变成两个线段。我会留下那个让你弄明白的。