我正在做一个路径规划程序,其中我有一个优先级队列**'U'**:
using HeapKey = pair<float, float>;
vector<pair<HeapKey, unsigned int>> U;
我将优先级队列作为二进制最小堆进行排序和维护(也就是队列中最便宜的第一个节点)使用greater作为比较函数来获得最小堆(可能不重要)。当程序执行并规划路径时,它使用push_back向'U'添加节点(),然后是push_heap(),以使该节点进入正确的顺序,并且那里的一切工作正常...
然而,我使用的算法有时会调用新值更新**'U'中已经存在的节点,方法是将其从'U'中删除(我用find_if()找到它,用erase删除它(),如果这很重要的话),然后调用函数重新插入(再次执行push_back(),然后执行push_heap()),这样节点就有了更新后的值。
这对我来说是一个意想不到的问题。我不是这方面的Maven,但就我所能想到的,因为节点被移到了'U'内部的某个地方,所以它会打乱堆的顺序。我可以通过使用make_heap来让程序工作然而,这个解决方案带来了另一个问题,因为程序现在需要更多的时间来完成,时间越长,堆中的my map/节点越大,大概是因为每次更新节点时,**make_heap()**都要重新组织/迭代整个堆,从而减慢了整体规划。
很遗憾我没有时间去改变我的程序并得到新的结果,我只能使用简单的,我可以快速实现的简单解决方案。我主要是来学习,也许看看是否有一些建议可以传递给你,当你不只是删除第一个或最后一个元素,而是删除中间的元素时,如何有效地维护一个堆/优先级队列。减少计划所需的时间是我唯一缺少的。
尝试最小化可重现示例,而不进入实际算法,例如:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
using Cost = float;
using HeapKey = pair<Cost, Cost>;
pair<Cost, Cost> PAIR1;
vector<pair<HeapKey, unsigned int>> U;
using KeyCompare = std::greater<std::pair<HeapKey, unsigned int>>;
int in_U[20];
ostream& operator<<(ostream& os, pair<Cost, Cost> const& p) {
return os << "<" << p.first << ", " << p.second << ">";
}
bool has_neightbor(unsigned int id) {
if ( (in_U[id+1]) && (in_U[id-1])) {
return true;
}
return false;
}
void insert(unsigned int id, HeapKey k) {
U.push_back({ k, id });
push_heap(U.begin(), U.end(), KeyCompare());
in_U[id]++;
}
void update(unsigned int id) {
Cost x;
Cost y;
if (id != 21) { //lets say 21 is the goal
x = U[id].first.first;
y = U[id].first.second;
}
if (in_U[id]) {
auto it = find_if(U.begin(), U.end(), [=](auto p) { return p.second == id; });
U.erase(it);
make_heap(U.begin(), U.end(), KeyCompare());
in_U[id]--;
}
int r1 = rand() % 10 + 1;
int r2 = rand() % 10 + 1;
if (x != y) {
insert(id, {x + r1, y + r2});
}
}
int main() {
U.push_back({ {8, 2}, 1 });
in_U[1]++;
U.push_back({ {5, 1}, 2 });
in_U[2]++;
U.push_back({ {6, 1}, 3 });
in_U[3]++;
U.push_back({ {6, 5}, 4 });
in_U[4]++;
U.push_back({ {2, 3}, 5 });
in_U[5]++;
U.push_back({ {2, 9}, 6 });
in_U[6]++;
U.push_back({ {9, 2}, 7 });
in_U[7]++;
U.push_back({ {4, 7}, 8 });
in_U[8]++;
U.push_back({ {11, 4}, 9 });
in_U[9]++;
U.push_back({ {2, 2}, 10 });
in_U[10]++;
U.push_back({ {1, 2}, 11 });
in_U[11]++;
U.push_back({ {7, 2}, 12 });
in_U[12]++;
make_heap(U.begin(), U.end(), KeyCompare());
PAIR1.first = 14;
PAIR1.second = 6;
while (U.front().first < PAIR1) {
cout << "Is_heap?: " << is_heap(U.begin(), U.end(), KeyCompare()) << endl;
cout << "U: ";
for (auto p : U) {
cout << p.second << p.first << " - ";
}
cout << endl;
auto uid = U.front().second;
pop_heap(U.begin(), U.end(), KeyCompare());
U.pop_back();
if (has_neightbor(uid)) {
update(uid - 1);
update(uid + 1);
}
}
//getchar();
}
2条答案
按热度按时间qfe3c7zg1#
是的,这个算法相对简单,注意当考虑一个索引为
i
的项时,它在堆中的“父项”位于索引(i-1)/2
,它的子项位于索引i*2+1
和i*2+2
。1.用
item_to_pop
交换范围内的最后一个项。这会将该项移动到所需的(最后一个)位置,但会在堆的中间插入一个“小”项。这需要修复。1.如果
item_to_pop
位置上的“small”项大于它的当前父项,则与它的父项进行交换。重复操作,直到该项不再大于它的当前父项或者成为新的根。然后我们就完成了。值得注意的是,这与push_heap
的算法相同,只是我们从中间而不是从末尾开始的快捷方式不同。1.如果
item_to_pop
位置的“small”项小于 * 任一个 * 当前子项,则与 larger 子项交换。重复此操作,直到该项大于其任何当前子项(注意到最后可能只有一个孩子或者没有孩子)。然后我们就完成了。值得注意的是,这是与pop_heap
相同的算法,除了我们从中间而不是顶部开始的捷径。这个算法最多可以执行
log2(n)+1
交换和log2(n)*2+1
比较,这使得它几乎和pop_heap
和push_heap
一样快,这并不奇怪,因为它是同一个算法。https://ideone.com/zNW7h7
理论上,可以优化
push_heap
部分中的“or is the new root”检查,但是用于检测这种情况的检查增加了复杂性,似乎不值得这样做。IMO,这很有用,应该成为C++标准库的一部分。
z0qdvdin2#
一般来说,在二进制堆中间更新节点的代价很高,不是因为更新操作代价高,而是因为 * 查找 * 节点是一个O(n)操作。如果你知道节点在堆中的位置,更新它的优先级非常容易。我在https://stackoverflow.com/a/8706363/56778中的答案显示了如何删除节点。更新节点的优先级类似于:不是用堆中的最后一个节点替换该节点,而是根据需要向上或向下筛选该节点。
如果你想快速找到一个节点,那么你必须建立一个索引堆,基本上,你有一个字典条目为每个节点,字典键是节点的ID(或任何你用来识别它的东西),值是该节点在二进制堆中的索引,修改堆代码,使其在节点在堆中移动时更新字典条目。它使堆稍微慢了一点(按常数因子),但使查找任意节点成为O(1)操作。
或者,可以用Pairing Heap、skip list或其他任何使用节点指针的“堆”类型来替换二进制堆,我的经验是,尽管这两种堆的理论性能不如Fibonacci堆,但实际性能要好得多。
有了这两种方法,维护索引就容易多了:当你向堆中添加一个节点时,你只需要添加一个节点引用,当节点从堆中移除时,你只需要移除一个引用。这两种堆类型都很容易构建,性能和二进制堆差不多,尽管它们会使用更多的内存。根据经验,我会说配对堆比跳跃列表更容易构建,但是跳跃列表是一种更常用的数据结构。