Day 174-Q4 — 2026-10-10 | 無向グラフの郵便配達路(奇頂点の最小重み完全マッチングを「到達する状態だけ」の bit DP で)

2026-10-10 赤色 Master (Phase 8+) ★★★★★★★★★ グラフ・マッチング(焦点帯 M2)。Day171-Q4 は有向グラフの郵便配達路で、入出次数の差を最小費用流で埋めた。無向では「どちら向きに足すか」が無く、奇数次の頂点を 2 つずつ組ませる最小重み完全マッチングになる。フローでは書けない理由と、bit DP の状態が $2^K$ よりずっと少なくて済む理由を扱う

問題

$N$ 頂点 $M$ 辺の連結な無向グラフがある。辺 $i$ は頂点 $u_i$ と $v_i$ を結び、長さは $c_i$ である(多重辺はあり得るが、自己ループは無い)。

頂点 1 から出発して頂点 1 に戻る歩道のうち、すべての辺を 1 回以上通るものの長さの最小値を求め、その歩道を 1 つ出力せよ。

入力形式

N M
u_1 v_1 c_1
...
u_M v_M c_M

出力形式

1 行目に最小の長さ、2 行目に歩道の辺の数 $L$、3 行目に通る辺の番号を通る順に $L$ 個。条件を満たす歩道ならどれでもよい。

制約

#条件
1$2\le N\le10^4$、$N-1\le M\le2\times10^4$
2$1\le c_i\le10^9$
3グラフは連結
4次数が奇数の頂点の数を $K$ とすると $K\le22$

入出力例

入力例1

4 5
1 2 1
2 3 1
3 4 1
4 1 1
1 3 5

出力例1

11
7
1 1 4 3 2 2 5

(奇数次は頂点 1 と 3。最短路 1-2-3(長さ 2)をもう 1 回通る。辺の合計 9 と合わせて 11)

入力例2

3 3
1 2 2
2 3 3
3 1 4

出力例2

9
3
1 2 3

(すべての次数が偶数なので、オイラー閉路そのまま)

入力例3

4 4
1 3 1
3 4 1
1 2 9
1 3 5

出力例3

26
6
1 2 2 4 3 3

(奇数次は 1, 2, 3, 4。一番近い組 (1, 3) から組ませる貪欲は (1,3)=1 と (2,4)=11 で 28 になる。正解は (1,2)=9 と (3,4)=1 で 26。本セッションのランダム探索で見つけた最小の反例。3 例とも、各辺を 1 回か 2 回通す全組み合わせの最小と一致)

背景と出典

  • 中国人郵便配達問題(Guan Meigu 1962 が最初と記憶): 無向グラフでは、奇数次の頂点の集合 $T$ を最短路で 2 つずつ組ませ、組ごとに最短路の辺を 1 回ずつ足すとオイラーグラフになる。最小の足し方は「$T$ 上で距離 = 最短路長とした最小重み完全マッチング」。多項式時間の解法は Edmonds–Johnson(1973)と記憶している(どちらの原典も本セッションでは未確認)
  • なぜフローで書けないか: 有向(Day171-Q4)では「入次数 > 出次数の頂点から、出次数 > 入次数の頂点へ」流すので、供給と需要の向きが決まる。無向では奇数次の頂点どうしに供給・需要の区別が無く、「どの 2 つを組ませるか」自体が組合せの選択になる。一般グラフの最小重み完全マッチングは花の縮約(重み付き Edmonds)が要る
  • 本問は $K\le22$ に絞り、マッチングを bit DP で解く。「残りのうち最小番号の頂点を必ず誰かと組ませる」と決めると、0 から到達する状態は $2^K$ よりずっと少ない($K=16,18,20,22$ で 1597, 4181, 10946, 28657 個、実測)。この数はフィボナッチ数 $F_{K+1}$ と一致した(4 つの $K$ で数えただけで、証明はしていない)

本問は Day171-Q4 の無向版として自作。

概念図

