Day 176-Q4 — 2026-10-12 | 最小比率全域木(Σa/Σb を最小にする全域木を、整数だけの Dinkelbach 法で既約分数として求める)

2026-10-12 赤色 Master (Phase 8+) ★★★★★★★★★ グラフ+分数計画(焦点帯 M2)。Day131 で「K 個選ぶ比の最小化」に Dinkelbach 法を使った。今回は選ぶものが全域木で、判定問題「$\min_T\sum(a-\lambda b)\le0$ か」が最小全域木になる。$\lambda=p/q$ を既約分数で持ち、辺の重みを $aq-bp$ の整数にすれば丸め誤差なしで止まり、答えを分数のまま出せる

問題

$N$ 頂点 $M$ 辺の連結な無向グラフがあり、辺 $i$ は頂点 $u_i,v_i$ を結び、費用 $a_i$ と利得 $b_i$ を持つ。全域木 $T$ の比

$$\rho(T)=\frac{\sum_{i\in T}a_i}{\sum_{i\in T}b_i}$$

の最小値を既約分数 p/q で出力せよ($\rho=0$ なら 0/1)。

入力形式

N M
u_1 v_1 a_1 b_1
...
u_M v_M a_M b_M

制約

#制約
1$2\le N\le10^4$、$N-1\le M\le10^5$
2$0\le a_i\le10^6$、$1\le b_i\le10^6$
3グラフは連結(多重辺はありうる、自己ループはない)

入出力例

入力例1

3 3
1 2 2 2
1 3 3 8
3 2 1 1

出力例1

4/9

(全域木は 3 つ: $\{12,13\}$ で $5/10$、$\{12,32\}$ で $3/3$、$\{13,32\}$ で $4/9$。最適な木は、1 辺だけの比で見ると最悪の $1/1$ の辺 3-2 を含む)

入力例2

2 2
1 2 0 5
1 2 3 1

出力例2

0/1

入力例3

4 5
1 2 6 2
2 3 4 4
3 4 5 1
4 1 2 3
1 3 7 7

出力例3

13/14

(3 例とも全域木の全列挙と一致)

背景と出典

  • 最小比率全域木(minimum ratio spanning tree)は Chandrasekaran(1977, *Networks*)が扱った、と私は記憶している(原典は本セッションで未確認)。競技プログラミングでは POJ 2728「Desert King」(完全グラフ上の最適比率全域木)が有名だと記憶している(未確認)
  • Dinkelbach 法(Dinkelbach 1967): $\lambda_{k+1}=\rho(T_k)$、$T_k=\arg\min_T\sum(a-\lambda_kb)$ を繰り返す。分数計画に対する Newton 法で、超 1 次収束する。組合せ最適化の比の問題では反復回数が強多項式で抑えられる(Radzik 1992 と記憶、未確認)。本問の実測では Kruskal 4〜7 回で止まった
  • 本問は自作。Day131-Q2(K 個選択の比)と同じ法を、判定問題が最小全域木になる形に移した

概念図

入力例 1: 辺のラベルは a/b。全域木 3 本と、Dinkelbach 法の λ の動き 2/23/81/1 1 2 3 {12,13}: 5/10 {12,32}: 3/3 {13,32}: 4/9 最小 λ₀ = 1(Σa 最小の木 {32,12})→ 重み a·1 − b·1 で木 {13,12}: λ₁ = 5/10 = 1/2 → 重み 2a − b で木 {13,32}: λ₂ = 4/9 → 重み 9a − 4b の最小全域木の重みが 0 なので停止(Kruskal 4 回) 1 辺の比 a/b で並べる貪欲: 3/8 の次は 2/2 と 1/1 が同値で、2/2 を選ぶと 5/10(誤り) 比の最適な木は、辺ごとの比では最悪の 1/1 を含む。比の和は辺ごとの比の和ではない

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

ヒント1(方向性)

「最小値が $\lambda$ 以下か」は「$\sum a\le\lambda\sum b$ となる木があるか」、つまり「$\sum(a_i-\lambda b_i)\le0$ の木があるか」。重み $a_i-\lambda b_i$ の最小全域木を 1 本作れば判定できる。

ヒント2(アプローチ)

