如何在Dijkstra算法中使用二进制堆?

时间:2013-01-10 07:11:50

标签: heap dijkstra

我正在编写dijkstra算法的代码,对于我们应该找到与当前正在使用的节点的距离最小的节点的部分,我在那里使用一个数组并完全遍历它以找出节点。 / p>

这部分可以用二进制堆代替,我们可以在O(1)时间内找出节点,但是我们还会在进一步的迭代中更新节点的距离,我将如何合并该堆?

在数组的情况下,我所要做的就是转到第(ith -1)索引并更新该节点的值,但在二进制堆中不能做同样的事情,我将不得不做搜索以找出节点的位置,然后更新它。

这个问题的解决方法是什么?

7 个答案:

答案 0 :(得分:22)

这只是我在课堂上发现的一些信息,我和同学分享了这些信息。我以为我会让人们更容易找到它,而且我已经离开了这篇帖子,以便在找到解决方案时能够回答它。

注意:我假设这个例子中你的图形的顶点有一个ID来跟踪哪个是哪个。这可以是名称,数字等等,只需确保更改下面struct中的类型即可。 如果你没有这样的区分方法,那么你可以使用指向顶点的指针并比较它们的指向地址。

你在这里遇到的问题是,在Dijkstra算法中,我们被要求将图形顶点及其键存储在此优先级队列中,然后更新队列中剩余的键< / i>的。 但是...... 堆数据结构无法获得任何不是最小节点或最后节点的特定节点! 我们能做的最好的事情是在O(n)时间内遍历堆以找到它,然后在O(Logn)更新其密钥并冒泡。这使得更新每个边缘的所有顶点 O(n),使我们实现Dijkstra O(mn),比最佳O(mLogn)更差。

的Bleh!必须有更好的方法!