入力例 3: 奇数次の頂点 1, 2, 3, 4 を 2 つずつ組ませる(最短路を 1 回ずつ足す) 近い組から貪欲: (1,3) を先に取る 9 1 5 1 2 1 3 4 (1,3)=1 + (2,4)=11 足す長さ 12、合計 16 + 12 = 28 最小重み完全マッチング(本解) 9 1 5 1 2 1 3 4 (1,2)=9 + (3,4)=1 足す長さ 10、合計 16 + 10 = 26 bit DP は「最小番号の未使用頂点の相手」だけを選ぶ。0 から到達する状態は K=22 で 28657 個(2^22 ≈ 419 万の 0.7%)

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

ヒント1(方向性)

すべての次数が偶数なら、オイラー閉路がそのまま答え。奇数次の頂点(必ず偶数個)を何とかして偶数次にすればよい。辺を「もう 1 回通る」ことは、その辺を複製することと同じ。

ヒント2(アプローチ)

複製した辺だけを見ると、各奇数次頂点の次数は奇数、他の頂点は偶数。こういう辺集合は「奇数次頂点を 2 つずつ結ぶ道」に分解でき、最小にするなら各道は最短路。だから「奇数次頂点の組ませ方のうち、最短路長の和が最小のもの」を探す。

ヒント3(誘導)
  • 奇数次の頂点ごとに Dijkstra($K$ 回)で距離行列と最短路木を作る
  • dp[mask] = 組み終わった集合が mask のときの残りの最小コスト。mask に入っていない最小番号 $i$ を、残りの $j$ と組ませる。0 から幅優先で到達する mask だけを列挙し、逆順に埋める
  • 組ませた $(i,j)$ ごとに $i$ の最短路木を $j$ からさかのぼって辺を複製し、Hierholzer で閉路を作る

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
import heapq


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    eu = [0] * m
    ev = [0] * m
    ew = [0] * m
    adj = [[] for _ in range(n)]
    deg = [0] * n
    for i in range(m):
        u = int(data[2 + 3 * i]) - 1
        v = int(data[3 + 3 * i]) - 1
        c = int(data[4 + 3 * i])
        eu[i], ev[i], ew[i] = u, v, c
        adj[u].append((v, c, i))
        adj[v].append((u, c, i))
        deg[u] += 1
        deg[v] += 1
    odd = [v for v in range(n) if deg[v] % 2]
    k = len(odd)
    INF = float('inf')

    # 奇頂点ごとに Dijkstra(距離と、木をたどるための親の辺)
    dist = []
    pe = []
    for s in odd:
        d = [INF] * n
        par = [-1] * n
        d[s] = 0
        pq = [(0, s)]
        while pq:
            du, u = heapq.heappop(pq)
            if du > d[u]:
                continue
            for v, c, i in adj[u]:
                nd = du + c
                if nd < d[v]:
                    d[v] = nd
                    par[v] = i
                    heapq.heappush(pq, (nd, v))
        dist.append(d)
        pe.append(par)
    D = [[dist[a][odd[b]] for b in range(k)] for a in range(k)]

    # 奇頂点の最小重み完全マッチング: 未使用のうち最小番号の i を必ず誰かと組ませる。
    # 到達する mask は「下位が詰まった接頭辞+上の数ビット」だけで、2^k よりずっと少ない
    full = (1 << k) - 1
    memo = {full: 0}
    choice = {}
    order = [0]                          # 0 から到達する mask を列挙(popcount の小さい順)
    seen = {0}
    for mask in order:
        if mask == full:
            continue
        rest = full ^ mask
        i = (rest & -rest).bit_length() - 1
        r = rest ^ (1 << i)
        while r:
            low = r & -r
            nm = mask | (1 << i) | low
            if nm not in seen:
                seen.add(nm)
                order.append(nm)
            r ^= low
    for mask in reversed(order):         # popcount の大きい順に確定
        if mask == full:
            continue
        rest = full ^ mask
        i = (rest & -rest).bit_length() - 1
        Di = D[i]
        best = INF
        bj = -1
        r = rest ^ (1 << i)
        while r:
            low = r & -r
            j = low.bit_length() - 1
            val = Di[j] + memo[mask | (1 << i) | low]
            if val < best:
                best = val
                bj = j
            r ^= low
        memo[mask] = best
        choice[mask] = bj

    total = sum(ew)
    extra = [0] * m                     # 各辺をもう何回通るか
    mask = 0
    while mask != full:
        rest = full ^ mask
        i = (rest & -rest).bit_length() - 1
        j = choice[mask]
        total += D[i][j]
        v = odd[j]                       # odd[i] からの最短路木を odd[j] からさかのぼる
        while v != odd[i]:
            e = pe[i][v]
            extra[e] += 1
            v = eu[e] if ev[e] == v else ev[e]
        mask |= (1 << i) | (1 << j)

    # Hierholzer(反復版)で頂点 1 から出る閉路を辺番号の列で
    g = [[] for _ in range(n)]
    cnt = []
    for i in range(m):
        c = 1 + extra[i]
        for _ in range(c):
            idx = len(cnt)
            cnt.append(i)
            g[eu[i]].append((ev[i], idx))
            g[ev[i]].append((eu[i], idx))
    used = [False] * len(cnt)
    ptr = [0] * n
    stack = [(0, -1)]
    route = []
    while stack:
        v, e = stack[-1]
        lst = g[v]
        while ptr[v] < len(lst) and used[lst[ptr[v]][1]]:
            ptr[v] += 1
        if ptr[v] == len(lst):
            stack.pop()
            if e >= 0:
                route.append(cnt[e] + 1)
        else:
            to, idx = lst[ptr[v]]
            used[idx] = True
            stack.append((to, idx))
    route.reverse()
    print(total)
    print(len(route))
    print(' '.join(map(str, route)))