二分探索でもよいが、浮動小数の $\lambda$ では答えを既約分数にできない。Dinkelbach 法なら、$\lambda$ を毎回「実際にある木の比」に更新するので、$\lambda$ は常に分数 $p/q$ で、終わったときの $\lambda$ がそのまま答え。

ヒント3(誘導)
  • $\lambda=p/q$ のとき、重み $a_i-\lambda b_i$ を $q$ 倍した $a_iq-b_ip$(整数)で辺を並べても、最小全域木は同じ
  • 初期値は $\lambda=0$($\sum a$ 最小の木)の比
  • 最小全域木 $T$ の $\sum a\cdot q-\sum b\cdot p\ge0$ なら $\lambda$ が最適。そうでなければ $\lambda\leftarrow\sum a/\sum b$(既約にする)

模範解答 (Python)

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


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    U = [int(x) - 1 for x in data[2:2 + 4 * m:4]]
    V = [int(x) - 1 for x in data[3:3 + 4 * m:4]]
    A = [int(x) for x in data[4:4 + 4 * m:4]]
    B = [int(x) for x in data[5:5 + 4 * m:4]]

    def kruskal(p, q):
        """辺の重み a_i*q - b_i*p(= q·(a_i - λ b_i)、λ = p/q)で最小全域木を作り、Σa と Σb を返す"""
        key = [a * q - b * p for a, b in zip(A, B)]
        order = sorted(range(m), key=key.__getitem__)
        par = list(range(n))
        sa = sb = 0
        need = n - 1
        for e in order:
            x, y = U[e], V[e]
            while par[x] != x:
                par[x] = par[par[x]]
                x = par[x]
            while par[y] != y:
                par[y] = par[par[y]]
                y = par[y]
            if x != y:
                par[x] = y
                sa += A[e]
                sb += B[e]
                need -= 1
                if need == 0:
                    break
        return sa, sb

    # Dinkelbach: λ_0 は任意の全域木の比。λ_{k+1} = (T_k の Σa)/(T_k の Σb)、T_k は a - λ_k b の最小全域木
    p, q = kruskal(0, 1)                     # λ = 0 のときの木(Σa 最小の木)から始める
    g = gcd(p, q)
    p, q = p // g, q // g
    while True:
        sa, sb = kruskal(p, q)
        if sa * q - sb * p >= 0:             # min Σ(a - λb) = 0 なら λ が最適
            break
        g = gcd(sa, sb)
        p, q = sa // g, sb // g
    print(f"{p}/{q}")


main()

計算量: Kruskal 1 回 $O(M\log M)$、それを $K$ 回。$N=10^4,M=10^5$ で $K=5\sim7$、0.60〜0.69 s(Python 3.14.7、M1 MacBook Pro 8GB、負荷平均 11〜18 での実測、起動込み)。重みの整数は $|aq-bp|\le10^6\cdot\sum b\le10^{16}$ 程度。

Step-by-Step 解説

1比の最小化を判定に直す

$\rho^*=\min_T\rho(T)$ とすると、$h(\lambda)=\min_T\sum_{i\in T}(a_i-\lambda b_i)$ について $h(\lambda)\ge0\iff\lambda\le\rho^*$($b>0$ なので $\sum b>0$)。$h$ は 1 次関数の最小なので上に凸で減少し、根がちょうど $\rho^*$。

2判定は最小全域木

$h(\lambda)$ は辺の重みを $a_i-\lambda b_i$ にした最小全域木の重み。Kruskal なら辺の順序だけが要るので、$q>0$ 倍した整数 $a_iq-b_ip$ で並べてよい。

3Dinkelbach の更新

$\lambda_k=\rho(T_{k-1})$ から、$T_k=\arg\min\sum(a-\lambda_kb)$ を作り $\lambda_{k+1}=\rho(T_k)$。$h(\lambda_k)\le\sum_{T_{k-1}}(a-\lambda_kb)=0$ なので $\lambda_{k+1}\le\lambda_k$。$h(\lambda_k)=0$ になったら $\lambda_k=\rho^*$。木の個数は有限なので必ず止まり、凸な $h$ の Newton 法なので反復は少ない。

