跳转至

lec5|Binomial Queue

约 774 个字 49 行代码 8 张图片 预计阅读时间 3 分钟

Binomial Queue

相较于常规的优先队列,二项队列的优点

  • 高效的合并操作:

    • 二项队列:两个二项队列可以在 \(O(log n)\) 时间内合并,其中 \(n\) 是两个队列中元素的总数。这是因为二项队列由一系列二项树组成,而二项树的合并操作类似于二进制数的加法

    • 常规优先队列(如二叉堆):合并两个二叉堆通常需要 \(O(n)\) 时间,因为需要将两个堆的元素重新排列成一个合法的堆。

  • 一个二项队列并非一个堆排列规则的树,而是一系列堆排列规则的树的集合,里面的每棵树都是二项树(Binomial Tree)

Binomial Tree

  • 二项树满足堆的性质,即父节点的value小于(大于)子节点的value

  • 然后\(k\)阶二项树是同构的,是由两个\(k-1\)阶二项树合并而得

alt text

  • 关注每一层的数量总数量,我们能够发现,这个二项树其实是一个二项式的具体实例;

  • 比如说:B3,其实就是一个\((1+x)^3\),分别得到每层为\(C_3^0,C_3^1,C_3^2,C_3^3\)

  • \(B_k\)的第\(d\)层一共有\(C_k^d\)个节点

  • 同时,这无形间还证明了一个定理:\(C_k^d=C_{k-1}^d + C_{k-1}^{d-1}\)


  • 于是,对于二项队列的理解,有个伟大的思想就是,看作是二进制

  • 那么一个二项队列中,\(k\)阶二项树一定是完整的,而且数量为0个或1个merge操作也就是二进制的加法

alt text


合并

  • 合并两个二项队列,其实就是二进制数的相加,而且是一个全加器

  • 然后当有"carry-in"时,合并是会有多种情况的,一共\(2^3=8\)种可能

graph LR;
A(["T1.B[k]"])
B(["T2.B[k]"])
C(["T.carry[k-1]"])
D(["T.result[k]"])
E(["T.carry[k]"])
ADD["Adder"]
A ---> ADD
B ---> ADD
C ---> ADD
ADD ---> D
ADD ---> E

Example

alt text

\(1011 + 1001 = 10100\)

最后结果一定是\(B_4\)\(B_2\)

我们从LSB开始,依次往前加

alt text

alt text

alt text


Find_Min

  • 二项队列的队首,也就是整个队列的最小值(最大值),就是这若干个 \(O(logN)\) 个二项树的根中最小(最大)的那个。所以其时间复杂度为 \(O(logN)\)

  • 不过有时候我们也会额外维护一个指针,指向当前最小的那个根,此时其复杂度为\(O(1)\)

Delete_Min

分为四步

  1. \(B_k\)中找到最小的root;FindMin in \(B_k\) -> \(O(logN)\)

  2. 暂时把\(B_k\)树从二项队列中拿走; \(O(1)\)

  3. 将root从\(B_k\)中拿走,这棵二项树就分裂成若干个小的二项树,得到一个新的二项队列;\(O(logN)\)

  4. 最后merge这两个二项队列;\(O(logN)\)

demo

alt text

Implementation

Definition
typedef struct BinNode *Position;
typedef struct Coleection *BinQueue;
typedef struct BinNode *BinTree;

struct BinNode{
    ElementType Element;
    Position LeftChild;
    Position NextSibling; //二项树不是一个二叉树,所以是Leftchild + Sibling 结构
};

struct Collection{
    int CurrentSize; // 节点总个数
    BinTree TheTrees[MaxTrees]; //二项树的集合 
};
CombineTrees
BinTree CombineTrees(BinTree T1,BinTree T2){
    if(T1->Element > T2->Element){
        return CombineTrees(T2,T1);
    }

    // 把T2挪成T1的第一个孩子,这样就不用遍历第二层的NextSibling,简化了操作
    T2->NextSibling = T1->LeftChild;
    T1->LeftChild = T2;
    return T1;
}

alt text

  • 不用遍历NextSibling,于是复杂度来到了\(O(1)\)
  • 在 C 语言中,4*!!Carry + 2*!!T2 + !!T1 是一种常见的技巧,用于将多个布尔值组合成一个唯一的整数值。具体来说,这段代码将三个布尔值 CarryT2T1 组合成一个 0 到 7 之间的整数。
Merge
BinQueue Merge(BinQueue H1, BinQueueH2){
    BinTree T1,T2,Carry=NULL;
    int i,j;

    if(H1->CurrentSize + H2->CurrentSize > Capacity) ErrorMessage() //超出最大上限了

    H1->CurrentSize += H2->CurrentSize;
    for (i=0,j=1; j <= H1->CurrentSize; i++, j*=2){ //遍历每个bit,j是遍历过的总节点数;
        T1 = H1->TheTrees[i]; T2 = H2->TheTrees[i]; //从两边取出 B_i 树
        switch(4*!!Carry + 2*!!T2 + !!T1){ //相当于一个真值表
            case 0:/*000*/ 
            case 1:/*001*/ break;
            case 2:/*010*/ H1->TheTrees[i] = T2; H2->TheTrees[i]=NULL;break //最后我们return的是H1,所以把结果都挪到H1上
            case 3:/*011*/ Carry=CombineTrees(T1,T2);H1->TheTrees[i]=H2->TheTrees[i]=NULL;break;
            case 4:/*100*/ H1->TheTrees[i]=Carry;Carry=NULL;break;
            case 5:/*101*/ Carry=CombineTrees(Carry,T1);H1->TheTrees[i]=NULL;break;
            case 6:/*110*/ Carry=CombineTrees(Carry,T2);H2->TheTrees[i]=NULL;break;
            case 7:/*111*/ H1->TheTrees[i]=Carry;Carry=CombineTrees(T1,T2);H2->TheTrees[i]=NULL;break;
        }
    }
    return H1;
}