main()

計算量: Dijkstra $K$ 回で $O(K\,M\log N)$、マッチングの DP は到達状態数 $\times K$($K=22$ で約 2.9 万状態)、Hierholzer は $O(M+\text{複製した辺の数})$。$N=10^4$、$M=2\times10^4$、$K=22$ で 1.30〜1.52 s(Python 3.14.7、M1 MacBook Pro 8GB、負荷平均 2〜3 での実測、起動込み)。

Step-by-Step 解説

1奇数次頂点ごとに Dijkstra

距離だけでなく、各頂点へ最後に使った辺 par[v] も持つ。あとで最短路の辺を実際に複製するのに使う。

2組ませ方の DP

mask は組み終わった奇数次頂点の集合。最小番号の未使用 $i$ は「誰かと組む」ことが確定しているので、$i$ の相手 $j$ だけを選べばよい。こうすると同じ組ませ方を別の順で数えることがなく、遷移は $K$ 本以下。

3到達する状態だけを列挙する

0 から「最小の未使用 $i$ と、もう 1 つ」を足していくと、mask は常に「下位の数ビットが全部埋まり、その上に何ビットか立つ」形になる。$2^K$ の全状態を回す版と同じ答えを出しつつ、DP だけの時間は $K=20$ で 7.91 s → 0.13 s、$K=22$ で 32.6 s → 0.34 s(実測)。

4辺を複製してオイラー閉路

組 $(i,j)$ について、$i$ からの最短路木を $j$ からさかのぼり、通った辺の extra を 1 増やす。各辺を $1+\text{extra}$ 本とみなしたグラフは全頂点が偶数次で連結なので、頂点 1 からの Hierholzer(スタックを使う反復版)でオイラー閉路が取れる。出力は辺の番号の列なので、多重辺があっても一意に検査できる。

5貪欲がなぜ外れるか

入力例 3 で、一番近い組 (1,3) を先に取ると、残りの (2,4) が遠い($9+1+1=11$)。少し遠い (1,2)=9 を選ぶと残りの (3,4)=1 が近くなる。組ませ方は全体で決めないといけない。

迷った他案と採らなかった理由

