ダイクストラアルゴリズムのコードを書いています。現在使用されているノードからの距離が最小のノードを見つけることになっている部分については、その上に配列を使用し、それを完全にトラバースしてノードを特定しています。
この部分はバイナリヒープで置き換えることができ、O(1)時間でノードを計算できますが、以降の反復でノードの距離を更新します。どのようにそのヒープを組み込むのでしょうか。 ?
配列の場合、私がしなければならないのは(ith -1)インデックスに行き、そのノードの値を更新することだけですが、バイナリヒープでは同じことを実行できません。図を完全に検索する必要があります。ノードの位置を確認してから更新します。
この問題の回避策は何ですか?
これは、クラスでこれを行っているときに見つけたいくつかの情報であり、クラスメートと共有しました。人々が見つけやすいようにしたいと思ったので、解決策を見つけたときに答えられるように、この投稿は残しておきました。
注意: この例では、グラフの頂点にIDがあり、どちらがどれかを追跡することを想定しています。これは、名前、番号など、どのようなものでもかまいませんが、下のstruct
でタイプを変更してください。そのような区別の手段がない場合は、頂点へのポインタを使用して、それらのポイント先のアドレスを比較できます。
ここで直面している問題は、ダイクストラのアルゴリズムで、グラフの頂点とそのキーをこの優先度キューに格納するように求められるという事実です。 キューに残っているキーを更新する。だが... ヒープデータ構造には、最小または最後のノードではない特定のノードに到達する方法がありません。
私たちにできる最善のことは、O(n)時間でヒープをトラバースしてから、キーを更新し、Oでバブルアップします(Logn)これにより、すべての頂点が更新されます O(n) エッジごとに、ダイクストラO(mn)の実装を最適なO(mLogn)よりもはるかに悪くします。
ブレ!より良い方法があるはずです!
したがって、実装する必要があるのは、標準の最小ヒープベースの優先度キューではありません。標準の4 pq操作よりもさらに1つの操作が必要です。
のために DecreaseKey、 必要がある:
基本的に、あなたが(配列ベースの)ヒープ実装を使用することになったので(過去4か月間に実装されたと思います)、これは、 配列内の各頂点とそのインデックスを追跡するためにヒープが必要 この操作を可能にするため。
次のようなstruct
を考案する: (c ++)
struct VertLocInHeap
{
int vertex_id;
int index_in_heap;
};
それを追跡することはできますが、配列にそれらを保存すると、ヒープ内の頂点を見つけるためのO(n)時間を与えることになります。複雑さの改善はなく、それよりも複雑です。以前。>。<
私のおすすめ (ここで最適化が目標の場合):
私は実際に std::map
として宣言:std :: map m_locations;構造体を使用する代わりにヒープ内。最初のパラメーター(Key)はvertex_idで、2番目のパラメーター(Value)はヒープの配列のインデックスです。 std::map
保証O(Logn)検索、これは標準でうまく機能します。そして、挿入またはバブルするときはいつでもm_locations[vertexID] = newLocationInHeap;
簡単なお金。
分析:
メリット: O(Logn)は、pqの任意の頂点を見つけるためにあります。バブルアップの場合、O(Log(n))移動します。スワップごとにO(Log(n))配列インデックスのマップを検索し、バブルアップのためのO(Log ^ 2(n)演算を実行します。
したがって、Log(n)+ Log ^ 2(n)= O(Log^2(n)) 単一エッジのヒープ内のキー値を更新する操作。これにより、ダイクストラalgはO(mLog ^ 2(n))を取ります。これは、理論上の最適値にかなり近く、少なくとも私が得ることができる限り近くなっています。素晴らしいポッサム!
欠点: 文字列の2倍の情報をヒープのメモリに格納しています。 「現代」の問題ですか?あんまり;私のデスクは80億以上の整数を格納でき、最近の多くのコンピューターには少なくとも8 GBのRAMが搭載されています。しかし、それはまだ要因です。 40億の頂点のグラフを使用してこの実装を行った場合、思ったよりもはるかに頻繁に起こり、問題を引き起こします。また、特に情報が外部に保存されている場合、一部のマシンでは、分析の複雑さに影響を及ぼさない可能性があるこれらすべての追加の読み取り/書き込みがまだ時間がかかる場合があります。
これが将来の誰かの助けになることを願っています。私がこのすべての情報を見つけ、それから私がここ、そこ、そして至る所から得たビットをつなぎ合わせてこれを形成する時の悪魔がいたからです。私はインターネットと睡眠不足を非難しています。
Min-Heap配列に加えて、ハッシュテーブルを使用してこれを行います。
ハッシュテーブルには、ノードオブジェクトになるようにハッシュコード化されたキーと、それらのノードが最小ヒープ配列内のどこにあるかのインデックスである値があります。
次に、min-heapで何かを移動するときはいつでも、それに応じてハッシュテーブルを更新する必要があります。最大2つの要素が最小ヒープ内の操作ごとに移動される(つまり、それらが交換される)ため、移動ごとのコストはO(1)でハッシュテーブルを更新するため)、は、min-heap操作の漸近的な境界に損傷を与えません。たとえば、minHeapifyはO(lgn)です。minHeapify操作ごとに2 O(1)ハッシュテーブル操作を追加しました。したがって、全体的な複雑さはO(lgn)のままです。
この追跡を行うには、ノードを最小ヒープ内で移動するメソッドを変更する必要があることに注意してください。たとえば、minHeapify()では、Javaを使用して次のように変更する必要があります。
Nodes[] nodes;
Map<Node, int> indexMap = new HashMap<>();
private minHeapify(Node[] nodes,int i) {
int smallest;
l = 2*i; // left child index
r = 2*i + 1; // right child index
if(l <= heapSize && nodes[l].getTime() < nodes[i].getTime()) {
smallest = l;
}
else {
smallest = i;
}
if(r <= heapSize && nodes[r].getTime() < nodes[smallest].getTime()) {
smallest = r;
}
if(smallest != i) {
temp = nodes[smallest];
nodes[smallest] = nodes[i];
nodes[i] = temp;
indexMap.put(nodes[smallest],i); // Added index tracking in O(1)
indexMap.put(nodes[i], smallest); // Added index tracking in O(1)
minHeapify(nodes,smallest);
}
}
buildMinHeap、heapExtractはminHeapifyに依存する必要があるため、ほとんどが修正されますが、抽出されたキーもハッシュテーブルから削除する必要があります。また、これらの変更を追跡するために減少キーを変更する必要もあります。それが修正されたら、reduceKeyメソッドを使用する必要があるため、挿入も修正する必要があります。これですべてのベースがカバーされ、アルゴリズムの漸近的境界が変更されることはなく、優先度キューにヒープを使い続けることができます。
この実装では、フィボナッチ最小ヒープが実際には標準の最小ヒープよりも優先されますが、それはワームのまったく異なる缶であることに注意してください。
ヒープの任意の形式を使用して遭遇した問題は、ヒープ内のノードを並べ替える必要があることです。それを行うには、必要なノードが見つかるまでヒープからすべてをポップし続け、次に重みを変更して、(ポップした他のすべてのものとともに)プッシュバックする必要があります。正直なところ、配列を使用するだけで、おそらくそれよりも効率的でコーディングが簡単になります。
これを回避する方法は、Red-Blackツリーを使用することでした(C++では、これはSTLの_set<>
_データ型です)。データ構造には、double
(コスト)およびstring
(ノード)を持つ_pair<>
_要素が含まれていました。ツリー構造のため、最小要素にアクセスすることは非常に効率的です(C++では、最小要素へのポインターを維持することにより、さらに効率的になると思います)。
ツリーとともに、指定されたノードの距離を含むdoubleの配列も保持しました。したがって、ツリー内のノードを並べ替える必要がある場合は、dist配列からの古い距離とノード名を使用して、セット内でノードを見つけました。次に、その要素をツリーから削除し、新しい距離でツリーに再挿入します。ノードO(log n)
を検索し、ノードO(log n)を挿入するため、ノードを並べ替えるコストはO(2 * log n)
= O(log n)
です。バイナリヒープの場合、挿入と削除の両方にO(log n)
も含まれます(検索はサポートされていません)。したがって、必要なノードが見つかるまですべてのノードを削除するコストで、その重みを変更してから、すべてのノードを挿入します。ノードが並べ替えられたら、新しい距離を反映するように配列の距離を変更します。
ヒープの構造全体がノードが維持する重みに基づいているため、ノードの重みを動的に変更できるようにヒープを変更する方法を正直に考えることはできません。
このアルゴリズム: http://algs4.cs.princeton.edu/44sp/DijkstraSP.Java.html は、「インデックス付きヒープ」を使用してこの問題を回避します: http:// algs4。 cs.princeton.edu/24pq/IndexMinPQ.Java.html キーから配列インデックスへのマッピングのリストを本質的に維持します。
私は次のアプローチを使用しています。ヒープに何かを挿入するときは常に、ヒープに管理されている配列内の要素の位置を含む整数へのポインタ(このメモリの場所は、ヒープではなく私が所有しています)を渡します。したがって、ヒープ内の要素のシーケンスが再配置されると、これらのポインターが指す値が更新されるはずです。
ダイクストラアルゴリズムでは、sizeNのposInHeap
配列を作成しています。
うまくいけば、コードはそれをより明確にするでしょう。
template <typename T, class Comparison = std::less<T>> class cTrackingHeap
{
public:
cTrackingHeap(Comparison c) : m_c(c), m_v() {}
cTrackingHeap(const cTrackingHeap&) = delete;
cTrackingHeap& operator=(const cTrackingHeap&) = delete;
void DecreaseVal(size_t pos, const T& newValue)
{
m_v[pos].first = newValue;
while (pos > 0)
{
size_t iPar = (pos - 1) / 2;
if (newValue < m_v[iPar].first)
{
swap(m_v[pos], m_v[iPar]);
*m_v[pos].second = pos;
*m_v[iPar].second = iPar;
pos = iPar;
}
else
break;
}
}
void Delete(size_t pos)
{
*(m_v[pos].second) = numeric_limits<size_t>::max();// indicate that the element is no longer in the heap
m_v[pos] = m_v.back();
m_v.resize(m_v.size() - 1);
if (pos == m_v.size())
return;
*(m_v[pos].second) = pos;
bool makingProgress = true;
while (makingProgress)
{
makingProgress = false;
size_t exchangeWith = pos;
if (2 * pos + 1 < m_v.size() && m_c(m_v[2 * pos + 1].first, m_v[pos].first))
exchangeWith = 2 * pos + 1;
if (2 * pos + 2 < m_v.size() && m_c(m_v[2 * pos + 2].first, m_v[exchangeWith].first))
exchangeWith = 2 * pos + 2;
if (pos > 0 && m_c(m_v[pos].first, m_v[(pos - 1) / 2].first))
exchangeWith = (pos - 1) / 2;
if (exchangeWith != pos)
{
makingProgress = true;
swap(m_v[pos], m_v[exchangeWith]);
*m_v[pos].second = pos;
*m_v[exchangeWith].second = exchangeWith;
pos = exchangeWith;
}
}
}
void Insert(const T& value, size_t* posTracker)
{
m_v.Push_back(make_pair(value, posTracker));
*posTracker = m_v.size() - 1;
size_t pos = m_v.size() - 1;
bool makingProgress = true;
while (makingProgress)
{
makingProgress = false;
if (pos > 0 && m_c(m_v[pos].first, m_v[(pos - 1) / 2].first))
{
makingProgress = true;
swap(m_v[pos], m_v[(pos - 1) / 2]);
*m_v[pos].second = pos;
*m_v[(pos - 1) / 2].second = (pos - 1) / 2;
pos = (pos - 1) / 2;
}
}
}
const T& GetMin() const
{
return m_v[0].first;
}
const T& Get(size_t i) const
{
return m_v[i].first;
}
size_t GetSize() const
{
return m_v.size();
}
private:
Comparison m_c;
vector< pair<T, size_t*> > m_v;
};
別の解決策は「遅延削除」です。キー操作を減らす代わりに、ノードをもう一度挿入して、新しい優先順位でヒープを作成します。したがって、ヒープ内にはノードの別のコピーがあります。ただし、そのノードは、以前のどのコピーよりもヒープ内で上位になります。次に、次の最小ノードを取得するときに、ノードがすでに受け入れられているかどうかを確認できます。そうであれば、ループを省略して続行します(遅延削除)。
これは、ヒープ内のコピーのために、パフォーマンスが少し低下し、メモリ使用量が多くなります。ただし、それでも(接続数に)制限があり、問題のサイズによっては他の実装よりも高速になる場合があります。