在 C++ 中使用带有键更新的最小优先级队列的最简单方法
有时在编程比赛等期间,我们需要一个带有递减键的最小优先级队列的简单工作实现来实现 Dijkstra 算法等。我经常使用 set<;对<键值,ID> >和一个数组(映射 ID-->key_value)一起来实现这一点。
向集合中添加一个元素需要 O(log(N)) 时间。要使用 N 个元素构建优先级队列,我们只需将它们一一添加到集合中。这总共需要 O(N log(N)) 时间。
具有最小 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++ 中使用最小优先级队列和键更新的最简单、最有效的方法是什么?
Sometimes during programming contests etc., we need a simple working implementation of min priority queue with decrease-key to implement Dijkstra algorithm etc.. I often use set< pair<key_value, ID> > and an array (mapping ID-->key_value) together to achieve that.
Adding an element to the set takes O(log(N)) time. To build a priority queue out of N elements, we simply add them one by one into the set. This takes O(N log(N)) time in total.
The element with min key_value is simply the first element of the set. Probing the smallest element takes O(1) time. Removing it takes O(log(N)) time.
To test whether some ID=k is in the set, we first look up its key_value=v_k in the array and then search the element (v_k, k) in the set. This takes O(log(N)) time.
To change the key_value of some ID=k from v_k to v_k', we first look up its key_value=v_k in the array, and then search the element (v_k, k) in the set. Next we remove that element from the set and then insert the element (v_k', k) into the set. We then update the array, too. This takes O(log(N)) time.
Although the above approach works, most textbooks usually recommend using binary heaps to implement priority queues, as the time of building the binary heaps is just O(N). I heard that there is a built-in priority queue data structure in STL of C++ that uses binary heaps. However, I'm not sure how to update the key_value for that data structure.
What's the easiest and most efficient way of using min priority queue with key update in C++?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
虽然我的回答不会回答原来的问题,但我认为这对于尝试在 C++/Java 中实现 Dijkstra 算法(比如我自己)时遇到这个问题的人来说可能很有用,这是 OP 评论的,
priority_queue<如前所述,C++ 中的 /code>(或 Java 中的
PriorityQueue
)不提供decrease-key
操作。在实现 Dijkstra 时使用这些类的一个好技巧是使用“延迟删除”。 Dijkstra算法的主循环从优先级队列中提取下一个要处理的节点,并分析其所有相邻节点,最终改变优先级队列中节点的最小路径成本。这就是通常需要decrease-key
来更新该节点的值的地方。诀窍是根本不改变。相反,该节点的“新副本”(具有新的更好的成本)被添加到优先级队列中。由于成本较低,节点的新副本将在队列中的原始副本之前被提取,因此它将被更早地处理。
这种“惰性删除”的问题在于,节点的第二个副本具有较高的不良成本,最终将从优先级队列中提取。但是,在处理第二个添加的具有更好成本的副本之后,这种情况总是会发生。因此,从优先级队列中提取下一个节点时,主 Dijkstra 循环必须做的第一件事是检查该节点是否先前已被访问过(并且我们已经知道最短路径)。正是在那个精确的时刻,我们将进行“延迟删除”,并且必须简单地忽略该元素。
这种解决方案会产生内存和时间成本,因为优先级队列存储着我们尚未删除的“死元素”。但真正的成本将相当小,而且恕我直言,对这个解决方案进行编程比任何其他尝试模拟缺失的
decrease-key
操作的替代方案更容易。Although my response will not answer the original question, I think it could be useful for people who reach this question when trying to implement Dijkstra algorithm in C++/Java (like myself), something that was comment by the OP,
priority_queue
in C++ (orPriorityQueue
in Java) do not provide adecrease-key
operation, as said previously. A nice trick for using those classes when implementing Dijkstra is using "lazy deletion". The main loop of Dijkstra algorithm extracts the next node to be processed from the priority queue, and analises all its adjacent nodes, eventually changing the cost of the minimal path for a node in the priority queue. This is the point wheredecrease-key
is usually needed in order to update the value of that node.The trick is not change it at all. Instead, a "new copy" for that node (with its new better cost) is added into the priority queue. Having a lower cost, that new copy of the node will be extracted before the original copy in the queue, so it will be processed earlier.
The problem with this "lazy deletion" is that the second copy of the node, with the higher bad cost, will be eventually extracted from the priority queue. But that will be always occur after the second added copy, with a better cost, has being processed. So the very first thing that the main Dijkstra loop must do when extracting the next node from the priority queue is checking if the node has being previously visited (and we know the shortest path already). It is in that precise moment when we will be doing the "lazy deletion" and the element must be simply ignored.
This solution will have a cost both in memory and time, because the priority queue is storing "dead elements" that we have not removed. But the real cost will be quite small, and programming this solution is, IMHO, easier than any other alternative that tries to simulate the missing
decrease-key
operation.好吧,正如达伦已经说过的,< 中的 std 堆函数。 /code> (
std::priority_queue
没有办法降低元素的优先级,也没有删除除当前最小值之外的元素。但默认的 std::priority_queue 只不过是一个围绕std::vector
的简单容器适配器,它使用std::push_heap
,std::pop_heap
和std::make_heap
)。因此,对于 Dijkstra(需要优先更新的地方),我通常自己执行此操作并使用简单的std::vector
。那么推送只是 O(log N) 操作
当然,为了从 N 个元素重新构造一个队列,我们不会使用这个 O(log N) 操作将它们全部推送(使整个事情变得 O(Nlog N)),但是只需将它们全部放入向量中,然后是一个简单的 O(N)
最小元素是一个简单的 O(1)
弹出是一个简单的 O(log N) 序列
到目前为止,这就是 std::priority_queue
std::priority_queue< /code> 通常在 兜帽。现在要更改项目的优先级,我们只需要更改它(但是它可能会合并到项目的类型中)并使序列再次成为有效的堆
我知道这是一个 O(N) 操作,但另一方面,这会删除任何需要使用附加数据结构或(更糟糕的是)项目类型的扩展来跟踪项目在堆中的位置。考虑到 std::vector 的易用性、紧凑性和线性内存使用(这也会影响运行时),并且考虑到对数优先级更新的性能损失实际上并不那么重要,而且事实上无论如何,我经常使用具有相当少边(顶点数呈线性)的图。
它可能并非在所有情况下都是最快的方法,但肯定是最简单的方法。
编辑:哦,由于标准库使用最大堆,您需要使用与
>
等效的内容来比较优先级(但是您从项目中获取它们),而不是默认的<
运算符。Well, as Darren already said,
std::priority_queue
doesn't have means for decreasing the priority of an element and neither the removal of an element other than the current min. But the defaultstd::priority_queue
is nothing more than a simple container adaptor around astd::vector
that uses the std heap functions from<algorithm>
(std::push_heap
,std::pop_heap
andstd::make_heap
). So for Dijkstra (where you need priority update) I usually just do this myself and use a simplestd::vector
.A push is then just the O(log N) operation
Of course for constructing a queue anew from N elements, we don't push them all using this O(log N) operation (making the whole thing O(Nlog N)) but just put them all into the vector followed by a simple O(N)
The min element is a simple O(1)
A pop is the simple O(log N) sequence
So far this is just what
std::priority_queue
usually does under the hood. Now to change an item's priority we just need to change it (however it may be incorporated in the item's type) and make the sequence a valid heap againI know this is an O(N) operation, but on the other hand this removes any need for keeping track of an item's position in the heap with an additional data structure or (even worse) an augmentation of the items' type. And the performance penalty over a logarithmic priority update is in practice not that signficant, considering the ease of use, compact and linear memory useage of
std::vector
(which impacts runtime, too), and the fact that I often work with graphs that have rather few edges (linear in the vertex count) anyway.It may not in all cases be the fastest way, but certainly the easiest.
EDIT: Oh, and since the standard library uses max-heaps, you need to use an equivalent to
>
for comparing priorities (however you get them from the items), instead of the default<
operator.我不认为
std::priority_queue
类可以实现高效decrease-key
风格操作的实现。我推出了自己的基于二进制堆的数据结构来支持这一点,基本上与您为基于
std::set
的优先级队列所描述的非常相似:value
存储pair
的元素以及映射ID -> 的数组。堆索引
。在堆例程heapify_up、heapify_down
等中,有必要确保映射数组与元素的当前堆位置保持同步。这会增加一些额外的O(1)
开销。O(N)
内完成数组到堆的转换。 /a>.O(1)
。ID
当前是否在堆中只需要在映射数组中进行O(1)
查找。这还允许O(1)
查看与任何ID
对应的元素。Decrease-key
需要在映射数组中进行O(1)
查找,然后对映射数组进行O(log(N))
更新通过heapify_up、heapify_down
进行堆。O(log(N))
,就像从堆中弹出现有项目一样。因此,与基于
std::set
的数据结构相比,一些操作的运行时间逐渐得到改善。另一个重要的改进是二叉堆可以在数组上实现,而二叉树是基于节点的容器。二进制堆的额外数据局部性通常会转化为改进的运行时间。此实现的一些问题是:
ID
。ID
的分布紧密,从零开始(否则映射数组的空间复杂度会爆炸!)。如果您维护映射哈希表而不是映射数组,则可能会克服这些问题,但运行时开销会稍多一些。对于我的使用来说,整数
ID
一直就足够了。希望这有帮助。
I don't think the
std::priority_queue
class allows for an efficient implementation ofdecrease-key
style operations.I rolled my own binary heap based data structure that supports this, bascially along very similar lines to what you've described for the
std::set
based priority queue you have:value
that stores elements ofpair<value, ID>
and an array that mapsID -> heap_index
. Within the heap routinesheapify_up, heapify_down
etc it's necessary to ensure that the mapping array is kept in-sync with the current heap position of elements. This adds some extraO(1)
overhead.O(N)
according to the standard algorithm described here.O(1)
.ID
is currently in the heap just requires anO(1)
look-up in the mapping array. This also allowsO(1)
peeking at the element corresponding to anyID
.Decrease-key
requires anO(1)
look-up in the mapping array followed by anO(log(N))
update to the heap viaheapify_up, heapify_down
.O(log(N))
as is popping an exitsing item from the heap.So asymptotically the runtime is improved for a few of the operations compared with the
std::set
based data structure. Another important improvment is that binary heaps can be implemented on an array, while binary trees are node-based containers. The extra data locality of the binary heap usually translates to improved runtime.A few issues with this implementation are:
ID
's.ID
's, starting at zero (otherwise the space complexity of the mapping array blows up!).You could potentially overcome these issues if you maintained a mapping hash-table, rather than a mapping array, but with a little more runtime overhead. For my use, integer
ID
's have always been enough.Hope this helps.
实际上,有一种方法可以使用标准 C++ 库中的内置二进制堆。关键的观察是所有堆函数的实现(即 std::push_heap , std::pop_heap和 std::make_heap) 仅需要类的存储元素中的以下方法A:
A::A()
A& A::operator=(const A& rhs)
bool 运算符<(const A& lhs, const A& rhs)
表示每当移动元素时都会调用赋值运算符在存储所有堆元素的容器中。通过重载该运算符,您可以控制堆中的索引。有了索引后,您可以访问堆中的元素,更改其值并调用 std: :push_heap 更新其在堆中的位置。
查看 Dijkstra 算法的简化实现(无图):
我知道这个解决方案取决于标准库的内部实现,但是它只适用于我所知道的任何编译器,并且我在编程比赛中使用了它。
Actually, there is a way to use built-in binary heap from the standard c++ library. The key observation is that the implementation of all heap functions (i.e. std::push_heap, std::pop_heap and std::make_heap) need only the following methods from the stored element of class A:
A::A()
A& A::operator=(const A& rhs)
bool operator<(const A& lhs, const A& rhs)
It means that assignment operator is called whenever an element is moved in the container storing all heap elements. By overloading this operator you can control the index in the heap. When you have the index you can access the element in the heap, change its value and call std::push_heap to update its position in the heap.
See the simplified implementation of Dijkstra algorithm (without graph):
I know that this solution depends on the inner implementation of the standard library, however it just works for any compiler I am aware of and I used it in programming competitions.
我认为目前 C++ 中没有简单的方法(之前做了一些研究)。
但我可以发布这样一个数据结构的实现:
作为奖励,它在
MakeHeap()
操作中具有一定的并行性。I think there is no easy way in C++ currently for that (did some research earlier).
But I can post my implementation for such a data structure:
As a bonus, it comes with some parallelism in
MakeHeap()
operation.使用这些代码而不是使用STL优先级队列
arr:要插入到优先级队列中的元素数组
n:要插入到优先级队列中的元素数量
索引:元素索引(从1开始,而不是从0开始)
new_key :新值(键值)
用于构建优先级队列。
用于删除元素
用于调整父项
用于减少键 // 也可用于增加键
用于插入优先级队列
用于提取顶部(最小)元素
以相同的顺序使用此模块
并且此代码也可以针对自定义数据类型进行修改
这里使用 long int 块的值作为键
Use these code instead of using STL priority QUEUE
arr : array of elements which you want to insert in priority queue
n : number of element which you want to insert in priority queue
index : index of elements( starting from 1, not from zero)
new_key : new value(key value)
For building Priority QUEUE.
For Deleting an Element
FOR Adjusting parents
For Decreasing key // can also be use for increasing key
For Inserting in Priority Queue
For Extracting top(min) element
Use this modules in same order
And this code could also be modified for custom data type
Here value of long int block is used as key