案計算量採否理由
各辺を 1 回か 2 回通す全組み合わせ$O(2^M)$不採用検証用。最適解では各辺の複製は高々 1 回で足りる(Edmonds–Johnson と記憶、未確認)。$M\le12$ まで
一番近い奇数次の組から貪欲に$O(K^3)$不採用(誤り)800 グラフ中 61 グラフで長さを誤る(実測)
最小費用流(Day171-Q4 の有向版の方法)—不採用無向では供給と需要の向きが無く、組ませ方を流れで表せない
$2^K$ の全状態を回す bit DP$O(2^K K)$不採用正しいが、DP だけで $K=20$ で 7.91 s、$K=22$ で 32.6 s(実測)
重み付き一般マッチング(重み付き Edmonds)$O(K^3)$不採用$K$ が大きいときの本命。実装が長く(双対変数の更新と花の展開)、$K\le22$ なら DP で 0.34 s に収まる
到達する状態だけの bit DP(本解)到達状態数 $\times K$採用全体 1.30〜1.52 s

前提が変わったら:

  • $K$ が数百以上(奇数次の頂点が多い)なら、重み付き Edmonds($O(K^3)$)か、Blossom V のような実装に切り替える。本解は $K=24$ で 2.07〜2.20 s、$K=30$ で 16.77 s(実測)
  • 一部の辺が一方通行(混合グラフ)なら、混合郵便配達問題は NP 困難と記憶しており(未確認)、整数計画か近似になる
  • 辺を通る回数に上限がある、またはいくつかの辺だけ通ればよい(農村郵便配達)なら、別の問題になる

よくあるミス

ミス原因正しい書き方
一番近い組から貪欲に組ませる局所的に最短でも全体で最小とは限らない800 グラフ中 61 グラフで誤答(実測)。DP で組ませ方全体を最小化
bit DP で $i$ と $j$ を両方自由に選ぶ同じ組ませ方を $(K/2)!$ 通りの順で数え、遷移が $K^2$ 本に最小の未使用 $i$ を固定し、相手 $j$ だけ選ぶ
最短路の長さだけ足して、歩道を出せない複製する辺を覚えていないDijkstra の par を持ち、木をさかのぼって extra を増やす
歩道を頂点の列で出力する多重辺のどれを通ったか分からない辺の番号の列で出す
再帰版の Hierholzer歩道の長さが 2 万を超え、再帰の深さ上限スタックを使う反復版

次のステップ

  • 発展1: 重み付き Edmonds(双対変数つきの花アルゴリズム)を実装し、$K=1000$ 級で本解の DP と比べる
  • 発展2: 「到達する状態の数がフィボナッチ数になる」ことを証明する(本セッションでは $K=16..22$ で数えただけ)
  • 発展3: 農村郵便配達問題(指定した辺だけ通ればよい)の小さい場合を全探索と比べる

検証ログ(本セッションで実行した数値)

  • 正解との照合(実測): $N\le7$、$M\le12$、辺の長さ $\le3$/$\le10$/$\le100$ のランダムな連結グラフ 800 個(多重辺あり)で、各辺を 1 回か 2 回通す全組み合わせのうち全頂点が偶数次になる最小と、出力の長さが一致。出力の歩道が「頂点 1 から始まり 1 に戻る・隣り合う辺がつながる・全辺を含む・長さの和が出力と等しい」ことも全件で確認。不一致 0($2^K$ 全状態の DP 版 400 個、本解 400 個)
  • 誤った方法(実測): 一番近い組からの貪欲は 800 グラフ中 61 グラフで長さを誤る。最小の反例は入力例 3
  • 到達状態数(実測): $K=16,18,20,22$ で 1597, 4181, 10946, 28657($2^K$ は 65536 〜 約 419 万)
  • DP だけの時間(実測、距離はランダム): 全状態/到達状態のみ で $K=16$: 0.345/0.013 s、$K=18$: 1.665/0.045 s、$K=20$: 7.912/0.131 s、$K=22$: 32.6/0.341 s。両者の答えは一致
  • 実行時間(実測、起動込み、$N=10^4$, $M=2\times10^4$, 長さ $\le10^9$): $K=18$: 1.08 s、$K=20$: 1.19 s、$K=22$: 1.30〜1.52 s(3 ケース)、$K=24$: 2.07〜2.20 s、$K=30$: 16.77 s。歩道の長さは約 2.26 万辺
  • 扱っていない範囲: 重み付き Edmonds、混合グラフ、農村郵便配達、原典の確認

自己評価

自分の回答

気づき・メモ