代表的な面接トピック

コーディング面接:フィボナッチヒープの実装とdecrease-keyのならし計算量の説明

コーディング難しい
Offer.cc 編集チーム公開日 更新日

質問

フィボナッチヒープのinsert、meld、find-min、extract-min、decrease-key、deleteを実装し、その主要な操作のならし計算量を証明してください。

問題

insert、meld、find-min、extract-min、decrease-key、deleteをサポートするフィボナッチヒープを実装してください。ルートリスト、親子リンク、次数(degree)、マークビット、カスケードカットがどのように連携して機能するかを説明し、ポテンシャル関数を使用して、insert、meld、find-min、decrease-keyがならしO(1)であり、extract-minがならしO(log n)である理由を示してください。

面接官がテストしていること

  • すべてのならしO(1)操作が常にO(1)であると述べるのではなく、実際のコストとならしコストを区別できているか。
  • 双方向循環リスト、最小値ルートポインタ、ノードハンドル、親ポインタを正しく維持管理できるか。
  • decrease-keyがカット、マーキング、カスケードカットを正しく実行しているか。
  • 理論上の優位性、エンジニアリングにおける定数倍のオーバーヘッド、ペアリングヒープや二分ヒープとのトレードオフを説明できるか。

模範解答

フィボナッチヒープは、ヒープ順序を満たす木の集合です。ルートは双方向循環リストを形成し、各ノードは親、子リスト、次数、マークビットを保持します。この構造では、次数ごとにルートが結合されるextract-minの実行時まで、統合作業(consolidation)を遅延させます。

insertはノードをルートリストに追加して最小値を更新します。meldは2つのルートリストを結合します。decrease-keyによってヒープ順序が崩れた場合、そのノードを親からカットしてルートリストに追加します。親がすでに子を1つ失っている場合は、再帰的にカスケードカットを実行します。マークは、ノードがすでに子を1つ失っているかどうかを記録し、カスケードによる影響を制限します。

extract-minは最小値ルートの子をルートリストに昇格させ、そのルートを削除し、同じ次数を持つルートを繰り返し結合します。一般的なポテンシャルは、ルートの数にマークされたノードの数の2倍を加えた値です。insertとmeldはルートを増やしますが、定数コストしか支払いません。カスケードカットはマークされたノードを減らし、そのコストはポテンシャルによって支払われます。ヒープ順序によって最大次数が制限されているため、extract-minでの結合回数はO(log n)に抑えられます。

実装のスケッチ

以下の擬似コードは重要なdecrease-keyの処理フローを示しています。ノードハンドルは呼び出し元が保持します。

text
decreaseKey(x, newKey):
  if newKey > x.key: error
  x.key = newKey
  p = x.parent
  if p is not empty and x.key < p.key:
    cut(x, p)
    cascadingCut(p)
  if x.key < min.key:
    min = x

cut(x, p):
  removeFromChildList(p, x)
  p.degree -= 1
  addToRootList(x)
  x.parent = empty
  x.mark = false

cascadingCut(y):
  p = y.parent
  if p is empty: return
  if y.mark is false:
    y.mark = true
  else:
    cut(y, p)
    cascadingCut(p)

extract-minの実行中は、最小値ルートを削除する前に子を昇格させながら、nextポインタを安全に退避させてください。同じ次数のルートを結合する際は、親、子、次数、マークを更新し、その後に新しい最小値を走査して見つける必要があります。

よくある落とし穴

  • 配列ベースの二分ヒープを書いて、それがフィボナッチヒープのならしO(1)のdecrease-keyを持つと主張してしまう。
  • カット後に親ポインタやマークのクリアを忘れ、次のカスケードを破損させてしまう。
  • 無効なnextポインタを使用しながら、双方向循環リストからの削除を行ってしまう。
  • extract-min後に古いルートのみを比較し、ルートリストに昇格した子ノードや最小値スキャンを見落としてしまう。
  • ポインタの追跡、キャッシュ局所性、メモリアロケーション、実装の複雑さを無視して、漸近的限界値(O表記)のみを比較してしまう。

計算量のトレードオフ

decrease-keyが頻繁に発生し、meldが必要で、ならし解析が許容される場合、フィボナッチヒープは魅力的な理論的限界値を持ちます。代表的な例として、プリム法やダイクストラ法の計算量改善が挙げられます。本番環境では、ペアリングヒープ、ランク・ペアリングヒープ、二分ヒープの方がシンプルでキャッシュ効率が高いため、より競争力がある場合が多々あります。

これらの計算量はノードハンドルを保持していることを前提としています。呼び出し元がキーによってのみノードを検索できる場合、補助インデックスが必要となり設計が変わります。並行実装では、ルートリストとハンドルの所有権も定義する必要があります。ロックフリーの安全性はならし解析から自動的に得られるものではありません。

単一要素、重複キー、空のヒープとのmeld、連続するdecrease-key、最後のノードの削除などをテストしてください。ランダムな操作シーケンスを生成し、最小値とextract-minの順序を基準となる優先度付きキューと比較します。ノードが連続して2つの子を失う状況を構築し、1回目の喪失でマークされ、2回目の喪失でカットされることを確認してください。

参考文献

  • MIT OpenCourseWare Fibonacci heaps 講義:ポテンシャル解析と decrease-key / extract-min の計算量。
  • Fibonacci Heaps Revisited:カスケードカットとならし計算量の再検証。
  • Fredman と Tarjan の原著論文:Fibonacci heaps and their use in network optimization algorithms。

関連する質問

マークされたノードがポテンシャルに2ユニット寄与するのはなぜですか?

カスケードカットは、マークされたノードを1つ削除し、ルートを1つ追加します。2ユニットのポテンシャルにより、マークのクリアとルートの追加のコストが賄われるため、カスケード全体のならしコストが定数に保たれます。

extract-minがならしO(log n)であるのはなぜですか?

最小値を削除してその子を昇格させた後、統合作業によって次数ごとに最大1つのルートのみが残されます。ヒープ順序によりノードの次数はその部分木のサイズに結びつけられているため、最大次数はO(log n)となり、結合回数が制限されます。

meldがならしO(1)で可能なのはなぜですか?

2つの循環ルートリストを直接連結し、それらの最小値ポインタを比較するだけで済むためです。同じ次数の木の統合は、その後のextract-minまで行われません。

二分ヒープの方が適しているのはどのような場合ですか?

decrease-keyの頻度が低い場合、配列の局所性が重要な場合、ノードハンドルの管理が不便な場合、またはチームがシンプルな実装を重視する場合には、二分ヒープを使用します。二分ヒープは予測可能なメモリ挙動でO(log n)の操作を提供します。

deleteはどのように実装しますか?

ノードのキーを負の無限大に減らし、extract-minを呼び出します。本番環境の実装では、有効なビジネスキーが番兵と誤認されないよう、キーのドメイン、番兵の動作、ハンドルの無効化を適切に定義する必要があります。

公開情報ソース

関連する質問

関連面接ツール

コーディング問題にはスクリーンショットを使用

問題をキャプチャし、制約条件、解法アプローチ、コード、エッジケース、計算量の順に進めます。

ツールを見る