Fork me on GitHub

堆堆的世界

可并堆

可并堆有三种常见方法:斜堆,左偏树,随机堆我们分别聊聊这三种堆吧!

斜堆

斜堆的构建其实是和二叉堆差不多的,它只是用链表来维护关系罢了,这是构建代码:

struct Heap
{
    Heap *lson, *rson;
    int val;
    Heap(int n = 0)
    {
        val = n;
        lson = rson = 0;
    }
};

然后,可并堆可并堆,最重要的是合并吧,合并怎么做呢,就是假设有两个斜堆,将其权值比较大的斜堆插在权值比较小的斜堆的右子树之下,但是如果只这样的话,时间复杂度就会退化到 \(O(n)\) 的,我们只要将左子树和右子树交换一下 这样子的左右子树就比较均匀啦!,时间复杂度就讲到 \(O(log_n)\) 啦,代码如下:

Heap* merge(Heap*& a, Heap*& b)
{
    if(a == 0) return a;
    if(b == 0) return b;
    if(a -> val > b -> val) swap(a, b);
    a -> rson = merge(a -> rson, b);
    swap(a -> lson, a -> rson);
    return a;
}

剩下的就和二叉堆差不多了,代码如下:

void Heap_insert(Heap*& a, int val)//插入
{
    Heap* b = new Heap(val);
    a = merge(a, b);
}

int Top(Heap*& a)//求最小
{
    int val = a -> val;
    return val;
}

void Del(Heap*& a)//删最小
{
    a = merge(a -> lson, a -> rson);
}

红黑树和随机堆就没有那么简单了。

你会发现,虽然你将左子树和右子树调换了从而减少了常数,但是你没法保证这调换完之后一定左子树比右子树来的重呀,所以还是时间复杂度很大滴,但是欸,我们可以存一个值,表示该子树的大小呀,这不就好了嘛。。所以,左偏树诞生了!

左偏树

基本结构与斜堆一样,只是左偏树的每个结点多了一个距离值NPL即该结点一直向右儿子走,到达空结点的距离。
构建还是和斜堆很像,代码如下:

struct Heap
{
    Heap* lson;
    Heap* rson;
    int VAL, npl;
    Heap(int n = 0)
    {
        val = n;
        npl = 1;
        lson = 0;
        rson = 0;
    }
};

接下来依然是合并,维护了 \(npl\) 之后,我们就不用再乱交换了,而且保证了左儿子大于右儿子的\(npl\) ,效率就会提升。

那么怎么维护 \(npl\) 呢?我们来看看新的\(Merge\)

Heap* Merge(Heap*& a, Heap*& b)
{
	if(a == 0) return b;
	if(b == 0) return a;
	if(a -> val > b -> val) swap(a, b);
	a -> rson = Merge(a -> rson, b);
	if(!a -> lson || a -> lson -> npl < a -> rson -> npl) swap(a -> lson, a -> rson);
	if(!a -> rson) a -> npl=0;
	else a -> npl = a -> rson -> npl + 1;
	return a;
}

下面是注解
首先,如果左右子树为空,直接返回另一个
第二,如果左子树的 \(npl\) 比较小,交换后使右子树的高度变小,再合并。
合并后维护 \(npl\),如果右子树为空,\(npl\)\(0\),否则为右子树的 \(npl++\)

剩下的就都差不多了,代码如下:

void Heap_insert(Heap*& root,int k)
{
	Heap* n = new Heap(k);
	root = Merge(root,n);
}
int Top(Heap*& root)
{
	return root -> val;
}
void del(Heap*& root)
{
	root = Merge(root -> lson, root -> rson);
}

随机堆

其实我们有另一种方法,欸,就是随机,我们可以通过随机数的奇偶来决定这次要不要交换,这样子的话就能比一直交换的常数来的小,剩下的重要点前面都已经讲过啦!所以就只放全部代码了:

struct Heap
{
	Heap* lson;
	Heap* rson;
	int val;
	Heap(int n = 0)
	{
		val = n;
		lson = 0;
		rson = 0;
	}
}*root;

Heap* Merge(Heap*& a,Heap*& b)
{
	if(a == 0) return b;
	if(b == 0) return a;
	if(a -> val > b -> val) swap(a,b);
	if(rand() & 1) a -> rson = Merge(a -> rson, b);
	else a -> lson = Merge(a -> lson, b);
	return a;
}
void Heap_insert(Heap*& root,int k)
{
	Heap* n = new Heap(k);
	root = Merge(root, n);
}

int Top(Heap*& root)
{
	return root -> val;
}

void del(RandHeap*& root)
{
	root = Merge(root -> lson, root -> rson);
}

我这里用的是 \(rand()\) 常数超级大,大家可以换成其他的随机数生成方式。

小结

这三种可并堆的优缺点各不相同,虽然普遍都是用左偏树,但是其他的也要学呀,要不然他为什么要存在呢?


例题

# P11833 [省选联考 2025] 推箱子

呃,就直接放个用可并堆打的的文章吧,在这

还有关于题单,找了好久,终于在半个月后找到了。。。。

posted @ 2025-06-04 15:12  tony0530  阅读(16)  评论(0)    收藏  举报