Day 171-Q4 — 2026-10-06 | 有向グラフの郵便配達路(入次数と出次数の差を最小費用流で埋め、多重グラフのオイラー閉路を Hierholzer で作る)

2026-10-06 赤色 Master (Phase 8+) ★★★★★★★★★ グラフ・フロー(焦点帯 M2)。Day101-Q2 の中国郵便配達問題は無向グラフで、奇数次の頂点どうしの最小重み完全マッチングだった。有向では「どの頂点から何回出直すか」が量になるので、マッチングではなく輸送問題(最小費用流)になる。答えの値だけでなく、実際の巡回路も出力する

問題

$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$ の多重グラフは各頂点で入出が等しく連結だから、オイラー閉路がある。

概念図

入力例 3: 入次数 − 出次数 = +1 の頂点(1, 3)から −1 の頂点(2, 4)へ、追加で歩く道を足す e1: 6e4: 1e2: 3e3: 2e5: 5e6: 2e7: 9e8: 6 1+1 2−1 3+1 4−1 50 組み方の比較(最短距離) 1→2 (14) + 3→4 (3) = 17 1→4 (5) + 3→2 (18) = 23 最小費用流: 17 → 答え 34+17=51 貪欲(1 の最寄り 4 から): 23 → 57(誤り) 辺の長さの和 = 34 辺 i を 1 + f_i 回使う多重グラフは各頂点で入 = 出 → 頂点 1 からオイラー閉路(Hierholzer)

ヒント(クリックで段階的に開示)

ヒント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不足を数える

各辺を 1 回ずつ通るとして、$\text{bal}(v)=\text{in}(v)-\text{out}(v)$ を求める。$\sum_v\text{bal}(v)=0$。$\text{bal}(v)>0$ の頂点は「入ってきたのに出ていけない」回数が $\text{bal}(v)$ ある。

2出直しの道を最小費用流で選ぶ

$\text{bal}>0$ の頂点から $\text{bal}<0$ の頂点へ、元の辺をたどる道を合わせて $\sum\text{bal}^+$ 本足す。道の組み方(誰と誰を組むか)と道そのもの(最短路)を同時に決めるのが最小費用流。容量 $\infty$ の辺に費用 $C_i$ を付けるので、流れは自然に最短路に沿う。

3流量を辺の本数に戻す

辺 $i$ の逆辺の残余容量が、辺 $i$ に流れた量 $f_i$。辺 $i$ を $1+f_i$ 回使う。長さの和は $\sum C_i+(\text{最小費用})$。

4オイラー閉路(Hierholzer、反復版)

スタックに(頂点, そこへ来た辺)を積み、まだ使える辺があれば進み、無ければ戻りながら辺を記録する。記録を逆順にすると、頂点 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、例 2 13、例 3 51(全探索と一致。例 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 つとも出力した経路の検査に合格
  • 扱っていない範囲: 強連結でない入力、混合グラフ・田舎の郵便配達、原典の確認

自己評価

自分の回答

気づき・メモ