問題
$N$ 頂点 $M$ 辺の有向グラフがある。辺 $i$ は頂点 $A_i$ から $B_i$ への長さ $C_i$ の一方通行である(自己ループ・多重辺があってもよい)。グラフは強連結である。
頂点 $1$ を出発して頂点 $1$ に戻る経路で、すべての辺を少なくとも 1 回通るもののうち、長さの和が最小のものを 1 つ求めよ。
1 行目に長さの和の最小値、2 行目に経路が通る辺の本数 $K$、3 行目に通る辺の番号を通る順に空白区切りで出力せよ。最小のものが複数あるときはどれを出力してもよい。
入力形式
N M
A_1 B_1 C_1
A_2 B_2 C_2
...
A_M B_M C_M
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le200$ |
| 2 | $1\le M\le5000$ |
| 3 | $1\le A_i,B_i\le N$ |
| 4 | $1\le C_i\le10^6$ |
| 5 | グラフは強連結 |
| 6 | 入力はすべて整数 |
入出力例
入力例1
3 4
1 2 1
2 3 1
3 1 1
1 3 5出力例1
9
5
1 2 3 4 3(辺 4($1\to3$)を通った後、$3\to1$ を 2 回目に通って戻る)
入力例2
2 3
1 2 1
1 2 2
2 1 5出力例2
13
4
1 3 2 3入力例3
5 8
4 1 6
3 4 3
2 1 2
4 1 1
5 2 5
1 3 2
1 5 9
2 3 6出力例3
51
11
6 2 1 7 5 3 7 5 8 2 4(入次数 − 出次数 が $+1$ の頂点は 1 と 3、$-1$ の頂点は 2 と 4。最短距離は $1\to2$ が 14、$1\to4$ が 5、$3\to2$ が 18、$3\to4$ が 3。組み方は $\{1\to2,3\to4\}=17$ と $\{1\to4,3\to2\}=23$。頂点 1 から「いちばん近い相手」を貪欲に選ぶと 23 を選び、答えが 57 になる。3 例とも全探索で確認)
背景と出典
有向版の郵便配達問題は Edmonds と Johnson の 1973 年の論文「Matching, Euler tours and the Chinese postman」で、無向版のマッチングと並べて扱われている、と私は記憶している(本セッションでは原典を参照していない)。本問は自作。
なぜ最小費用流か: 閉路で各辺を使う回数を $x_e\ge1$ とすると、閉路があるための条件は各頂点で(入ってくる回数)=(出ていく回数)。$x_e=1+f_e$ と書けば、$f_e$ は
$$\sum_{e:\ \to v}f_e-\sum_{e:\ v\to}f_e=\text{out}(v)-\text{in}(v)$$
を満たす非負の流れで、目的は $\sum C_ef_e$ の最小化。これは「入次数 > 出次数 の頂点から、出次数 > 入次数 の頂点へ、差の分だけ流す」最小費用流そのもの。強連結なので必ず実行可能で、得られた $x_e$ の多重グラフは各頂点で入出が等しく連結だから、オイラー閉路がある。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
答えの経路を「各辺を何回通るか」に言い換える。どんな回数の組なら、1 本の閉じた経路として通れるか(オイラーの定理)。
ヒント2(アプローチ)
各辺を 1 回ずつ通ると、頂点 $v$ で入次数と出次数が $\text{bal}(v)=\text{in}(v)-\text{out}(v)$ だけずれる。$\text{bal}>0$ の頂点から $\text{bal}<0$ の頂点へ、元の辺をたどって追加で歩く道を足して差を埋める。どの頂点からどの頂点へ何本の道を足すかは、最小費用流で一度に決まる。
ヒント3(誘導)
超頂点 $S\to v$(容量 $\text{bal}(v)$、$\text{bal}>0$)、$v\to T$(容量 $-\text{bal}(v)$)、元の辺は容量 $\infty$・費用 $C_i$。流量 $\sum_{\text{bal}>0}\text{bal}(v)$ を流す。辺 $i$ に流れた量を $f_i$ とし、辺 $i$ を $1+f_i$ 本に増やした多重グラフで、頂点 $1$ からオイラー閉路をたどる。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from heapq import heappush, heappop
def main():
data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
A = [0] * m; B = [0] * m; C = [0] * m
bal = [0] * n # 入次数 − 出次数
base = 0
for e in range(m):
a, b, c = int(data[2 + 3 * e]) - 1, int(data[3 + 3 * e]) - 1, int(data[4 + 3 * e])
A[e], B[e], C[e] = a, b, c
bal[b] += 1
bal[a] -= 1
base += c
# 最小費用流: S → (入 > 出 の頂点) 容量 bal、(出 > 入 の頂点) → T 容量 −bal、元の辺は容量 ∞ で費用 c
S, T = n, n + 1
V = n + 2
to, cap, cost, head, nxt = [], [], [], [-1] * V, []
def add_edge(u, v, cp, cs):
for x, y, p, q in ((u, v, cp, cs), (v, u, 0, -cs)):
to.append(y); cap.append(p); cost.append(q); nxt.append(head[x]); head[x] = len(to) - 1
INF = 1 << 60
for e in range(m):
add_edge(A[e], B[e], INF, C[e]) # 辺 e の順向きは添字 2e
need = 0
for v in range(n):
if bal[v] > 0:
add_edge(S, v, bal[v], 0)
need += bal[v]
elif bal[v] < 0:
add_edge(v, T, -bal[v], 0)
h = [0] * V # ポテンシャル(費用は非負なので 0 から始めてよい)
extra = 0
flowed = 0
while flowed < need:
dist = [INF] * V
pe = [-1] * V
dist[S] = 0
pq = [(0, S)]
while pq:
d, u = heappop(pq)
if d > dist[u]:
continue
i = head[u]
hu = h[u]
while i != -1:
if cap[i] > 0:
v = to[i]
nd = d + cost[i] + hu - h[v]
if nd < dist[v]:
dist[v] = nd
pe[v] = i
heappush(pq, (nd, v))
i = nxt[i]
for v in range(V):
if dist[v] < INF:
h[v] += dist[v]
# 経路上の最小残余容量だけ流す(内側の辺は ∞ なので、S か T の辺が必ず飽和する)
f = need - flowed
v = T
while v != S:
i = pe[v]
if cap[i] < f:
f = cap[i]
v = to[i ^ 1]
v = T
while v != S:
i = pe[v]
cap[i] -= f
cap[i ^ 1] += f
v = to[i ^ 1]
flowed += f
extra += f * (h[T] - h[S])
# 各辺を 1 + (流れた量) 回使う多重グラフでオイラー閉路(Hierholzer、反復版)
rem = [1 + cap[2 * e + 1] for e in range(m)] # 逆辺の容量 = 流れた量
out = [[] for _ in range(n)]
for e in range(m):
out[A[e]].append(e)
ptr = [0] * n
stack = [(0, -1)] # (頂点, そこへ来た辺)
walk = []
while stack:
u, e_in = stack[-1]
lst = out[u]
while ptr[u] < len(lst) and rem[lst[ptr[u]]] == 0:
ptr[u] += 1
if ptr[u] == len(lst):
stack.pop()
if e_in >= 0:
walk.append(e_in)
else:
e = lst[ptr[u]]
rem[e] -= 1
stack.append((B[e], e))
walk.reverse()
sys.stdout.write(f"{base + extra}\n{len(walk)}\n{' '.join(str(e + 1) for e in walk)}\n")
main()
計算量: 最小費用流は Dijkstra(ポテンシャル付き)を増加路の本数だけ回す。内側の辺は容量 $\infty$ なので、1 回の増加で $S$ 側か $T$ 側の辺が必ず 1 本飽和し、増加は $N$ 回以下。全体で $O(N\cdot M\log N)$。オイラー閉路は経路の長さ $K$ に比例し、$K\le M+(\text{流量})\cdot(N-1)$ で最大 $10^6$ 程度。$N=200$、$M=5000$ で 0.25〜0.35 s($K=483404$ のケースを含む。Python 3.14.7、M1 Mac、負荷平均 5〜6 のときの実測)。
Step-by-Step 解説
各辺を 1 回ずつ通るとして、$\text{bal}(v)=\text{in}(v)-\text{out}(v)$ を求める。$\sum_v\text{bal}(v)=0$。$\text{bal}(v)>0$ の頂点は「入ってきたのに出ていけない」回数が $\text{bal}(v)$ ある。
$\text{bal}>0$ の頂点から $\text{bal}<0$ の頂点へ、元の辺をたどる道を合わせて $\sum\text{bal}^+$ 本足す。道の組み方(誰と誰を組むか)と道そのもの(最短路)を同時に決めるのが最小費用流。容量 $\infty$ の辺に費用 $C_i$ を付けるので、流れは自然に最短路に沿う。
辺 $i$ の逆辺の残余容量が、辺 $i$ に流れた量 $f_i$。辺 $i$ を $1+f_i$ 回使う。長さの和は $\sum C_i+(\text{最小費用})$。
スタックに(頂点, そこへ来た辺)を積み、まだ使える辺があれば進み、無ければ戻りながら辺を記録する。記録を逆順にすると、頂点 1 から始まって 1 に戻る経路になる。再帰で書くと $K=4.8\times10^5$ の深さになるので、反復で書く。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| (頂点, 通った辺の集合) の Dijkstra | $O(2^M\cdot M)$ | 検算用のみ | $M\le8$ のランダム 300 件の正解に使用 |
| 全点対最短路+余りと不足の組み方を全列挙 | $O(N^3+k!)$ | 検算用のみ | 不足の合計 $k\le7$、$N\le30$ の 200 件の正解に使用(流れとは独立な検算) |
| 余りの頂点ごとに、いちばん近い不足の頂点と組む(貪欲) | $O(N^3)$ | 不採用 | 200 件中 29 件で最小にならない(入力例 3 が反例) |
| 全点対最短路+ハンガリー法(割当問題) | $O(N^3+k^3)$ | 不採用 | 正しいが、$\text{bal}$ の大きい頂点を $\text{bal}$ 個に複製するので $k$ が $M$ 近くまで増えうる。最小費用流なら複製が要らない |
| 最小費用流(SSP+ポテンシャル)+Hierholzer(本解) | $O(NM\log N+K)$ | 採用 | 300+200 件一致、最大ケース 0.35 s 以下 |
前提が変わったら:
- 強連結の保証が無いなら、辺を持つ頂点がすべて頂点 1 から到達でき、かつ頂点 1 へ戻れるかを先に調べる(できなければ答えなし)。流れが $\sum\text{bal}^+$ 流しきれないことでも検出できる
- 無向グラフなら、奇数次の頂点の最小重み完全マッチング(Day101-Q2)。有向と無向が混ざった混合グラフの郵便配達は NP 困難として知られている(記憶。本セッションでは未確認)
- 「少なくとも 1 回」ではなく、一部の辺は通らなくてよい(田舎の郵便配達)なら、これも NP 困難として知られ、別の手法(分枝限定など)が要る
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 余りの頂点ごとに最寄りの不足の頂点と貪欲に組む | 局所的に最短なら全体も最短だと思う | 最小費用流(200 件中 29 件誤答、実測) |
| 各余りを「容量を無視して」最寄りの不足へ送る | 不足の頂点が受け入れられる回数を忘れる | 不足の容量 $-\text{bal}(v)$ を守る(200 件中 150 件で値が変わる、実測) |
| $\text{bal}$ の向きを逆にする | 「出が多い頂点から流す」と思う | 入が多い頂点(行き止まりになる側)から、出が多い頂点へ道を足す |
| オイラー閉路を再帰で書く | 経路長が $10^5$ を超える | 反復版の Hierholzer |
| 頂点の列で出力する | 多重辺の長さが違うと、どの辺を通ったか決まらない | 辺の番号で出力する |
次のステップ
- 発展1: 強連結の保証を外し、到達できない辺があれば
-1を出す版を作る - 発展2: 無向の郵便配達(Day101-Q2)を「各辺を向き付けして有向にする」ことで解けないか考え、反例を全探索で探す
- 発展3: Edmonds–Johnson の原典で有向版の扱いを確かめる(本セッションでは未確認)
検証ログ(本セッションで実行した数値)
- 全探索との照合(実測): $N\le5$、$M\le8$、自己ループ・多重辺ありのランダム強連結グラフ 300 件で、最小値の不一致 0。出力した経路は全件で「頂点 1 から始まり 1 に戻る」「辺がつながっている」「全辺を含む」「長さの和が 1 行目と等しい」を満たした
- 中規模の独立な検算(実測): $4\le N\le30$、$M\le3N$、不足の合計 1〜7 の 200 件で、全点対最短路+組み方の全列挙と一致。経路の検査も全件合格
- 誤った方法(実測): 小さい 300 件では貪欲は 0 件、容量無視は 31 件誤答。中規模 200 件では貪欲 29 件・容量無視 150 件
- 入力例(実測): 例 1
9、例 213、例 351(全探索と一致。例 3 の貪欲は 57) - 実行時間(実測、負荷平均 5〜6): $N=100,M=3000$ 0.09 s、$N=200,M=5000$(長さ $10^6$ 以下)0.25 s、同(長さすべて 1)0.25 s、頂点 1 から $4800$ 本が出る偏ったグラフ(経路の長さ $K=483404$)0.31 s。4 つとも出力した経路の検査に合格
- 扱っていない範囲: 強連結でない入力、混合グラフ・田舎の郵便配達、原典の確認