Day 072-Q4 — パラメトリック最短路(Parametric Shortest Path・凸最小化)

2026-06-25 赤色 Master / Phase 8+ ★★★★★★★★★ パラメトリック最短路・Dijkstra・二分探索・凸関数

問題

$N$ 頂点 $M$ 辺の有向グラフがある。辺 $e_i$ は頂点 $u_i$ から $v_i$ へ向かい、コスト $c_i + \lambda \cdot d_i$ を持つ($\lambda$ はパラメータ)。

頂点 $s$ から $t$ への最短路長 $f(\lambda) = \min_{\text{path}} \sum_{e \in P} (c_e + \lambda d_e)$ を考える。

  1. $f(\lambda)$ が $\lambda$ の区分線形凸関数であることを確認
  2. $f(\lambda) = 0$ となる $\lambda^*$ を二分探索で求めよ
  3. $\lambda^*$ における最短路の経路を出力せよ

制約

パラメータ範囲備考
$N$$2 \le N \le 10^4$頂点数
$M$$1 \le M \le 5 \times 10^4$辺数
$c_i$$-10^9 \le c_i \le 10^9$固定コスト
$d_i$$-10^3 \le d_i \le 10^3$パラメータ係数
$\lambda^*$$\lambda^* \in [-10^6, 10^6]$解の範囲保証

入出力例

入力例 1

4 5 1 4
1 2 3 -1
1 3 1 2
2 4 2 1
3 4 5 -1
2 3 1 0

出力例 1

Lambda*: 1.000
Path: 1 -> 2 -> 4
Total cost at lambda*: 5.0

概念図: 区分線形凸関数 f(λ) と二分探索

f(λ) = min_path Σ(c_e + λd_e) は区分線形凸関数 各パスのコストは λ の1次式 → 最小値は凸関数 → f(λ)=0 を二分探索で解ける f(λ) の形状 λ f(λ) λ* f(λ*)=0 f(λ) > 0 の領域 f(λ) < 0 パス1(傾き大) パス2(傾き小) 二分探索アルゴリズム lo = -10^6, hi = 10^6 200回反復(十分な精度) mid = (lo + hi) / 2 Dijkstra(mid) → f(mid) を計算 f(mid)>0? lo=mid(正の側) hi=mid(負の側) 精度: |lo-hi| < 10^-9 → λ* = (lo+hi)/2

ヒント(段階的開示)

ヒント1: 方向性
$f(\lambda)$ は各パスのコスト($\lambda$ の1次式)の最小値なので凸関数(区分線形)。$f(\lambda) = 0$ を探すのは二分探索で可能。
ヒント2: アプローチ
  1. $\lambda$ を固定したとき各辺コストは $c_e + \lambda d_e$
  2. Dijkstra で最短路を計算($d_e$ が負の場合は Bellman-Ford)
  3. $f(\lambda_0) > 0$、$f(\lambda_1) < 0$ となる $\lambda_0 < \lambda_1$ を確認
  4. 200 回の二分探索で $10^{-9}$ の精度まで収束
ヒント3: コード骨格
import heapq

def dijkstra(N, adj, s, t, lam):
    dist = [float('inf')] * (N+1)
    dist[s] = 0.0
    pq = [(0.0, s)]
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]: continue
        for v, c, di in adj[u]:
            nd = d + c + lam * di
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(pq, (nd, v))
    return dist[t]

# 二分探索
lo, hi = -1e6, 1e6
for _ in range(200):
    mid = (lo + hi) / 2
    if dijkstra(N, adj, s, t, mid) > 0:
        lo = mid
    else:
        hi = mid

模範解答 (Python)

import sys
import heapq
input = sys.stdin.readline

def solve():
    N, M, s, t = map(int, input().split())
    adj = [[] for _ in range(N+1)]
    for _ in range(M):
        u, v, c, d = map(int, input().split())
        adj[u].append((v, c, d))

    INF = float('inf')

    def dijkstra_with_path(lam):
        dist = [INF] * (N+1)
        prev = [-1] * (N+1)
        dist[s] = 0.0
        pq = [(0.0, s)]
        while pq:
            d, u = heapq.heappop(pq)
            if d > dist[u] + 1e-12: continue
            for v, c, di in adj[u]:
                nd = d + c + lam * di
                if nd < dist[v] - 1e-12:
                    dist[v] = nd
                    prev[v] = u
                    heapq.heappush(pq, (nd, v))
        return dist[t], prev

    def f(lam):
        val, _ = dijkstra_with_path(lam)
        return val

    f_lo = f(-1e6)
    f_hi = f(1e6)
    if f_lo * f_hi > 0:
        print("No solution in range")
        return

    lo, hi = -1e6, 1e6
    for _ in range(200):
        mid = (lo + hi) / 2
        if f(mid) > 0:
            lo = mid
        else:
            hi = mid

    lam_star = (lo + hi) / 2
    val, prev = dijkstra_with_path(lam_star)

    print(f"Lambda*: {lam_star:.3f}")

    path = []
    cur = t
    while cur != -1:
        path.append(cur)
        cur = prev[cur]
    path.reverse()
    print("Path:", " -> ".join(map(str, path)))
    print(f"Total cost at lambda*: {val:.1f}")

solve()

Step-by-Step 解説

Step 1: f(λ) の凸性

各固定パス $P$ のコストは $\lambda$ の1次式 $C_P + \lambda D_P$(ここで $C_P = \sum c_e$、$D_P = \sum d_e$)。

$f(\lambda) = \min_P (C_P + \lambda D_P)$ は線形関数の最小値なので凸関数(区分線形)。

Step 2: 二分探索の適用

$f$ が連続凸関数であり $f(\lambda) = 0$ の解が存在するなら、$f(\lambda_0) > 0$、$f(\lambda_1) < 0$ となる $\lambda_0 < \lambda_1$ を見つけて二分探索。

200 回反復で精度 $|f(\lambda^*)| < 10^{-9}$ が得られる。

Step 3: Dijkstra の反復

各反復で Dijkstra を実行($O(M \log N)$)、合計 $O(M \log N \times 200) \approx 10^8$。

Step 4: 経路復元

最終的な $\lambda^*$ での Dijkstra の prev テーブルから $t$ を逆追跡。

よくあるミス

ミス原因正しい書き方
負辺のある場合Dijkstra が正辺のみ正確負辺の場合は Bellman-Ford を使用
浮動小数点精度反復回数が少なすぎる100〜200 回反復が安全
二分探索範囲f(lo) と f(hi) の符号確認忘れ実行前に両端の値チェック
等号の扱い$f = 0$ ちょうどは浮動小数で困難epsilon 許容範囲で判定

次のステップ

  • 発展問題: パラメトリック最小カット(PARAMETRIC MAX-FLOW)
  • 凸関数の傾き(劣微分)の追跡
  • Aliens Trick との関係(ラグランジュ緩和・WQS 二分探索)

自己評価

解いた後に記入してください

自分の回答:

気づき・メモ: