Data Structure - Paired Heap

introduce

A paired heap is a data structure that supports operations such as inserting , querying/deleting minimum values , merging , and modifying elements . It is a kind of mergeable heap. It has the advantages of fast speed and simple structure, but due to its amortized complexity based on potential energy analysis, it cannot be persisted.

definition

The paired heap is a weighted multi-fork tree that satisfies the heap property (as shown in the figure below), that is, the weight of each node is less than or equal to all his sons (take the small root heap as an example, the same below).
insert image description here

Usually we use the son-brother representation to store a paired heap (as shown in the figure below), and all the son nodes of a node form a one-way linked list. Each node stores a pointer to its first son, the head node of the linked list; and a pointer to its right sibling.

This method facilitates the implementation of paired heaps and also facilitates complexity analysis.

insert image description here

struct Node {
    
    
  T v;  // T为权值类型
  Node *child, *sibling;
  // child 指向该节点第一个儿子,sibling 指向该节点的下一个兄弟。
  // 若该节点没有儿子/下个兄弟则指针指向 nullptr。
};

It can be found from the definition that, compared with other common heap structures, the paired heap does not maintain any additional information such as tree size, depth, ranking, etc. (the binary heap does not maintain additional information, but it maintains a strict complete binary tree structure to ensure the complexity of the operation), and any tree that satisfies the heap property is a legal paired heap. This simple and highly flexible data structure lays the foundation for the excellent efficiency of the paired heap in practice; for comparison, Fibonacci Deeded heaps are bad constants because they require a lot of extra information to maintain.

The paired heap guarantees its total complexity through a set of carefully designed operation sequences. The original paper 1 called it "a self-adjusting heap (Self Adjusting Heap)". In this respect, it is quite similar to the Splay tree (called "Self Adjusting Binary Tree" in the original paper).

process

query minimum

From the definition of the paired heap, it can be seen that the weight of the root node of the paired heap must be the smallest, and the root node can be returned directly.

merge

The operation of merging two paired heaps is very simple. First, make the smaller one of the two root nodes the new root node, and then insert the larger root node as its son. (See below)

insert image description here
It should be noted that the child list of a node is sorted by insertion time, that is, the rightmost node becomes the son of the parent node first, and the leftmost node becomes the son of the parent node most recently.

accomplish

Node* meld(Node* x, Node* y) {
    
    
  // 若有一个为空则直接返回另一个
  if (x == nullptr) return y;
  if (y == nullptr) return x;
  if (x->v > y->v) std::swap(x, y);  // swap后x为权值小的堆,y为权值大的堆
  // 将y设为x的儿子
  y->sibling = x->child;
  x->child = y;
  return x;  // 新的根节点为 x
}

insert

There are all merges, and when inserting, just treat the new element as a new paired heap and merge it with the original heap.

remove minimum

The first thing to mention is that the above operations are very lazy and do not maintain the data structure at all, so we need to carefully design the operation of deleting the minimum value to ensure that the total complexity is not a problem.

The root node is the minimum value, so it is the root node that needs to be deleted . Consider what will happen after removing the root node: all the original sons of the root node form a forest; and the pairing heap should be a tree, so we need to merge all these sons in a certain order.

A very natural idea is to use meld meldThe me l d function combines the sons one by one from left to right. The correctness of this is obvious, but it will cause the complexity of a single operation to degenerate to O ( n ) O (n)O ( n )

In order to ensure the total amortized complexity, a "two-step" merging method needs to be used:

1. Match the two sons into a pair, and use the meld operation to merge the two sons who are paired together (see Figure 1 below), 2.
Move the newly generated heap from right to left** (that is, the direction from the old son to the new son) are merged together one by one (see Figure 2 below).

insert image description here
insert image description here

First implement an auxiliary function merges , which is used to merge all siblings of a node.

accomplish

Node* merges(Node* x) {
    
    
  if (x == nullptr || x->sibling == nullptr)
    return x;  // 如果该树为空或他没有下一个兄弟,就不需要合并了,return。
  Node* y = x->sibling;                // y 为 x 的下一个兄弟
  Node* c = y->sibling;                // c 是再下一个兄弟
  x->sibling = y->sibling = nullptr;   // 拆散
  return meld(merges(c), meld(x, y));  // 核心部分
}

