🎉

QUBO++入門:巡回セールスマン問題

2025/02/17に公開

巡回セールスマン問題のQUBO化

巡回セールスマン問題(Traveling Salesman Problem, TSP)とは,複数のノードと全ノード間の距離が与えられたとき,すべてのノードを一度ずつ訪問し,最初のノードに戻るツアー(巡回路)のうち,総移動距離が最小となるものを求める問題です.本稿では,簡単のため,2次元平面上の整数座標を持つ
n個のノードが与えられている場合を考えます.ノード間の距離としては,ユークリッド距離を整数に丸めた値を使用します.

この巡回セールスマン問題を,QUBO++を用いてQUBO式で表現し,その解を求めるプログラムを設計します.例えば,以下は n=8 の場合における入力ノードの例です.この8つのノードを巡り,元のノードに戻るツアーを巡回セールスマン問題の解とし,そのうち総移動距離が最小となるものを探索します.

巡回セールスマン問題の解は,全ノードの順列で表すことができます.そこで,

  • 前の記事:QUBO++入門:行列操作(順列生成)
    で使用した順列生成のためのQUBO式を利用します.二値変数で構成される大きさ n\times nの行列
    xを用いて,n個のノードの順列を表現します.このxの各行および各列がone-hotベクトルとなり,順列を表します.順列に基づく総移動距離は,次の式で計算できます
\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}\sum_{k=0}^{n-1} d_{j,k}x_{i,j}x_{(i+1)\bmod n,k}

ここで,d_{i,j}はノードiとノードjの距離を表します.x{i,j}=1かつx_{(i+1)\bmod n,k}=1の場合,すなわち,順列でi番目のノードがjで,次の(i+1)\bmod nがノードkであるとき,移動距離d_{j,k}が加算されます.この総移動距離が最小となるxの値を求めれば,それが巡回セールスマンの最適解を表します.

QUBO++プログラム

先の9ノードの場合に巡回セールスマン問題を解くQUBO++プログラムは以下のようになります.
二値変数が64個となり,Exhaustive Solverで解くには多すぎるため,Easy Solverを使用します.

#include "qbpp.hpp"
#include "qbpp_easy_solver.hpp"

int distance(const std::pair<int, int> &p1, const std::pair<int, int> &p2) {
  return static_cast<int>(
      std::round(std::sqrt((p1.first - p2.first) * (p1.first - p2.first) +
                           (p1.second - p2.second) * (p1.second - p2.second))));
}

int main() {
  std::vector<std::pair<int, int>> nodes = {
      {0, 0}, {10, 0}, {20, 0}, {0, 10}, {10, 10}, {20, 10}, {0, 20}, {10, 20}};

  auto x = qbpp::var("x", nodes.size(), nodes.size());
  auto permutation = qbpp::sum(qbpp::sum(x) == 1) +
                     qbpp::sum(qbpp::sum(qbpp::transpose(x)) == 1);

  auto dist = qbpp::expr();
  for (size_t i = 0; i < nodes.size(); i++) {
    for (size_t j = 0; j < nodes.size(); j++) {
      for (size_t k = 0; k < nodes.size(); k++) {
        if (j == k) continue;
        dist += distance(nodes[j], nodes[k]) * x[i][j] *
                x[(i + 1) % nodes.size()][k];
      }
    }
  }

  auto f = simplify_as_binary(permutation * 100 + dist);

  auto quad_model = qbpp::QuadModel(f);
  std::cout << "var_count = " << quad_model.var_count() << std::endl;
  std::cout << "linear_term_count = " << quad_model.term_count(1) << std::endl;
  std::cout << "quadratic_term_count = " << quad_model.term_count(2)
            << std::endl;

  auto solver = qbpp::easy_solver::EasySolver(quad_model);
  solver.set_time_limit(1);
  auto sol = solver.search();

  std::cout << sol << std::endl;
  auto tour = qbpp::onehot_to_int(sol.get(x));
  std::cout << str(tour, "tour") << std::endl;
}
  • 関数distanceは,2つのノードの座標を引数として受け取り,その距離を計算して返します.
  • 変数nodesには8個のノードの座標が格納されます.
  • xを二値変数の9\times 9行列として生成します.
  • permutaitonxの各行と各列がone-hotベクトル,つまり順列を表すときに最小値0をとるQUBO式を代入しています.
  • 三重のforループを用いて,順列により表されるツアーの総移動距離を計算するQUBO式distを求めています.
  • QUBO式fは,permutationを優先するために100を掛けた後,distを加算しています.
  • fqbpp::QuadModelオブジェクトquad_modelに変換し,変数,一次項,二次項の個数を表示しています.
  • Easy Solverを用いて,解を探索しています.ここで制限時間を1秒に設定しています.
  • 解xが保持する順列をtourに代入し,表示しています.

このプログラムを実行すると次の結果が得られます.

var_count = 64
linear_term_count = 64
quadratic_term_count = 1792
80:{{x[0][0],0},{x[0][1],0},{x[0][2],0},{x[0][3],0},{x[0][4],1},{x[0][5],0},{x[0][6],0},{x[0][7],0},{x[1][0],0},{x[1][1],0},{x[1][2],0},{x[1][3],0},{x[1][4],0},{x[1][5],0},{x[1][6],0},{x[1][7],1},{x[2][0],0},{x[2][1],0},{x[2][2],0},{x[2][3],0},{x[2][4],0},{x[2][5],0},{x[2][6],1},{x[2][7],0},{x[3][0],0},{x[3][1],0},{x[3][2],0},{x[3][3],1},{x[3][4],0},{x[3][5],0},{x[3][6],0},{x[3][7],0},{x[4][0],1},{x[4][1],0},{x[4][2],0},{x[4][3],0},{x[4][4],0},{x[4][5],0},{x[4][6],0},{x[4][7],0},{x[5][0],0},{x[5][1],1},{x[5][2],0},{x[5][3],0},{x[5][4],0},{x[5][5],0},{x[5][6],0},{x[5][7],0},{x[6][0],0},{x[6][1],0},{x[6][2],1},{x[6][3],0},{x[6][4],0},{x[6][5],0},{x[6][6],0},{x[6][7],0},{x[7][0],0},{x[7][1],0},{x[7][2],0},{x[7][3],0},{x[7][4],0},{x[7][5],1},{x[7][6],0},{x[7][7],0}}
{tour[0],4},{tour[1],7},{tour[2],6},{tour[3],3},{tour[4],0},{tour[5],1},{tour[6],2},{tour[7],5}

変数の個数は64個,一次項の個数は64個,二次項の個数は1792個です.Easy Solverを用いることで,エネルギー値80の解が得られています.このエネルギー値はツアーの総移動距離を表します.得られたツアーは,次の順序でノードを巡るものです.ツアーは,4,7,6,3,0,1,2,5の順でノードを巡回します.このツアーを図示すると,最適解が得られていることが確認できます.

まとめ

  • nノードの巡回セールスマン問題をQUBO形式で解く場合,大きさn \times nの二値変数行列を用います.
  • このQUBO式は,各行および各列がone-hotベクトルとなる制約と,総移動距離を最小化する目的関数を組み合わせたものです.
  • 変数の個数が多い場合,Exhaustive Solverでは現実的な時間内に探索が終了しないため,Easy Solverを使用します.

QUBO++情報

広島大学コンピューティングラボ

Discussion