因此,我们需要实现的并不是一个标准的基于最小堆的优先级队列。我们需要比标准的4 pq操作多一个操作:

  1. 的IsEmpty
  2. 添加
  3. PopMin
  4. PeekMin
  5. DecreaseKey
  6. 为了 DecreaseKey ,我们需要:

    • 在堆内找到特定的顶点
    • 降低其键值
    • “堆积”或“冒泡”顶点

    基本上,既然你(我假设它已经在过去4个月的某个时间实现过)可能会使用“基于数组的”堆实现, 这意味着我们需要堆来跟踪数组中的每个顶点及其索引,以便能够进行此操作。

    设计struct喜欢:(c ++)

    struct VertLocInHeap
    {
        int vertex_id;
        int index_in_heap;
    };
    

    允许你跟踪它,但是将它们存储在一个数组中仍然会给你O(n)时间来查找堆中的顶点。没有复杂性改进,而且比以前更复杂。 &GT;。&LT;
    我的建议(如果目标是优化)

    1. 将此信息存储在二进制搜索树中,其键值为“vertex_id”
    2. 进行二元搜索,以便在O(Logn)堆中找到顶点的位置
    3. 使用索引访问顶点并在O(1)
    4. 中更新其键
    5. 在O(Logn)
    6. 中冒泡顶点

      我实际上使用了std::map声明为:         std :: map m_locations; 在堆中而不是使用struct。第一个参数(Key)是vertex_id,第二个参数(Value)是堆数组中的索引。 由于std::map保证了O(Logn)搜索,因此这可以很好地开箱即用。然后,无论何时插入或冒泡,您只需m_locations[vertexID] = newLocationInHeap;
      轻松赚钱。

      分析:
      上行:我们现在有O(Logn)来查找p-q中的任何给定顶点。对于冒泡我们做O(Log(n))运动,对于每个交换在数组索引的映射中进行O(Log(n))搜索,导致O(Log ^ 2(n)操作为bubble -up。
      因此,我们有一个Log(n)+ Log ^ 2(n)= O(Log ^ 2(n))操作,用于更新单个边缘的堆中的键值。这使我们的Dijkstra alg取O(mLog ^ 2(n))。这非常接近理论上的最佳值,至少与我能得到的一样接近。令人敬畏的负鼠! 下行:我们在堆中的内存信息中存储了两倍的信息。这是一个“现代”的问题吗?并不是的;我的桌面可存储超过80亿个整数,许多现代计算机至少配备8GB内存;然而,它仍然是一个因素。如果你使用一个包含40亿个顶点的图形来实现这个实现,这可能比你想象的要频繁得多,那么它就会导致问题。此外,所有那些可能不会影响分析复杂性的额外读/写,可能仍需要一些机器上的时间,特别是如果信息存储在外部。

      我希望这对未来的某个人有所帮助,因为我有一个时间找到所有这些信息的魔鬼,然后拼凑我从这里,那里,到处都有的各个部分来形成这个。我责怪互联网和睡眠不足。

答案 1 :(得分:4)

使用任何形式的堆遇到的问题是,您需要重新排序堆中的节点。为了做到这一点,你必须不断地从堆中弹出所有东西,直到找到你需要的节点,然后改变重量,然后将其推回(与你弹出的其他所有内容一起)。老实说,只使用数组可能会比那更有效,更容易编码。

我解决这个问题的方法是使用红黑树(在C ++中它只是STL的set<>数据类型)。数据结构包含pair<>元素,其中double(成本)和string(节点)。由于树结构,访问最小元素非常有效(我相信C ++通过维护指向最小元素的指针使其更有效)。

除了树之外,我还保留了一系列包含给定节点距离的双精度数。因此,当我需要重新排序树中的节点时,我只是使用dist数组中的旧距离以及节点名称来在集合中找到它。然后,我将从树中删除该元素,并将其重新插入到具有新距离的树中。要搜索节点O(log n)并插入节点O(log n),因此重新排序节点的成本为O(2 * log n) = O(log n)。对于二进制堆,它还具有O(log n)用于插入和删除(并且不支持搜索)。因此,在找到所需节点之前删除所有节点的成本,更改其权重,然后重新插入所有节点。重新排序节点后,我会更改阵列中的距离以反映新的距离

老实说,我无法想到以这种方式修改堆的方法,以允许它动态地改变节点的权重,因为堆的整个结构基于节点维护的权重。

答案 2 :(得分:4)

除了Min-Heap数组之外,我还会使用哈希表。

哈希表的哈希编码键是节点对象,值是这些节点在最小堆阵列中的索引。

然后,只要你在min-heap中移动一些东西,你就需要相应地更新哈希表。由于最小堆中的每个操作最多会移动2个元素(即它们被交换),并且我们的每次移动成本是O(1)来更新散列表,那么我们不会破坏它的渐近边界。最小堆操作。例如,minHeapify是O(lgn)。我们每minHeapify操作只添加了2个O(1)哈希表操作。因此整体复杂性仍为O(lgn)。

请记住,您需要修改在min-heap中移动节点的任何方法来执行此跟踪!例如,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,因此一个主要是固定的,但你确实需要从哈希表中删除提取的密钥。您还需要修改decreaseKey以跟踪这些更改。一旦修复了,那么插入也应该被修复,因为它应该使用decreaseKey方法。这应该涵盖你的所有基础,你不会改变你的算法的渐近边界,你仍然可以继续使用堆作为优先级队列。

请注意,Fibonacci Min Heap实际上更倾向于此实现中的标准Min Heap,但这是一个完全不同的蠕虫病毒。

答案 3 :(得分:2)

此算法:http://algs4.cs.princeton.edu/44sp/DijkstraSP.java.html通过使用“索引堆”来解决此问题:http://algs4.cs.princeton.edu/24pq/IndexMinPQ.java.html基本上维护从键到数组索引的映射列表。

答案 4 :(得分:0)

我使用以下方法。每当我在堆中插入一些东西时,我都会传递一个指向整数的指针(这个内存位置由我拥有,而不是堆),它应该包含堆所管理的数组中元素的位置。因此,如果堆中的元素序列被重新排列,则应该更新这些指针所指向的值。

因此,对于Dijkstra算法,我正在创建一个大小为N的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;
};

答案 5 :(得分:0)

另一种解决方案是“懒惰删除”。您只需将节点再次插入具有新优先级的堆即可,而不是减少键操作。因此,在堆中将有另一个节点副本。但是,该节点在堆中的位置将高于以前的任何副本。然后,当获得下一个最小节点时,您只需检查节点是否已被接受。如果是,则只需省略循环并继续(延迟删除)。

由于堆内的副本,性能稍差/内存使用率更高。但是,它仍然受限(连接数量),并且可能比某些问题规模的其他实现更快。

答案 6 :(得分:0)

我认为当我们必须更新顶点距离时,主要的困难是能够达到 O(log n) 的时间复杂度。以下是有关如何执行此操作的步骤:

  1. 对于堆实现,您可以使用数组。
  2. 对于索引,使用哈希映射,以顶点号为键,以它在堆中的索引为值。
  3. 当我们想要更新一个顶点时,在 O(1) 时间内在 Hash Map 中搜索它的索引。
  4. 减少堆中的顶点距离,然后继续向上遍历(检查其与根的新距离,如果根的值更大,则交换根和当前顶点)。这一步也需要 O(log n)。
  5. 在遍历堆时进行更改时更新哈希映射中的顶点索引。

我认为这应该可行,并且总体时间复杂度为 O((E+V)*log V),正如理论所暗示的那样。