Official

E - 配達ドライバーの巡回 / Delivery Driver's Route Editorial by kyopro_friends


この問題は巡回セールスマン問題としてよく知られている問題であり、 bitDP により解くことができます。

まずワーシャルフロイド法などを用い「町 \(i\) から町 \(j\) へ最短時間」を求めておき、\(C_{i,j}\) とします。

「次に初めて訪れる町」を順に考えることで問題は「町 \(2,3,\dots, N\)\(N-1\) 個の町を適当な順に並べて \(P_1,P_2,\dots,P_{N-1}\) とするとき、\(C_{1,P_1}+C_{P_1,P_2}+\dots+C_{P_{N-2},P_{N-1}}+C_{P_{N-1},1}\) を最小化せよ」となります。

\(\mathrm{DP}[S][i]\) を「訪問済みの町の集合(※)が \(S\) で、町 \(i\) にいる状態になるまでの最短時間」と定めます(※スタート地点の町 \(1\) は含まず、ゴール地点の町 \(1\) は含む)。 次にどの町に行くかを考える配るDPにより

\(\mathrm{DP}[S\cup\{j\}][j]\leftarrow\min(\mathrm{DP}[S\cup\{j\}][j], \mathrm{dp}[S][i]+C_{i,j})\)

と更新することができます。初期値は \(\mathrm{DP}[\emptyset][1]\) のみ \(0\) で、それ以外は \(\infty\) とします。

このDPでは、更新先の \(S\) が必ず大きくなるため、\(S\) の大きさが小さい順に計算することでこの DP テーブルを埋めることができます。答えは \(\mathrm{DP}[\{1,2,\dots,N\}][1]\) となります。

状態数が \(O(N2^N)\) 、遷移が \(O(N)\) 通りあるため DP の計算には \(O(N^22^N)\) 時間かかります。

実装の工夫

以上までの説明でこの問題を解くことは可能ですが、 DP のキーとして集合を持つことや、要素数の小さい順に DP テーブルを埋めるのは実装が複雑になる傾向にあります。以下ではより簡潔な実装を紹介します。

説明の都合上、町の添字を \(1\) ずらして、町 \(0,1,\dots,N-1\) とします。

\(0\) 以上 \(N\) 未満の整数からなる集合」は、\(\sum_{i\in S}2^i\) により「\(0\) 以上 \(2^N\) 未満の整数」に1対1に対応させることができます。これにより集合 \(S\) と対応する整数を \(X_S\) と表すことにします。プログラミング的には、\(i\)\(S\) に入っていることと、\(X_S\)\(i\)-bit 目が \(1\) であることは同値となります。

この対応により、 DP のキーとして持つものが集合ではなく整数となるため、実装が容易になります。さらにこの対応において、 2 つの集合 \(S,T\) に対して、\(S\subset T\) ならば \(X_S\leq X_T\) となります。よって、 DP テーブルの更新を「\(S\) のサイズの昇順」ではなく「\(X_S\) の昇順」で行うことができるようになります。

競技プログラミングにおいては、集合を扱う DP のことを、以上の工夫まで含めて bitDP と呼びます。

実装例 (C++)

#include<bits/stdc++.h>
using namespace std;

int main(){
  int n, m;
  cin >> n >> m;
  int INF = 1e9;
  vector<vector<int>>G(n, vector<int>(n, INF));
  for(int i=0; i<n; i++) G[i][i] = 0;
  for(int i=0; i<m; i++){
    int u, v, w;
    cin >> u >> v >> w;
    u--, v--;
    G[u][v] = w;
    G[v][u] = w;
  }

  for(int k=0; k<n; k++){
    for(int i=0; i<n; i++){
      for(int j=0; j<n; j++){
        G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
      }
    }
  }

  vector<vector<int>>dp(1<<n, vector<int>(n, INF));
  dp[0][0] = 0;
  for(int s=0; s<1<<n; s++){
    for(int v=0; v<n; v++){
      for(int vv=0; vv<n; vv++){
        dp[s^(1<<vv)][vv] = min(dp[s^(1<<vv)][vv], dp[s][v] + G[v][vv]);
      }
    }
  }

  if(dp[(1<<n)-1][0] != INF){
    cout << dp[(1<<n)-1][0] << endl;
  }else{
    cout << -1 << endl;
  }
}

実装例 (Python)

N, M = map(int, input().split())
INF = 10**9
G = [[INF] * N for _ in range(N)]
for i in range(N):
  G[i][i]=0
for _ in range(M):
  u, v, w = map(int, input().split())
  u -= 1
  v -= 1
  G[u][v] = w
  G[v][u] = w

for k in range(N):
  for i in range(N):
    for j in range(N):
      G[i][j] = min(G[i][j], G[i][k] + G[k][j])

dp=[[INF] * N for _ in range(1<<N)]

dp[0][0] = 0
for s in range(1<<N):
  for v in range(N):
    for vv in range(N):
      dp[s^(1<<vv)][vv] = min(dp[s^(1<<vv)][vv], dp[s][v] + G[v][vv])

if dp[(1<<N)-1][0] != INF:
  print(dp[(1<<N)-1][0])
else:
  print(-1)

posted:
last update: