web-dev-qa-db-ja.com

C ++のキー更新で最小優先度キューを使用する最も簡単な方法

プログラミングコンテストなどで、Dijkstraアルゴリズムなどを実装するために、減少キーを使用した最小優先度キューの単純な実装が必要になることがあります。 )一緒にそれを達成します。

  • セットに要素を追加するには、O(log(N)) time。N個の要素から優先度キューを構築するには、それらを1つずつセットに追加します。これにはO( N log(N))合計時間。

  • Min key_valueを持つ要素は、単にセットの最初の要素です。最小の要素を調べるには、O(1)時間がかかります。削除するには、O(log(N))時間がかかります。

  • ID = kがセット内にあるかどうかをテストするには、まず配列でそのkey_value = v_kを検索し、次にセット内の要素(v_k、k)を検索します。これにはO(log(N))時間かかります。

  • ID = kのkey_valueをv_kからv_k 'に変更するには、まず配列でそのkey_value = v_kを検索し、次にセット内の要素(v_k、k)を検索します。次に、その要素をセットから削除し、要素(v_k '、k)をセットに挿入します。次に、配列も更新します。これにはO(log(N))時間かかります。

上記のアプローチは機能しますが、ほとんどの教科書では通常、バイナリヒープを構築する時間はO(N)であるため、バイナリヒープを使用して優先度キューを実装することを推奨しています。バイナリヒープを使用するC++のSTLには、優先度キューデータ構造が組み込まれていると聞きました。ただし、そのデータ構造のkey_valueを更新する方法がわかりません。

C++でキー更新で最小優先度キューを使用する最も簡単で効率的な方法は何ですか?

51
Chong Luo

ダレンが既に言ったように、 std::priority_queue には要素の優先度を下げる手段がなく、現在の最小値以外の要素を削除する手段もありません。ただし、デフォルトのstd::priority_queueは、std::vectorのstdヒープ関数を使用する<algorithm>を囲む単純なコンテナーアダプターにすぎません( std::Push_heap 、- std::pop_heap および std::make_heap )。したがって、Dijkstra(優先度の更新が必要な場合)の場合、通常は自分でこれを行い、単純なstd::vectorを使用します。

プッシュはO(log N)操作になります

vec.Push_back(item);
std::Push_heap(vec.begin(), vec.end());

もちろん、N個の要素から新たにキューを構築するために、このO(log N)操作を使用してそれらすべてをプッシュするのではなく(すべてをO(Nlog N)にします)、単純にOが続くベクトルにすべて入れます(N)

std::make_heap(vec.begin(), vec.end());

Min要素は単純なO(1)です

vec.front();

ポップは単純なO(log N)シーケンスです

std::pop_heap(vec.begin(), vec.end());
vec.pop_back();

これまでのところ、これはstd::priority_queueが通常内部で行うことです。アイテムの優先度を変更するには、アイテムのタイプに組み込むこともできますが、シーケンスを再び有効なヒープにするだけです。

std::make_heap(vec.begin(), vec.end());

これはO(N)操作であることがわかりますが、一方で、これにより、追加のデータ構造を使用してヒープ内のアイテムの位置を追跡する必要がなくなります。また、対数優先度の更新に対するパフォーマンスの低下は、使いやすさ、std::vector(ランタイムにも影響を与える)のコンパクトで線形のメモリ使用量を考慮して、実際にはそれほど重要ではありません。とにかく、私は多くの場合、エッジがほとんどないグラフ(頂点カウントで線形)で作業します。

すべての場合において最速の方法とは限りませんが、確かに最も簡単な方法です。

EDIT:ああ、標準ライブラリは最大ヒープを使用しているため、優先度を比較するために>と同等のものを使用する必要があります(ただし、デフォルトの<演算子の代わりに、アイテムから取得します)。

42
Christian Rau

私の回答は元の質問には答えませんが、ダイクストラアルゴリズムをC++/Java(私のように)に実装しようとするときにこの質問に答える人にとっては役立つと思います。

前述のように、C++のpriority_queue(またはJavaのPriorityQueue)はdecrease-key操作を提供しません。 Dijkstraを実装するときにこれらのクラスを使用するための素晴らしいトリックは、「遅延削除」を使用することです。ダイクストラアルゴリズムのメインループは、優先キューから処理される次のノードを抽出し、そのすべての隣接ノードを分析し、最終的に優先キュー内のノードの最小パスのコストを変更します。これは、そのノードの値を更新するためにdecrease-keyが通常必要とされるポイントです。

トリックは変更しないまったくです。代わりに、そのノードの「新しいコピー」が(その新しいより良いコストと共に)優先度キューに追加されます。コストが低いため、ノードの新しいコピーはキュー内の元のコピーの前に抽出されるため、より早く処理されます。

この「遅延削除」の問題は、コストの高いノードの2番目のコピーが最終的に優先キューから抽出されることです。しかし、それは、2番目に追加されたコピーがより良いコストで処理された後に常に発生します。 最初のこと優先キューから次のノードを抽出するときにメインのダイクストラループが実行する必要があるのは、ノードが以前にアクセスされたかどうかを確認することです(最短経路は既にわかっています)。 「遅延削除」を行うのはまさにその瞬間であり、要素は単に無視する必要があります。

優先度キューには削除されていない「デッド要素」が格納されているため、このソリューションにはメモリと時間の両方のコストがかかります。しかし、実際のコストは非常に小さく、このソリューションのプログラミングは、欠落しているdecrease-key操作をシミュレートしようとする他の代替手段よりも簡単です。

37
Googol

_std::priority_queue_ クラスでは_decrease-key_スタイルの操作を効率的に実装できるとは思わない。

これをサポートする独自のバイナリヒープベースのデータ構造を、_std::set_ベースの優先度キューで説明したものと非常によく似た行に沿ってロールバックしました。

  • _pair<value, ID>_の要素と_ID -> heap_index_をマッピングする配列を格納するvalueでソートされたバイナリヒープを維持します。ヒープルーチン_heapify_up, heapify_down_などでは、マッピング配列が要素の現在のヒープ位置と同期していることを確認する必要があります。これにより、余分なO(1)オーバーヘッドが追加されます。
  • 配列のヒープへの変換は、 here で説明されている標準アルゴリズムに従って、O(N)で実行できます。
  • ルート要素を覗くと、O(1)になります。
  • IDが現在ヒープにあるかどうかを確認するには、マッピング配列でO(1)ルックアップが必要です。これにより、IDに対応する要素をO(1)ピークすることもできます。
  • _Decrease-key_には、マッピング配列でのO(1)ルックアップと、それに続く_heapify_up, heapify_down_を介したヒープのO(log(N))更新が必要です。
  • ヒープから既存のアイテムをポップするのと同様に、新しいアイテムをヒープにプッシュするのはO(log(N))です。

そのため、_std::set_ベースのデータ構造と比較して、いくつかの操作のランタイムが漸近的に改善されています。もう1つの重要な改善点は、バイナリヒープを配列に実装できる一方で、バイナリツリーはノードベースのコンテナであるということです。バイナリヒープの余分なデータの局所性は、通常、ランタイムの向上につながります。

この実装のいくつかの問題は次のとおりです。

  • 整数項目IDのみを許可します。
  • ゼロから始まるアイテムIDの密な分布を前提としています(そうでなければ、マッピング配列のスペースの複雑さが爆発します!)。

マッピング配列ではなく、マッピングハッシュテーブルを維持すると、これらの問題を潜在的に克服できますが、実行時のオーバーヘッドが少し増えます。私の使用では、整数IDで十分でした。

お役に立てれば。

17
Darren Engwirda