ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

算法训练:堆 & 可并堆

2021-06-08 20:07:31  阅读:188  来源: 互联网

标签:npl lc 训练 int LeftHeapNode 算法 val rc


第一题:

在这里插入图片描述

算法分析:

这题一开始我愚蠢了……WA了一发……
题目说,他有一根完整的,长度是所需N块模板的长度之和的长木板,然后要你去切割,每次切割会消耗总长的长度那么多的能量,问你怎么搞耗能最少??这题一看很明显,每次切割耗能都是被切割的长度那么多,所以,当然是长痛不如短痛,一次性多切一点为好,难道这题是排序?我们不妨来模拟这个过程:

第一次切割:当然切最大的,也就是8
L = 21
耗能ans = 21
切完后:
L = 13

第二次切割:当然还是切当然最大的,也还是8
L = 13
耗能ans = 21 + 13 = 34
L = 5(此时已经完工,无需再切)

分析这个过程,貌似确实很容易被误解成排序+贪心……

那么这个排序+贪心错在了哪里呢?

从原始模板逐渐去切割,其实不是对的!因为一次不一定只可以切一块模板,只要是你手里有的模板都可以同时去切割,消耗的能量是它们这些模板的总和!!

那么正解是什么呢?

既然我们一次可能不止切一块木板,也就是说,可能有很多分支并线进行!于是我们应该从切割的结果开始向上溯源,也就是和合并果子那题差不多了!每次都是把一个长木板切成两个小木板,反过来看就是把两个子节点合并,之和便是它们的父节点!我们的目的是让每个合并后的父节点之和最小,那么就是要形成小顶堆,小顶堆代码如下不解释:

小顶堆的解决方案:

#include <cstdio>
#include <queue>
#include <vector>
using namespace std;

int main(){
	priority_queue<int, vector<int>, greater<int> > q;
	int n, a;
	long long ans = 0;
	scanf("%d", &n);
	while(n--){
		scanf("%d", &a);
		q.push(a);
	}
	while((int)q.size() > 1){
		int u = q.top(); q.pop();
		int v = q.top(); q.pop();
		ans += (u + v);
		q.push(u + v);
	}
	printf("%d", ans);
	return 0;
}
第二题:

在这里插入图片描述
在这里插入图片描述

算法分析:

这题一看就是要动态维护中位数!我们可以看到,这个输出的要求是前1 3 5 7……个数的中位数,这个要求说明,中位数也是输入的一个数,不会出现那种,前偶数个数的中位数,需要求平均值的情况!

所谓中位数,我们可以把输入的n个数分成两堆,一堆存放较大数,一堆存放较小数,由于是前奇数个的中位数,于是这两个堆应该是一个堆比另一个堆的规模大1(且只能大1!!)那么这个中位数如何得到呢?中位数一定是:

较大数所在堆的最小数
较小数所在堆的最大数

于是这个较大数,就应该放在一个小根堆的数据结构、较小数,就应该放在一个大根堆的数据结构!
然后每次输入的时候,首先判断是否比小根堆的最小数大,如果大则放入小根堆,否则放入大根堆。并且我们还要维护堆的规模的性质!如果出现了某个堆的规模比另一个堆的规模大的超过1,就把这个堆的堆顶取出来放入另一个堆,因为这样放才不会影响我们大、小数所在堆的性质!

堆动态维护中位数,我的解决方案:

#include <iostream>
#include <queue>
using namespace std;

int main(){
	priority_queue<int> q1;
	priority_queue<int, vector<int>, greater<int> > q2;
	int n, a;
	cin >> n;
	for(int i = 1;i <= n;++i){
		cin >> a;
		if(q2.empty() || a > q2.top())
			q2.push(a);
		else
			q1.push(a);
		if((int)q1.size() - (int)q2.size() > 1){
			int u = q1.top();
			q1.pop();
			q2.push(u);
		}
		if((int)q2.size() - (int)q1.size() > 1){
			int u = q2.top();
			q2.pop();
			q1.push(u);
		}
		if(i & 1){
			int res = (int)q1.size() > (int)q2.size() ? q1.top() : q2.top();
			cout << res << endl;
		}
	}
	return 0;
}
第三题:

在这里插入图片描述
在这里插入图片描述

算法分析:

还是再仔细读读这个操作:
在这里插入图片描述

首先要注意:
合并堆是合并第x个数所在的堆第y个数所在的堆 ,所以我们需要查堆顶,怎么查?
然后要注意:
还需要判断每个数是否已经被删除,以及是否在一个堆中,怎么处理?

其实这些问题还是蛮容易的,判断是否在一个堆中以及查堆顶,都很容易想到用并查集的查询方式,路径压缩找根!然后判断每个数是否被删除,那就给每个节点设置一个bool类型的visit变量即可!

然后剩下的,就是普通的可并堆 的算法问题!什么是可并堆?我只会一种可并堆,那就是左式堆

讲解左式堆:

两个堆合并,假设一个堆的规模是n,另一个堆的规模是m,如果采用朴素的插入→自顶向下的下滤,那么需要的开销是:O(n * log(n)+ m * log(n))这个复杂度来由我就不解释了,不懂可私信我,如果采用Floyd思想,采用朴素的插入→自底向上的下滤,需要的开销是:O(m + n),这个是因为自底向上,复杂度取决于高度之和,一颗完全二叉树(左式堆的基本结构)高度为1的节点,也就是叶子节点占n/2,然后逐层往上,高度最高的节点便是根节点,只存在一个,也就是说,高度高的节点是很少的,于是Floyd思想的下滤就开销很少了(其实这个思想说白了就是一种小堆叠成大堆的思想)。然后左式堆的思想就更牛逼了,单独搞一段来讲解:
左式堆有一个参数:npl(null path length)也就是空节点路径长度,我们设空节点的长度为0,其他的节点x的npl有公式:npl(x) = 1 + Min( npl(x.Lchild) , npl(x.Rchild) ),然后我们的左式堆就要维护左偏性+堆序性,所谓左偏就是始终要左子树的npl大于右子树!这样的化,根节点向下下滤,最长的路径便是右侧链,这个右侧链的高度,是根节点的最大满二叉子树的高度,便是log(n),于是左式堆的插入开销就是:O(log(n))就比前面的算法都要优秀很多很多!!!

左式堆的解决方案:

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 1e5 + 5;
struct LeftHeapNode
{
	LeftHeapNode* rc, * lc, * rt;
	int val, npl, vis, id;
};
LeftHeapNode* Node[maxn];
int n, m, x, y;
LeftHeapNode* find(LeftHeapNode* p)
{
	while (p->rt)
		p = p->rt;
	return p;
}
LeftHeapNode* mergeTop(LeftHeapNode* x, LeftHeapNode* y)
{
	if (!x)	return y;
	if (!y) return x;
	if (x->val > y->val || (x->val == y->val && x->id > y->id))
		swap(x, y);
	x->rc = mergeTop(x->rc, y);
	x->rc->rt = x;
	if (!x->lc || x->lc->npl < x->rc->npl)
		swap(x->lc, x->rc);
	if (!x->rc)
		x->npl = 0;
	else
		x->npl = x->rc->npl + 1;
	return x;
}
int main()
{
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n; ++i)
	{
		int val;
		scanf("%d", &val);
		Node[i] = new LeftHeapNode;
		Node[i]->val = val;
		Node[i]->lc = Node[i]->rc = Node[i]->rt = NULL;
		Node[i]->id = i;
		Node[i]->npl = 0;
		Node[i]->vis = 0;
	}
	while (m--)
	{
		int op;
		scanf("%d", &op);
		if (1 == op)
		{
			int x, y;
			scanf("%d %d", &x, &y);
			if (Node[x]->vis || Node[y]->vis)
				continue;
			LeftHeapNode* fx = find(Node[x]);
			LeftHeapNode* fy = find(Node[y]);
			if (fx != fy)
				mergeTop(fx, fy);
		}
		else
		{
			int x;
			scanf("%d", &x);
			if (Node[x]->vis)
			{
				printf("-1\n");
				continue;
			}
			LeftHeapNode* fx = find(Node[x]);
			printf("%d\n", fx->val);
			fx->vis = 1;
			LeftHeapNode* L = fx->lc;
			LeftHeapNode* R = fx->rc;
			if (L)	L->rt = NULL;
			if (R)	R->rt = NULL;
			fx->lc = fx->rc = NULL;
			mergeTop(L, R);
			delete fx;
		}
	}
	return 0;
}

详解左式堆操作:

首先是最重要的合并

左式堆的合并发生在右侧链,以每次处理都维护堆序性和左偏性闻名!而可能会破坏左偏性和堆序性的地方就出现在:合并、删除(其实删除也是合并,稍后讲!)
两堆合并,其实是个递归的过程,因为右侧链的子树也有右侧链,要递归到右侧链只有一个节点的时候,才真正发生链接操作!

LeftHeapNode* mergeTop(LeftHeapNode* x, LeftHeapNode* y)
{
	if (!x)	return y;
	if (!y) return x;
	if (x->val > y->val || (x->val == y->val && x->id > y->id))
		swap(x, y);
	x->rc = mergeTop(x->rc, y);
	x->rc->rt = x;
	if (!x->lc || x->lc->npl < x->rc->npl)
		swap(x->lc, x->rc);
	if (!x->rc)
		x->npl = 0;
	else
		x->npl = x->rc->npl + 1;
	return x;
}

这是合并的代码,递归基就不说了,
第一步先判断堆序性是否会被破坏,如果可能会被破坏,就交换节点,保证堆序性不破坏

if (x->val > y->val || (x->val == y->val && x->id > y->id))
	swap(x, y);

然后是关键的递归:

x->rc = mergeTop(x->rc, y);

每次都去找右侧链,把以 y 为顶的堆放到右侧链上去,递归完成之后会向上跑到根节点的右子节点处,然后要重新把右子节点链回父节点,这不是递归能做到的,需要手动完成:

