Day 137-Q1 — Fibonacci Heap(フィボナッチヒープ)

2026-08-29 赤色 Master / Phase 8+ ★★★★★★★★★ cut + cascading cutでO(1)償却decrease-key

問題

1つの優先度付きキュー(フィボナッチヒープ)に対して、以下の3種類のクエリを $Q$ 個、順番に処理せよ。

  • 0 x : ヒープに整数 $x$ を1つ追加する。追加された要素には、挿入した順に $1,2,3,\dots$ という一意な管理番号(id)が割り当てられる。
  • 1 : ヒープの最小値を出力し、その要素をヒープから取り除く。ヒープが空なら -1 を出力する。
  • 2 id x : 管理番号 id の要素の値を $x$ に減少させる。$x$ は現在値以下であることが保証される。id の要素はまだ抽出されていないことが保証される。

decrease-key(クエリ2)を償却 $O(1)$、extract-min(クエリ1)を償却 $O(\log Q)$ で処理する Fibonacci Heap を実装せよ。

入力形式

Q
query_1
...
query_Q

制約

$1 \le Q \le 2\times10^5$
$0 \le x \le 10^9$
クエリ2の新値は現在値以下
クエリ2のidは未抽出の要素

入出力例

入力例1

8
0 5
0 3
0 8
1
2 3 1
1
1
1

出力例1

3
1
5
-1

入力例2

3
0 10
1
1

出力例2

10
-1

概念図

decrease-key: 親より小さくなったら cut して根リストへ 根リスト(circular list) 2 9 x=15 mark=false decrease-key(x, 3): 3 < 9 (親) なので cut 2 9 3 x=3(旧15)は新しい根として合流 親9がすでにmark済みなら、9も連鎖的にcut(cascading cut) この連鎖の長さの償却がO(1)であることがFibonacci Heapの核心

ヒント(段階的開示)

ヒント1(方向性)

二分ヒープの decrease-key は根への浮上が必要で $O(\log N)$。Dijkstra法の理論限界 $O(E+V\log V)$ を達成するには insert・decrease-key を償却 $O(1)$ にできる構造がいる。1本の木ではなく「木の森」として持ち、根同士を循環リストで束ねる発想はどうか。

ヒント2(アプローチ)

Fibonacci Heap は多分木の森。insertは新しい単一ノードの木を根リストに足すだけで$O(1)$。decrease-keyは親より小さくなったら部分木ごと根リストへ切り離す(cut)だけで$O(1)$。ただし無制限の切り離しは償却を崩すので、「1回切り離された印(mark)がある状態でさらに切り離されたら親も連鎖的に切り離す」ルールで制御する。extract-minのときだけ、同じ次数の根同士をまとめる consolidate が $O(\log N)$ 償却を生む。

ヒント3(誘導)
def cascading_cut(y):
    z = parent[y]
    if z != 0:
        if not mark[y]:
            mark[y] = True        # 1回目は印だけ
        else:
            cut(y, z)              # 2回目は連鎖的に切り離す
            cascading_cut(z)

extract-minでは子を全員根リストへ移し、その後 consolidate() で同じ次数の根同士をペアマージして木の本数を $O(\log N)$ 本に減らす。

模範解答 (Python)

import sys