4既約分数

$\lambda$ は毎回ある木の比 $\sum a/\sum b$ で、$\gcd$ で割って持つ。最後の $\lambda$ をそのまま出力すれば既約分数になる。

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

案計算量採否理由
全域木の全列挙(Fraction)指数検算用のみ$N\le7,M\le12$
浮動小数の二分探索(60 回)$60\,M\log M$不採用(遅く、分数に戻せない)4.90〜6.64 s($N=10^4,M=10^5$、実測)。最後の木の比を Fraction にすれば値は合うが、$\lambda$ 自体を limit_denominator で分数に戻すと 2 入力とも誤り(例 555533501/5059507539、正しくは 651453272/5933094467)
$\sum a$ 最小の木の比$M\log M$不採用(誤り)500 グラフ中 154 で誤答(実測)
1 辺の比 $a/b$ の小さい順に Kruskal$M\log M$不採用(誤り)500 グラフ中 86 で誤答(実測)。入力例 1 が反例
$\sum b$ 最大の木の比$M\log M$不採用(誤り)500 グラフ中 279 で誤答(実測)
整数の Dinkelbach(本解)$K\,M\log M$、$K=4\sim7$採用0.60〜0.69 s

前提が変わったら

  • 完全グラフ(POJ 2728 型、$M=N^2/2$)なら Kruskal の整列が重いので、各反復を Prim 法 $O(N^2)$ にする
  • $b_i$ に負や 0 があると $\sum b$ の符号が木によって変わり、判定の向きが壊れる。本問の $b\ge1$ はそのための条件
  • $N=10^5,M=2\times10^5$ では 2.61〜3.37 s(実測、Kruskal 6〜7 回)。整列を前回の順序からの挿入ソートにするなどの工夫が要る(未実装)

よくあるミス

ミス原因正しい書き方
浮動小数で $a-\lambda b$ を比べる小数で十分だと思う$\lambda=p/q$ を分数で持ち、$aq-bp$ の整数で比べる
1 辺ごとの比 $a/b$ で並べる比の最小化を辺ごとの貪欲と混同86/500 で誤答(実測)
最初の $\lambda$ を 0 にしたまま「$h(0)\ge0$ なら答え 0」と判定し忘れる終了条件の書き漏れ初期値も実在の木の比にしておけば、同じ終了条件で扱える(入力例 2)
既約にせずに出力する$\sum a/\sum b$ をそのまま出す$\gcd$ で割る
判定の向きを逆にする$h(\lambda)<0$ の意味の取り違え$h(\lambda)<0$ なら $\lambda$ より小さい比の木がある

次のステップ

  • 発展1: 最小比率の有向全域木(最小有向木、Chu–Liu/Edmonds)に Dinkelbach を載せる
  • 発展2: 最小平均閉路(Karp)と最小比率閉路の関係を、本問と同じ判定の形で書き直す
  • 発展3: Radzik(1992)の反復回数の上界を原典で確かめる(本セッションでは未確認)

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

  • 全域木の全列挙との照合(実測): $N=2\sim7$、$M\le12$、$a,b$ の上限 3・10・1000 のランダム連結グラフ 500 個(多重辺あり)。既約分数が全件一致
  • 誤った方法(実測、同じ 500 グラフ): $\sum a$ 最小 154、辺の比の貪欲 86、$\sum b$ 最大 279 で誤答
  • 反例探し(実測): 3 頂点 3 辺・$a,b\le9$ で、辺の比の貪欲と $\sum a$ 最小の両方が誤る例のうち $\sum(a+b)$ 最小のものを入力例 1 にした
  • 実行時間と反復回数(実測、起動込み): $N=10^4,M=10^5$ で Kruskal 5 回 0.60 s・7 回 0.69 s。$N=2000,M=2\times10^5$ で 4 回 0.66 s・7 回 1.14 s。$N=10^5,M=2\times10^5$ で 5 回 2.61 s・6 回 3.37 s。浮動小数の二分探索 60 回は $N=10^4$ で 4.90〜6.64 s
  • 扱っていない範囲: 完全グラフ向けの Prim 版、反復回数の理論上界、原典の確認

自己評価

自分の回答

気づき・メモ