The last sentence is the core of the function. This sentence is divided into three parts:
1.meld(x,y) "pairs" x and y.
2.merges( c ) recursively merge c and his brothers.
3. Merge the two new trees generated by the above two operations.

It should be noted that the direction of merging in the second step mentioned above is required (merging from right to left). The implementation of this recursive function has guaranteed this order. If the reader needs to implement the iterative version by himself, please be sure Take care to guarantee this order, otherwise the complexity will lose its guarantee.

With the merges function, the delete-min operation is obvious.

Node* delete_min(Node* x) {
    
    
  Node* t = merges(x->child);
  delete x;  // 如果需要内存回收
  return t;
}

decrease the value of an element

To achieve this operation, you need to add a "parent" pointer to the node. When the node has a left brother, it points to the left brother instead of the actual parent node; otherwise, it points to its parent node.

First, the definition of the node is changed to:

struct Node {
    
    
  LL v;
  int id;
  Node *child, *sibling;
  Node *father;  // 新增:父指针,若该节点为根节点则指向空节点 nullptr
};

The meld operation is modified to:

Node* meld(Node* x, Node* y) {
    
    
  if (x == nullptr) return y;
  if (y == nullptr) return x;
  if (x->v > y->v) std::swap(x, y);
  if (x->child != nullptr) {
    
      // 新增:维护父指针
    x->child->father = y;
  }
  y->sibling = x->child;
  y->father = x;  // 新增:维护父指针
  x->child = y;
  return x;
}

The merges operation is modified to:

Node *merges(Node *x) {
    
    
  if (x == nullptr) return nullptr;
  x->father = nullptr;  // 新增:维护父指针
  if (x->sibling == nullptr) return x;
  Node *y = x->sibling, *c = y->sibling;
  y->father = nullptr;  // 新增:维护父指针
  x->sibling = y->sibling = nullptr;
  return meld(merges(c), meld(x, y));
}

Now let's consider how to implement the decrease-key operation.
First of all, we found that when we reduce the weight of node x , the subtree rooted at x still satisfies the paired heap property, but the heap property may no longer be satisfied between the parent of x and x .
Therefore, we cut out the entire subtree rooted at x , and now both trees conform to the paired heap property, and then merge them to complete all operations.

// root为堆的根,x为要操作的节点,v为新的权值,调用时需保证 v <= x->v
// 返回值为新的根节点
Node *decrease_key(Node *root, Node *x, LL v) {
    
    
  x->v = v;                 // 更新权值
  if (x == root) return x;  // 如果 x 为根,则直接返回
  // 把x从fa的子节点中剖出去,这里要分x的位置讨论一下。
  if (x->father->child == x) {
    
    
    x->father->child = x->sibling;
  } else {
    
    
    x->father->sibling = x->sibling;
  }
  if (x->sibling != nullptr) {
    
    
    x->sibling->father = x->father;
  }
  x->sibling = nullptr;
  x->father = nullptr;
  return meld(root, x);  // 重新合并 x 和根节点
}

Complexity Analysis

The paired heap structure and implementation are simple, but the time complexity analysis is not easy.

The original paper 1 only analyzes the complexity to the point that both meld and delete-min operations are amortized O ( log ⁡ n ) O(\log n)O(logn ) , but a conjecture is made that each operation has the same complexity as the Fibonacci heap.

Unfortunately, it was later found that the paired heap that does not maintain additional information, under a specific sequence of operations, the lower bound of the amortized complexity of the decrease-key operation is at least Ω ( log ⁡ log ⁡ n ) 2 \Omega (\log \log n )2Oh ( lo glogn)2

At present, a good estimate of the upper bound of the complexity is Iacono's O ( 1 ) O(1)O(1) meld,$O(\log n) $decrease;Pettie 的 O ( 2 2 log ⁡ log ⁡ n ) O(2^{2 \sqrt{\log \log n}}) O(22loglogn ) meld and decrease. It should be noted that the aforementioned complexity is an amortized complexity, so the minimum value cannot be taken for each result.

Guess you like

Origin blog.csdn.net/m0_61360607/article/details/132294997