x->rc->rt = x;

现在合并工作完成了,但是还需要善后,因为左偏性可能被破坏,于是判断左偏性,如果破坏了就需要交换左右子树,去维护左偏性:

if (!x->lc || x->lc->npl < x->rc->npl)
	swap(x->lc, x->rc);

左偏性完成了之和,就需要更新根节点的npl值,因为可能有新的右子树,右子树的npl值变了则根节点的npl值也要跟着变!这时候需要判断,如果右子树为空,就不能去访问人家的npl,否则空指针访问会抛出异常的!

if (!x->rc)
	x->npl = 0;
else
	x->npl = x->rc->npl + 1;

这里和《c++数据结构 邓俊辉版》的说法有点区别,这里我们规定叶子节点的(或者在左偏性下,右子树为空的)的节点,npl = 0,其他的很显然,不用多说。
这样就完成了合并操作!

然后讲讲删除操作:

LeftHeapNode* pop(LeftHeapNode* x)
{
	LeftHeapNode* L = x->lc;
	LeftHeapNode* R = x->rc;
	if (L)	L->rt = NULL;
	if (R)	R->rt = NULL;
	x->lc = x->rc = NULL;
	return merge(L, R);
/*	错误的解决方案
	if (x->lc)	x->lc->rt = NULL;
	if (x->rc)	x->rc->rt = NULL;
	return merge(x->lc, x->rc);// 会导致空指针访问!
*/
}

其实删除也是合并,这里我就只讲一下删除的思想,删除其实就是把被删除节点的左右子树合并的操作。但是重点是要妥善处理好善后的工作!!!千万不能造成空指针访问!!!

第四题:

在这里插入图片描述
在这里插入图片描述

算法分析:

这题和上题没啥大区别啊!一开始,各自为政,然后俩猴子打架,牛逼的做老大。这不就是大顶堆吗?然后俩猴群(俩堆)对垒,就会派出堆的最屌的猴子(也就是堆顶)去干架,然后干完俩猴子就亲密无间,成为一个堆的猴子了(不打不相识?)但是值得注意的是:
猴子都有一个强值,经过决斗后,强值会减少到原来的一半(即10减为5,5减为2)。
这也就意味着,堆序性可能需要重新维护!这就是和上题不一样的地方!这个如何处理呢??
每次干架前,我都可以把需要干起来的猴王(堆顶),先减半,减半再放入原堆,然后俩堆再合并 ,就解决这个问题了!!

此题的解决方案:

#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 1e5 + 5;
struct LeftHeapNode
{
	LeftHeapNode* lc, * rc, * rt;
	int val, npl;
	~LeftHeapNode() { delete lc; delete rc; delete rt; }
};
LeftHeapNode* Node[maxn];
LeftHeapNode* find(LeftHeapNode* x)
{
	while (x->rt)
		x = x->rt;
	return x;
}
LeftHeapNode* merge(LeftHeapNode* x, LeftHeapNode* y)
{
	if (!x)	return y;
	if (!y)	return x;
	if (x->val < y->val)
		swap(x, y);
	x->rc = merge(x->rc, y);
	x->rc->rt = x;
	if (!x->lc || x->lc->npl < x->rc->npl)
		swap(x->lc, x->rc);
	if (!x->rc)
		x->npl = 0;
	else
		x->npl = 1 + x->rc->npl;
	return x;
}
LeftHeapNode* pop(LeftHeapNode* x)
{
	LeftHeapNode* L = x->lc;
	LeftHeapNode* R = x->rc;
	if (L)	L->rt = NULL;
	if (R)	R->rt = NULL;
	x->lc = x->rc = NULL;
	return merge(L, R);
/*	错误的解决方案
	if (x->lc)	x->lc->rt = NULL;
	if (x->rc)	x->rc->rt = NULL;
	return merge(x->lc, x->rc);// 会导致空指针访问!
*/
}
int main()
{
	int n, m;
	while (~scanf("%d", &n))
	{
		for (int i = 1; i <= n; ++i)
		{
			int val;
			scanf("%d", &val);
			Node[i] = new LeftHeapNode;
			Node[i]->lc = Node[i]->rc = Node[i]->rt = NULL;
			Node[i]->val = val;
			Node[i]->npl = 0;
		}
		scanf("%d", &m);
		while (m--)
		{
			int x, y;
			scanf("%d %d", &x, &y);
			LeftHeapNode* fx = find(Node[x]);
			LeftHeapNode* fy = find(Node[y]);
			if (fx == fy)
				printf("-1\n");
			else
			{
				fx->val >>= 1;
				fy->val >>= 1;
				LeftHeapNode* newL = pop(fx);
				LeftHeapNode* newR = pop(fy);
				newL = merge(fx, newL);
				newR = merge(fy, newR);
				newL = merge(newL, newR);
				printf("%d\n", newL->val);
			}
		}
	}
	return 0;
}

标签:npl,lc,训练,int,LeftHeapNode,算法,val,rc
来源: https://blog.51cto.com/u_15262702/2883316

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有