def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    Q = int(data[idx]); idx += 1

    max_nodes = Q + 1
    val = [0] * max_nodes
    degree = [0] * max_nodes
    parent = [0] * max_nodes
    child = [0] * max_nodes
    left = [0] * max_nodes
    right = [0] * max_nodes
    mark = [False] * max_nodes

    min_node = 0
    next_id = 0

    def add_to_root_list(x):
        nonlocal min_node
        if min_node == 0:
            left[x] = x
            right[x] = x
            min_node = x
        else:
            m = min_node
            r = right[m]
            right[m] = x
            left[x] = m
            right[x] = r
            left[r] = x
            if val[x] < val[m]:
                min_node = x

    def remove_from_list(x):
        l = left[x]
        r = right[x]
        left[r] = l
        right[l] = r

    def insert(x_val):
        nonlocal next_id
        next_id += 1
        nid = next_id
        val[nid] = x_val
        degree[nid] = 0
        parent[nid] = 0
        child[nid] = 0
        mark[nid] = False
        left[nid] = nid
        right[nid] = nid
        add_to_root_list(nid)
        return nid

    def merge_child_into(y, x):
        parent[y] = x
        if child[x] == 0:
            child[x] = y
            left[y] = y
            right[y] = y
        else:
            c = child[x]
            r = right[c]
            right[c] = y
            left[y] = c
            right[y] = r
            left[r] = y
        degree[x] += 1
        mark[y] = False

    def consolidate():
        nonlocal min_node
        if min_node == 0:
            return
        roots = []
        start = min_node
        x = start
        while True:
            roots.append(x)
            x = right[x]
            if x == start:
                break
        DEG = 64
        A = [0] * DEG
        for x in roots:
            d = degree[x]
            cur = x
            while A[d] != 0:
                y = A[d]
                if val[cur] > val[y]:
                    cur, y = y, cur
                remove_from_list(y)
                merge_child_into(y, cur)
                A[d] = 0
                d += 1
            A[d] = cur
        min_node = 0
        for x in A:
            if x != 0:
                left[x] = x
                right[x] = x
                if min_node == 0:
                    min_node = x
                else:
                    m = min_node
                    r = right[m]
                    right[m] = x
                    left[x] = m
                    right[x] = r
                    left[r] = x
                    if val[x] < val[m]:
                        min_node = x

    def extract_min():
        nonlocal min_node
        z = min_node
        if z == 0:
            return None
        zval = val[z]
        if child[z] != 0:
            c0 = child[z]
            c = c0
            children = []
            while True:
                children.append(c)
                c = right[c]
                if c == c0:
                    break
            for c in children:
                remove_from_list(c)
                parent[c] = 0
                add_to_root_list(c)
        candidate = right[z]
        remove_from_list(z)
        if candidate == z:
            min_node = 0
        else:
            min_node = candidate
            consolidate()
        return zval

    def cut(x, y):
        if right[x] == x:
            child[y] = 0
        else:
            if child[y] == x:
                child[y] = right[x]
            remove_from_list(x)
        degree[y] -= 1
        add_to_root_list(x)
        parent[x] = 0
        mark[x] = False

    def cascading_cut(y):
        z = parent[y]
        if z != 0:
            if not mark[y]:
                mark[y] = True
            else:
                cut(y, z)
                cascading_cut(z)

    def decrease_key(x, k):
        nonlocal min_node
        val[x] = k
        y = parent[x]
        if y != 0 and val[x] < val[y]:
            cut(x, y)
            cascading_cut(y)
        if val[x] < val[min_node]:
            min_node = x

    out = []
    for _ in range(Q):
        t = int(data[idx]); idx += 1
        if t == 0:
            x = int(data[idx]); idx += 1
            insert(x)
        elif t == 1:
            r = extract_min()
            out.append("-1" if r is None else str(r))
        else:
            eid = int(data[idx]); idx += 1
            x = int(data[idx]); idx += 1
            decrease_key(eid, x)

    print("\n".join(out))


main()

Step-by-Step 解説

1「森」としてのヒープ表現
parent/child/left/right/degree/markの6配列で多分木の集合を表現。根同士、同じ親を持つ子同士がそれぞれ循環双方向リストになる。
2insertは根リストへの挿入だけ
add_to_root_listはリンクリスト挿入なので$O(1)$。木の統合は行わずコストを先送りする。
3decrease-keyはcutとcascading cut
markフラグで「1回目は印だけ、2回目は連鎖的に切り離す」ルールにより、1回のdecrease-keyが引き起こす切り離し回数を償却$O(1)$に抑える。
4extract-minだけがコストを支払う
consolidateで同じ次数の根同士をマージし本数を$O(\log N)$本まで減らす。ここでそれまで先送りしたコストを回収する。
5償却解析の直感
ポテンシャル=木の本数+2×mark数。insert/decrease-keyは償却$O(1)$、extract-minは償却$O(\log N)$。

よくあるミス

ミス原因正しい書き方
cascading cutを無条件に再帰させるmarkフラグの意味を理解せず毎回切り離すmarkがFalseならTrueにして止める。Trueなら切り離して親へ再帰する
extract-minで「zが唯一の根だったか」の判定を誤る子を移した後に right[z] を見て混同するremove_from_list(z)を呼ぶ直前にrigh[z]を候補として保存する
consolidateの次数テーブルサイズ不足最大次数O(log_φN)を過小評価安全側に大きめ(例:64)を確保する
decrease-keyで増加方向も許してしまうincreaseとdecreaseを区別せずヒープ順序を壊す新しい値が現在値以下であるという制約を前提にする
root list更新後min_nodeの更新漏れcut後に新しい最小値になるケースを見落とすdecrease_keyの最後で必ずval[x]<val[min_node]を確認

次のステップ

  • 発展: 複数ヒープ間の「統合(union)」クエリをO(1)で処理する拡張(根リスト同士を繋げるだけ)。Skew Heap(Day136-Q1)との統合コストの違いを比較せよ。
  • 発展2: Fibonacci Heapを用いたDijkstra法(O(E+V log V))を実装し、二分ヒープ版(O(E log V))と実測で速度比較せよ。密グラフ・疎グラフで結果がどう変わるか確認すること。

自己評価

自分の回答

気づき・メモ