配对堆变种

小菜鸟

2020-11-03 21:19:03

Personal

# 一个复杂度较为稳定的配对堆变种 ## 0x00.前置知识: * 堆 * 配对堆(**尤其是配对堆的两趟/多趟合并**) --- ## 0x01.简介 我们都知道配对堆是一种思想、实现十分简单,实际运行效率十分优秀的数据结构。 它支持:(以小根堆为例) * 插入(`insert`)$O(1)$ * 删除最小值(`delete-min`)$O(\log n)$ * 查询最小值(`find-min`)$O(1)$ * 合并(`meld`)$O(1)$ * 减小堆中的关键字(`decrease-key`)$\Omega(\log\log n),\;O(4^{\sqrt{\log\log n}})$ 然而我们也知道,由于配对堆的结构过于随意,很多操作均摊复杂度存在较大争议(特别是`meld`和`decrease-key`) 但是考虑到它思想和实现的简洁性,配对堆具有较大的扩展空间。 于是就有了下面要说的这个变种( --- 这个配对堆变种由Amr Elmasry在2009年提出,拥有$O(\log \log n)$的`meld`和`decrease-key`。 论文地址:https://core.ac.uk/download/pdf/204209612.pdf 本文某种程度上是对这篇论文的翻译... 不过说实话我觉得实现难度可能快顶上rank pairing heap了…… 并且在数据小的地方实际效率跟朴素配对堆只能持平,合并操作居多时甚至慢于朴素配对堆…… 好在思想难度不高,也不需要二项堆作为基础。 --- ## 0x02.问题分析 我们先考虑普通配对堆存在的一些问题。 ### 1.insert过于粗暴 朴素配对堆直接将新插入结点连到根上的方式会导致势能上升严重。 所以我们考虑给插入结点开一个缓冲区,称为`insert-buffer`。 ### 2.decrease-key过于粗暴。 朴素配对堆将整个以被减小结点为根的子树切下来并重新连接。这个做法导致了原本的树形结构被较大地破坏,,,势能也会严重上升,,, 所以我们考虑不切下整棵子树,而是用被切下结点最左边的儿子填补原来的位置。 同时参考插入缓冲区,我们为插入缓冲区以外的所有树开一个缓冲区,称为`pool`。 --- ## 0x03.结构及定义 那么我们先考虑一下这种堆的大致结构: * 一个`insert-buffer`直接存储暂存的结点 * 一个`pool`,存有一组**堆有序**的多叉树 * 一个`minimum-pointer`,用于在这一堆树和结点中维护最值 以下给出我的C++~~伪代码~~ 与我的代码并非完全一致,为了可读性隐藏了很多细节 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> class elmasry_pairing_heap:private _Cmp { public: struct iterator; elmasry_pairing_heap(); iterator push(_Tp); void pop(); const _Tp &top()const; void modify(const iterator&,_Tp); void join(elmasry_pairing_heap&); size_type size()const; bool empty()const; private: struct Node; struct ptr_cmp;//这个东西将会辅助pool的排序 Node *insert_buffer,*tree_pool,*top_node; //利用链表来存储insert-buffer和pool size_type _size; std::set<Node*> tree_pool_mem; //为了判断一个结点是否是根,用set存下pool中的所有结点 //过于丑陋,,,会想办法改进的 inline Node *__get_node(_Tp);//分配结点的函数,内部实现可以忽略 inline void __delete_node(Node*); void __clear();//辅助清空一个堆 void move(elmasry_pairing_heap&);//辅助将一个堆中的所有数据移动到这个堆 void push_to_tree_pool(Node*);//辅助将一个树插入pool Node *link(Node*,Node*);//直接合并两棵树,返回合并后的根结点 Node *two_pass(Node*);//配对堆的两趟合并 Node *multi_pass(Node*);//配对堆的多趟合并 void combine(); }; ``` ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> struct elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::Node { _Tp val; Node *left,*sibling,*child; //left:左边的兄弟(如果有)/父亲(如果没有左边的兄弟) //sibling:右边的兄弟 //child:最左边的儿子 Node(_Tp _val,Node *child_ptr=nullptr): val(_val), left(nullptr), sibling(nullptr), child(child_ptr)//为了方便识别缓冲区内的结点,初始化时会把child指向该结点自身 { } }; ``` ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> struct elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::iterator { private: Node *real_node; friend class elmasry_pairing_heap; iterator(Node* ptr):real_node(ptr) {} public: iterator():real_node(nullptr){} const _Tp &operator*()const//普通的解引用,懒得实现->了( { return real_node->val; } operator void*()const//方便兼容pd_ds的写法,,,实际上比较废( { return (void*)real_node; } }; ``` 为了不影响理解,`push_to_pool`也放上来。配对堆本身的相关操作讲解请参考我的另一篇blog。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> void elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::push_to_tree_pool(Node *ptr) { ptr->left=nullptr; ptr->sibling=tree_pool; if(tree_pool!=nullptr)tree_pool->left=ptr; tree_pool=ptr;//链表基操 tree_pool_mem.insert(ptr); if(top_node==nullptr||_Cmp::operator()(top_node->val,ptr->val))top_node=ptr;//顺手维护 if((1ULL<<tree_pool_mem.size())>_size)combine();//pool过大就combine } ``` --- 显然缓冲区是不能苟到底的。所以我们采用`combine`操作来把所有结点合并成一棵树。 --- ## 0x04.核心操作`combine` 对于`insert-buffer`中单个的结点,通过一次多趟合并获得一棵树。 对于`pool`中的树,由于我们不知道每棵树的大小,不一定能找出最优的合并方案... 然而我们可以对`pool`中的树进行一次排序,由此来得出一个接近最优的合并顺序。 把两边出来的树合并后塞`pool`里就成了( 排序十分缓慢...所以我们给出一个限制:`pool`中的树不能超过$\lceil \log n \rceil$棵($n$是堆中结点个数) 一旦`pool`大小因为其他操作超限,就立即调用一次`combine`。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> void elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::combine() { for(Node *i=insert_buffer;i!=nullptr;i=i->sibling)i->child=nullptr; insert_buffer=multi_pass(insert_buffer); typedef std::vector<Node*,node_ptr_alloc_type> table_type; static table_type root_table;//这里不用管细节。只要知道用vector暂存元素来排序。 root_table.clear(); for(Node *i=tree_pool,*other;i!=nullptr;i=other) { other=i->sibling; i->left=nullptr; i->sibling=nullptr; root_table.push_back(i); } std::sort(root_table.begin(),root_table.end(),ptr_cmp()); tree_pool=nullptr; for(typename table_type::const_iterator iter=root_table.begin();iter!=root_table.end();++iter) { Node *ptr=*iter; tree_pool=link(tree_pool,ptr); } tree_pool=link(tree_pool,insert_buffer); insert_buffer=nullptr; tree_pool_mem.clear();//记得更新pool中的根表 tree_pool_mem.insert(tree_pool); top_node=tree_pool;//由于很多操作最后可能combine,直接在这维护最值可以省点工作量 } ``` --- ## 0x05.具体操作 有了`combine`后就可以轻易写出其他操作了( --- ### insert 因为`insert-buffer`的存在,我们只要直接把新建的结点放入缓冲区就行( 注意同时维护`minimum-pointer`。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> typename elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::iterator elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::push(_Tp Value) { Node *ptr=__get_node(Value); if(insert_buffer!=nullptr)insert_buffer->left=ptr; ptr->sibling=insert_buffer;//链表基操 insert_buffer=ptr; if(top_node==nullptr||_Cmp::operator()(top_node->val,Value))top_node=ptr; ++_size; return iterator(ptr); } ``` --- ### delete-min 分类讨论: * 对于在`pool`里的结点,我们把它的子树两趟合并后重新放回`pool` * 对于在`insert-pool`里的结点,我们直接删除 但是在删除之后并不能轻易找到新的`minimum-pointer`。所以我们调用一次`combine`,此时合并出的树根就存有最值。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> void elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::pop() { if(top_node->child==top_node)//对于insert-buffer中的结点进行处理 { if(top_node->left!=nullptr)top_node->left->sibling=top_node->sibling; if(top_node->sibling!=nullptr)top_node->sibling->left=top_node->left; if(insert_buffer==top_node)insert_buffer=top_node->sibling; //链表基操 } else//对于pool中的结点进行处理 { Node *son=top_node->child; if(son!=nullptr)son->left=nullptr; son=two_pass(son); if(top_node->left!=nullptr)top_node->left->sibling=top_node->sibling; if(top_node->sibling!=nullptr)top_node->sibling->left=top_node->left; if(tree_pool==top_node)tree_pool=top_node->sibling; if(son!=nullptr) { son->sibling=tree_pool; tree_pool=son; } //依然是链表基操 } --_size; Node *dead_node=top_node; combine(); __delete_node(dead_node); } ``` --- ### find-min 略。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> const _Tp& elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::top()const { return top_node->val; } ``` --- ### meld 用类似启发式合并的方法: * 把较小的堆`combine`后直接存入较大的堆的`pool`。 * 如果较大的堆`pool`超出限制,就`combine`一次。 注意同时维护`minimun-pointer`。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> void elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::join(elmasry_pairing_heap &other) { if(other._size==0)return; if(_size==0)move(other);//先对二者有空堆的情况特判 if(_size>=other._size) { other.combine(); _size+=other._size; push_to_tree_pool(other.tree_pool); other.__clear(); } else { combine(); other._size+=_size; other.push_to_tree_pool(tree_pool); __clear(); move(other); }//如上所述 } ``` --- ### decrease-key 如果该结点在`insert-buffer`里或者已经是根就直接搞了。 如前文所述,用被减小结点的第一个儿子代替它所在的位置。该结点连同剩下的儿子被作为一棵树放入`pool`。 * 如果`pool`大小超了,就`combine`一次。 依然要注意维护`minimum-pointer`。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> void elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::modify(const iterator& Iterator,_Tp Value) { Node *ptr=(Node*)Iterator.real_node; if(_Cmp::operator()(top_node->val,Value))top_node=ptr; ptr->val=Value; if(ptr->child==ptr||tree_pool_mem.find(ptr)!=tree_pool_mem.end()) { return; } if(ptr->child!=nullptr)//把儿子切出来替换,链表基操 { Node *replace=ptr->child; ptr->child=ptr->child->sibling; if(ptr->child!=nullptr)ptr->child->left=ptr; if(ptr->left!=nullptr) { if(ptr==ptr->left->child)ptr->left->child=replace; else ptr->left->sibling=replace; } if(ptr->sibling!=nullptr) { ptr->sibling->left=replace; } replace->left=ptr->left; replace->sibling=ptr->sibling; } else//没儿子就直接删,链表基操 { if(ptr->left!=nullptr) { if(ptr==ptr->left->child)ptr->left->child=ptr->sibling; else ptr->left->sibling=ptr->sibling; } if(ptr->sibling!=nullptr) { ptr->sibling->left=ptr->left; } } push_to_tree_pool(ptr); } ``` --- ### size&empty 略。 ```cpp template<typename _Tp,typename _Cmp,typename _Alloc> typename elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::size_type elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::size()const { return _size; } template<typename _Tp,typename _Cmp,typename _Alloc> bool elmasry_pairing_heap<_Tp,_Cmp,_Alloc>::empty()const { return _size==0; } ``` --- ## 0x06.复杂度分析 咕咕咕。 --- ## 0x07.完整的代码实现 直接[广告](https://github.com/OIerTemplate/OIerTemplateLib/blob/master/elmasry_pairing_heap.hpp) 用法参见项目README