Day 102-Q5 — Auction Algorithm(オークションアルゴリズム)

2026-07-25 赤色 Master / Phase 8+ ★★★★★★★★★ 価格上昇による分散型最適割当・ε-scaling

問題

$N$人の作業員と$N$個の仕事がある。作業員$i$が仕事$j$を担当したときの価値は$v_{i,j}$で与えられる。各作業員がちょうど1つの仕事を、各仕事がちょうど1人の作業員に割り当てられるようにするとき、価値の総和の最大値を求めよ。

ハンガリアン法($O(N^3)$)が定番だが、ここでは経済学的発想のAuction Algorithm(Bertsekasのオークションアルゴリズム)を扱う。各仕事に「価格」を設定し、各作業員が「価値−価格」最大の仕事に対して2番目との差額+$\varepsilon$だけ価格を吊り上げて入札する。整数問題では$\varepsilon$を段階的に縮小する$\varepsilon$-scalingにより厳密最適解に収束する。

入力形式

N
v_{1,1} v_{1,2} ... v_{1,N}
:
v_{N,1} v_{N,2} ... v_{N,N}

制約

$1 \le N \le 8$
$1 \le v_{i,j} \le 10^4$
すべて整数

入出力例

入力例1

3
3 1 2
1 4 2
2 1 3

出力例1

10

作業員1→仕事1(3)、作業員2→仕事2(4)、作業員3→仕事3(3)で合計10が最大。

入力例2

2
5 1
1 5

出力例2

10

対角割当(1→1, 2→2)で5+5=10。交差割当は1+1=2で明らかに劣る。

概念図

オークション:価値-価格が最大の仕事に入札し価格を吊り上げる 作1 作2 作3 仕事1 仕事2 仕事3 価値3 入札 価値4 入札 価値3 入札 εを段階的に縮小(ε-scaling)しながら再入札 → 最終的に最適割当(価値合計10)に収束

ヒント(段階的開示)

ヒント1: 方向性
「全員が同時に最も欲しい仕事に応札し、価格をせり上げていく」という市場メカニズムをそのままシミュレーションする、という発想がスタート地点。ハンガリアン法のような「増加路を明示的に探す」手法とは全く異なるアプローチで、局所的な入札のやり取りだけで大域最適に収束する点が面白い。
ヒント2: アプローチ
未割当の作業員$i$がいる限り、(1)$i$にとって「価値−現在の価格」が最大になる仕事$j^*$と2番目の値$w$を求める。(2)$j^*$の価格を「現在の価格+(最良の差−2番目の差)+$\varepsilon$」まで引き上げる。(3)$j^*$に以前割り当てられていた作業員がいればその人を未割当に戻し、$i$を$j^*$に割り当てる。整数値に厳密に収束させるには、価値を$(N+1)$倍にスケーリングしてから$\varepsilon$を段階的に縮小する「$\varepsilon$-scaling」を行う。
ヒント3: 誘導(コード骨格)
scale = N + 1
v = [[value[i][j] * scale for j in range(N)] for i in range(N)]
price = [0] * N
eps = max(max(row) for row in v)

while eps >= 1:
    owner = [-1] * N
    assigned_to = [-1] * N
    unassigned = list(range(N))
    while unassigned:
        i = unassigned.pop()
        best_val = second_val = -float('inf')
        best_j = -1
        for j in range(N):
            val = v[i][j] - price[j]
            if val > best_val:
                best_val, second_val, best_j = val, best_val, j
            elif val > second_val:
                second_val = val
        bid = price[best_j] + (best_val - second_val) + eps
        price[best_j] = bid
        if owner[best_j] != -1:
            unassigned.append(owner[best_j])
        owner[best_j] = i
        assigned_to[i] = best_j
    eps //= 4

total = sum(value[i][assigned_to[i]] for i in range(N))

各$\varepsilon$の段階で割当をリセットして再度オークションを行う「$\varepsilon$-scaling」の実装が要点。

模範解答 (Python)

import sys


def solve():
    data = sys.stdin.read().split()
    idx = 0
    N = int(data[idx]); idx += 1
    value = []
    for _ in range(N):
        row = [int(data[idx + j]) for j in range(N)]
        idx += N
        value.append(row)

    scale = N + 1
    v = [[value[i][j] * scale for j in range(N)] for i in range(N)]

    price = [0] * N
    assigned_to = [-1] * N

    eps = max(max(row) for row in v) if N > 0 else 1
    if eps == 0:
        eps = 1

    while eps >= 1:
        owner = [-1] * N
        assigned_to = [-1] * N
        unassigned = list(range(N))

        while unassigned:
            i = unassigned.pop()
            best_val = -float('inf')
            second_val = -float('inf')
            best_j = -1
            for j in range(N):
                val = v[i][j] - price[j]
                if val > best_val:
                    second_val = best_val
                    best_val = val
                    best_j = j
                elif val > second_val:
                    second_val = val
            if second_val == -float('inf'):
                second_val = best_val

            bid = price[best_j] + (best_val - second_val) + eps
            price[best_j] = bid

            prev_owner = owner[best_j]
            if prev_owner != -1:
                assigned_to[prev_owner] = -1
                unassigned.append(prev_owner)
            owner[best_j] = i
            assigned_to[i] = best_j

        eps //= 4

    total = sum(value[i][assigned_to[i]] for i in range(N))
    print(total)


solve()
計算量: $O(N^2 \log(N \cdot V_{\max}))$ 程度($\varepsilon$-scalingの段階数×各段階での入札処理)。

Step-by-Step 解説

1整数スケーリング
価値を$(N+1)$倍にしておくことで、$\varepsilon<1$に到達したときの割当が厳密最適であることが保証される($\varepsilon$-complementary slacknessの理論)。
2最良・2番目良い仕事を探す
「価値−価格」が最大の仕事とその次に良い値の差が、入札の上乗せ額の核心部分になる。
3価格の吊り上げと割当の入れ替え
入札額だけ価格を上げ、既存の担当者がいれば未割当に戻して再入札させる。
4$\varepsilon$-scalingで段階的に精度を上げる
$\varepsilon$を4分の1ずつ縮小しながらオークションをやり直し、$\varepsilon<1$になった時点の割当が厳密最適。

よくあるミス

ミス原因正しい書き方
スケーリングをせず$\varepsilon$-scalingを行う$\varepsilon$が整数のまま1未満になれず理論的保証が成立しない価値をあらかじめ$(N+1)$倍しておく
「2番目に良い値」の初期化を忘れる仕事が1つしかない場合など2番目の候補が存在しない`second_val`未更新時は`best_val`と同値にするフォールバックを入れる
$\varepsilon$を一気に0にする途中で最適でない割当のまま終了し局所最適に収束する徐々に(例えば4分の1ずつ)縮小するscalingを行う
出力を「割当そのもの」にしてしまうタイがある場合、実装依存でジャッジと一致しない可能性がある本問では「価値の総和」のみを出力すればよい

次のステップ

  • 発展: 非正方(作業員数≠仕事数)の場合への拡張(ダミー仕事/作業員を追加して正方化する)
  • 発展: 分散システムでの並列実装(各買い手が独立に入札するAuction Algorithmは並列化しやすく、実際にネットワークルーティングや経済学のモデルにも応用される)
  • 次回予告: (Master Levelローテーション継続、次回の出題テーマは実行時に選定)

自己評価

自分の回答

気づき・メモ