Оптимизированные алгоритмы TSP - PullRequest
17 голосов
/ 23 августа 2011

Меня интересуют способы улучшить или придумать алгоритмы, способные решить проблему коммивояжера для примерно n = 100 to 200 городов.

Ссылка на википедию, которую я давал списки различныхоптимизаций, но это происходит на довольно высоком уровне, и я не знаю, как на самом деле реализовать их в коде.

Существуют промышленные решатели, такие как Concorde , но они слишком сложны для того, что я хочу, и классические решения, которые затопляют поиски TSP, представляют все рандомизированные алгоритмы или классические алгоритмы обратного отслеживания или динамического программирования, которые работают только для примерно 20 городов.

ТакКто-нибудь знает, как реализовать простой (под простым я имею в виду, что реализация не занимает более 100-200 строк кода) решатель TSP, который работает в разумные сроки (несколько секунд) как минимум для 100 городов?Меня интересуют только точные решения.

Вы можете предположить, что входные данные будут генерироваться случайным образом, поэтому меня не волнуют входные данные, специально предназначенные для взлома определенного алгоритма.

Ответы [ 7 ]

51 голосов
/ 23 августа 2011

200 строк и отсутствие библиотек - жесткое ограничение. Продвинутые решатели используют ветвление и связывают с релаксацией Хелда-Карпа, и я не уверен, что даже самая базовая версия этого уместится в 200 нормальных линий. Тем не менее, вот схема.

Хельд Карп

Один из способов написать TSP в виде целочисленной программы заключается в следующем (Данциг, Фулкерсон, Джонсон). Для всех ребер e постоянная w e обозначает длину ребра e, а переменная x e равна 1, если ребро e находится в туре, и 0 в противном случае. Для всех подмножеств S вершин через 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, x e в {0, 1}

Условие 1 гарантирует, что множество ребер является набором туров. Условие 2 гарантирует, что есть только один. (В противном случае, пусть S будет множеством вершин, посещаемых одним из туров.) Релаксация Хельда-Карпа получается с помощью этого изменения.

3. для всех ребер e в E, x e в {0, 1}
3. для всех ребер e в E, 0 ≤ x e ≤ 1

Held – Karp - линейная программа, но она имеет экспоненциальное число ограничений. Один из способов ее решения - ввести множители Лагранжа, а затем провести субградиентную оптимизацию. Это сводится к циклу, который вычисляет минимальное остовное дерево, а затем обновляет некоторые векторы, но детали являются своего рода сложными. Помимо «Held – Karp» и «subgradient (спуск | оптимизация)», «1-tree» - еще один полезный поисковый термин.

(Более медленной альтернативой является написание решателя LP и введение ограничений подуровня, поскольку они нарушаются предыдущей оптимой. Это означает написание решателя LP и процедуры min-cut, которая также является большим количеством кода, но она может быть расширена до более экзотические ограничения TSP.)

Ветвь и переплет

Под «частичным решением» я подразумеваю частичное присвоение переменных 0 или 1, где назначенное ребро 1 определенно находится в туре, а назначенное ребро 0 определенно отсутствует. Оценка Хельда-Карпа с этими побочными ограничениями дает более низкую оценку оптимального тура с учетом уже принятых решений (расширение).

Ветвление и граница поддерживают множество частичных решений, по крайней мере одно из которых распространяется на оптимальное решение. Псевдокод для одного варианта поиска в глубину с обратным отслеживанием по принципу «лучший по началу» выглядит следующим образом.

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

Идея ветвления и границы заключается в том, что существует дерево поиска частичных решений. Смысл решения Хелда-Карпа заключается в том, что значение LP составляет не более длины OPT оптимального тура, но также предположительно составляет не менее 3/4 OPT (на практике, обычно ближе к OPT).

Единственная деталь в псевдокоде, которую я пропустил, это как выбрать переменную ветвления. Обычно цель состоит в том, чтобы сначала принять «жесткие» решения, поэтому фиксировать переменную, значение которой уже близко к 0 или 1, вероятно, нецелесообразно. Одним из вариантов является выбор ближайшего к 0,5, но есть много-много других.

EDIT

Реализация Java. 198 непустых, некомментированных строк. Я забыл, что 1-деревья не работают с присвоением переменных 1, поэтому я выполняю ветвление, находя вершину, у которой 1-дерево имеет степень> 2, и удаляю каждое ребро по очереди. Эта программа принимает экземпляры TSPLIB в формате EUC_2D, например, eil51.tsp и eil76.tsp и eil101.tsp и lin105.tsp из http://www2.iwr.uni -heidelberg.de / groups / comopt / software / TSPLIB95 / 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);
  }
}
3 голосов
/ 23 августа 2011

Если ваш график удовлетворяет неравенству треугольника, и вы хотите гарантировать 3/2 в пределах оптимального, я предлагаю алгоритм christofides. Я написал реализацию в php на phpclasses.org.

1 голос
/ 06 ноября 2013

Я взял алгоритм Хельда-Карпа из библиотеки конкордов, и 25 городов решаются за 0,15 секунды. Это представление совершенно хорошо для меня! Вы можете извлечь код (записанный в ANSI C) hold-karp из библиотеки concorde: http://www.math.uwaterloo.ca/tsp/concorde/downloads/downloads.htm. Если загрузка имеет расширение gz, это должно быть tgz. Возможно, вам придется переименовать его. Затем вы должны внести небольшие изменения в порт в VC ++. Сначала возьмите файл holdkarp h и c (переименуйте его в cpp) и другие около 5 файлов, внесите коррективы, и он должен работать, вызывая CCheldkarp_small (...) с edgelen: euclid_ceiling_edgelen.

