c++ 有没有一种高效、省时的方法来维护一个在中间删除元素的堆?

0g0grzrc  于 2023-02-17  发布在  其他
关注(0)|答案(2)|浏览(124)

我正在做一个路径规划程序,其中我有一个优先级队列**'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();
}
qfe3c7zg

qfe3c7zg1#

是的,这个算法相对简单,注意当考虑一个索引为i的项时,它在堆中的“父项”位于索引(i-1)/2,它的子项位于索引i*2+1i*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_heappush_heap一样快,这并不奇怪,因为它是同一个算法。

template< class RandomIt, class Compare >
constexpr void pop_mid_heap(RandomIt first, RandomIt last, RandomIt item_to_pop, Compare comp) {
    assert(std::is_heap(first, last)); //this is compiled out of release builds
    assert(first<=item_to_pop);
    assert(item_to_pop<last);
    using std::swap;
    std::size_t new_size = last - first - 1;
    if (new_size == 0) return;
    //swap the end of the range and item_to_pop, so that item_to_pop is at the end
    swap(*item_to_pop, *--last);
    if (new_size == 1) return;
    //If item_to_pop is bigger than it's parent, then swap with the parent
    bool moved_up = false;
    RandomIt swap_itr;
    while (true) {
        std::size_t offset = item_to_pop - first;
        if (offset == 0) break; //item_to_pop is at root: exit loop 
        swap_itr = first + (offset-1) / 2;
        if (comp(*item_to_pop, *swap_itr)) 
            break; //item_to_pop smaller than it's parent: exit loop
        swap(*item_to_pop, *swap_itr); //swap with parent and repeat
        item_to_pop = swap_itr;
        moved_up = true;        
    }
    if (moved_up) return; //if we moved the item up, then heap is complete: exit
    //If biggest child is bigger than item_to_pop, then swap with that child
    while (true) {
        std::size_t offset = item_to_pop - first;
        std::size_t swap_idx = offset * 2 + 1;
        if (swap_idx >= new_size) break; //no children: exit loop
        swap_itr = first + swap_idx;
        if (swap_idx+1 < new_size && comp(*swap_itr, *(swap_itr+1))) //if right child exists and is bigger, swap that instead
            ++swap_itr;
        if (!comp(item_to_pop, swap_itr)) break; //item_to_pop bigger than biggest child: exit loop
        swap(*item_to_pop, *swap_itr); //swap with bigger child and repeat
        item_to_pop = swap_itr;
    }
}
template< class RandomIt >
constexpr void pop_mid_heap(RandomIt first, RandomIt last, RandomIt item_to_pop) {
    pop_mid_heap(first, last, item_to_pop, std::less<>{});
}

https://ideone.com/zNW7h7
理论上,可以优化push_heap部分中的“or is the new root”检查,但是用于检测这种情况的检查增加了复杂性,似乎不值得这样做。
IMO,这很有用,应该成为C++标准库的一部分。

z0qdvdin

z0qdvdin2#

一般来说,在二进制堆中间更新节点的代价很高,不是因为更新操作代价高,而是因为 * 查找 * 节点是一个O(n)操作。如果你知道节点在堆中的位置,更新它的优先级非常容易。我在https://stackoverflow.com/a/8706363/56778中的答案显示了如何删除节点。更新节点的优先级类似于:不是用堆中的最后一个节点替换该节点,而是根据需要向上或向下筛选该节点。
如果你想快速找到一个节点,那么你必须建立一个索引堆,基本上,你有一个字典条目为每个节点,字典键是节点的ID(或任何你用来识别它的东西),值是该节点在二进制堆中的索引,修改堆代码,使其在节点在堆中移动时更新字典条目。它使堆稍微慢了一点(按常数因子),但使查找任意节点成为O(1)操作。
或者,可以用Pairing Heapskip list或其他任何使用节点指针的“堆”类型来替换二进制堆,我的经验是,尽管这两种堆的理论性能不如Fibonacci堆,但实际性能要好得多。
有了这两种方法,维护索引就容易多了:当你向堆中添加一个节点时,你只需要添加一个节点引用,当节点从堆中移除时,你只需要移除一个引用。这两种堆类型都很容易构建,性能和二进制堆差不多,尽管它们会使用更多的内存。根据经验,我会说配对堆比跳跃列表更容易构建,但是跳跃列表是一种更常用的数据结构。

相关问题