1 голос
/ 22 августа 2013

По состоянию на 2013 год можно решить для 100 городов, используя только точную формулировку в Cplex.Добавляйте уравнения степени для каждой вершины, но включайте ограничения, исключающие обход, только по мере их появления.Большинство из них не являются необходимыми.У Cplex есть пример по этому вопросу.

Вы должны быть в состоянии решить для 100 городов.Вам придется повторять каждый раз, когда будет найден новый подпроток.Я запустил пример здесь, и через пару минут и 100 итераций я получил свои результаты.

0 голосов
/ 23 августа 2011

У меня есть теория, но у меня никогда не было времени ее развить:

TSP - это ограничивающая проблема (единственная форма, где все точки лежат на периметре), где оптимальное решение - это решениеу которого самый короткий периметр.

Существует множество простых способов получить все точки, которые лежат на минимальном ограничивающем периметре (представьте себе большую эластичную полосу, растянутую вокруг гвоздей на большой доске.)

Моя теория состоит в том, что если вы начнете нажимать на эластичную ленту так, чтобы длина полосы увеличивалась на ту же величину между соседними точками по периметру, и каждый сегмент оставался в форме эллиптической дуги, растянутая эластичнаябудет пересекать точки на оптимальном пути до пересечения точек на неоптимальных путях.См. эту страницу на mathopenref.com , посвященную рисованию эллипсов - в частности, этапы 5 и 6. Точки ограничивающего периметра можно рассматривать как фокусные точки эллипса (F1, F2) на изображениях ниже.

Step 5.

Step 6.

Чего я не знаю, так это того, нужно ли сбрасывать процесс «растягивания пузырьков» после добавления каждой новой точки или еслисуществующие «пузыри» продолжают расти, и каждая новая точка на периметре заставляет только локализованный «пузырь» превращаться в два отрезка.Я оставлю это на ваше усмотрение.

0 голосов
/ 23 августа 2011

Чтобы дать тупой ответ: я тоже.Каждый заинтересован в таком алгоритме, но, как уже говорили другие: я не существует (пока?).Esp Ваша комбинация точных, 200 узлов, несколько секунд выполнения и всего 200 строк кода невозможна.Вы уже знаете, что это NP сложнее, и если у вас есть малейшее впечатление асимптотического поведения, вы должны знать, что нет никакого способа достичь этого (кроме как доказать, что NP = P, и даже что я бы сказал, что это невозможно).Даже точные коммерческие решатели требуют для таких экземпляров гораздо больше, чем несколько секунд, и, как вы можете себе представить, они содержат более 200 строк кода (даже если вы просто рассматриваете их ядра).

РЕДАКТИРОВАТЬ: алгоритмы вики являются "обычными подозреваемыми" области: линейное программирование и ветвления и ограничения.Их решения для экземпляров с тысячами узлов заняли годы (они просто сделали это с очень очень параллельными процессорами, чтобы они могли делать это быстрее).Некоторые даже используют для решения проблем ветвления и привязки специальные знания, поэтому они не являются общими подходами.

Ветвь и граница просто перечисляют все возможные пути (например, с обратным отслеживанием) и применяются, когда у него есть решение для остановки запущенной рекурсии, когда он может доказать, что результат не лучше, чем уже найденное решение (например,если вы только что посетили 2 из своих городов, и путь уже длиннее, чем экскурсия по 200. Вы можете отказаться от всех туров, которые начинаются с этой комбинации 2 городов).Здесь вы можете вложить очень много знаний о проблемах в функцию, которая говорит вам, что путь не будет лучше, чем уже найденное решение.Чем лучше, тем меньше путей, на которые вы должны смотреть, тем быстрее ваш алгоритм.

Линейное программирование - это метод оптимизации, поэтому решайте проблемы линейного неравенства.Он работает за полиномиальное время (симплекс просто практически, но это не имеет значения), но решение реально.Когда у вас есть дополнительное ограничение, что решение должно быть целочисленным, оно становится NP-полным.Для небольших случаев это возможно, например, один метод, чтобы решить его, затем посмотреть, какая переменная решения нарушает целочисленную часть, и добавить дополнительные неравенства, чтобы изменить ее (это называется плоскостью среза, название происходит от того факта, что неравенства определяют(многомерная) плоскость, пространство решений является многогранником, и, добавляя дополнительные неравенства, вы что-то вырезаете плоскостью из многогранника).Тема очень сложная, и даже простой простой симплекс трудно понять, когда вы не хотите углубляться в математику.Есть несколько хороших книг о том, что один из лучших игроков из Chvatal, Linear Programming, но есть еще несколько.

0 голосов
/ 23 августа 2011

TSP - это NP-сложная проблема. (Насколько нам известно) не существует алгоритма NP-сложных задач, который выполняется за полиномиальное время, поэтому вы запрашиваете что-то, чего не существует.

Это либо достаточно быстро, чтобы закончить за разумное время, а затем оно не является точным, либо точным, но не завершится за 100 лет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...