《算法笔记》学习日记——9.7 堆

目录

  • 9.7 堆
    • 问题 A: 算法10-10,10-11:堆排序
    • 问题 B: 序列合并
    • 问题 C: 合并果子(堆)
    • 小结

9.7 堆

Codeup Contest ID:100000616

问题 A: 算法10-10,10-11:堆排序

题目描述
堆排序是一种利用堆结构进行排序的方法,它只需要一个记录大小的辅助空间,每个待排序的记录仅需要占用一个存储空间。
首先建立小根堆或大根堆,然后通过利用堆的性质即堆顶的元素是最小或最大值,从而依次得出每一个元素的位置。
堆排序的算法可以描述如下:
《算法笔记》学习日记——9.7 堆_第1张图片
在本题中,读入一串整数,将其使用以上描述的堆排序的方法从小到大排序,并输出。
输入
输入的第一行包含1个正整数n,表示共有n个整数需要参与排序。其中n不超过100000。
第二行包含n个用空格隔开的正整数,表示n个需要排序的整数。
输出
只有1行,包含n个整数,表示从小到大排序完毕的所有整数。
请在每个整数后输出一个空格,并请注意行尾输出换行。
样例输入

10
2 8 4 6 1 10 7 3 5 9

样例输出

1 2 3 4 5 6 7 8 9 10 

思路
这题我直接用了头文件里的make_heap()(建堆)和sort_heap()(对堆排序),需要注意的是,make_heap()默认建立的是大顶堆,而且使用sort_heap()之后会失去堆的性质。

The range loses its properties as a heap. ——《C++ Reference》

代码

#include
#include
#include
#include
#include
#include
using namespace std;
vector<int> Heap;
int main(){
	int n;
	while(scanf("%d", &n) != EOF){
		for(int i=1;i<=n;i++){
			int tmp;
			scanf("%d", &tmp);
			Heap.push_back(tmp);
		}
		make_heap(Heap.begin(), Heap.end());
		sort_heap(Heap.begin(), Heap.end());
		for(int i=0;i<Heap.size();i++) printf("%d ", Heap[i]);
		printf("\n");
		Heap.clear();
	}
	return 0;
}

问题 B: 序列合并

题目描述
有两个长度都为N的序列A和B,在A和B中各取一个数相加可以得到N2个和,求这N2个和中最小的N个。
输入
第一行一个正整数N(1 <= N <= 100000)。
第二行N个整数Ai,满足Ai <= Ai+1且Ai <= 109
第三行N个整数Bi,满足Bi <= Bi+1且Bi <= 109
输出
输出仅有一行,包含N个整数,从小到大输出这N个最小的和,相邻数字之间用空格隔开。
样例输入

3
2 6 6
1 4 8

样例输出

3 6 7

提示
建议用最小堆实现。
思路
这题感觉判题机怪怪的,要求的时间限制是1s,也就是1000ms,然而2000ms多也能AC,最奇怪的是,在判断语句里我这么写就会超时(5000多ms):

int sum = seqA[i]+seqB[j];
if(sum<q.top()){//如果比队首元素更小
    q.pop(); //弹出原先的队首元素
    q.push(sum);//放入优先队列里 
}

而这么写就AC了:

if(seqA[i]+seqB[j]<q.top()){//如果比队首元素更小
    q.pop(); //弹出原先的队首元素
    q.push(seqA[i]+seqB[j]);//放入优先队列里 
}

网上搜了一下,应该是局部变量的声明会增加耗时(但是差这么多也太夸张了吧……),而全局变量和静态变量不会。

另外,这题想不到怎么直接用堆来写不超时(就算采用大佬的方法优化了判断大小的算法,还存在一个堆排序的问题,因为就算是大顶堆,也只能保证根结点的值比左右孩子大,但是并不能保证左右孩子是有序的,因此在处理完之后的堆里,并不直接就是我们要的答案,还需要经过堆排序,但是这种操作一定是会超时的……),然后参考了网上各位大佬的代码,发现都是用优先队列做的,不过既然优先队列底部就是堆,那用优先队列应该也算堆操作吧……

这一题的算法比较机智,我参考了【慢浸天空的雨色】,简单的来讲就是先让A序列的第一个数去依次加上B序列的所有数,然后就得到了存放N个和的优先队列(这里的优先队列是默认的,也就是最大的数在队首),之后,再让A序列的第二个数去依次加上B序列的所有数,并判断,如果加出来的和比队首元素还小(我们要得到的是尽可能都是小的数的这样一个队列,因此需要剔除大数),那就让队首元素出队,让那个数入队,这里有一个很重要的地方是,如果加出来的和大于等于队首元素,那么直接break掉(这里是利用了两个序列都是非递减序列的特性,如果当前之和已经较大了,往后循环只会更大,所以加上这句话可以让耗时减少一倍)。

最后处理完之后,得到的就是一个从大到小排列的优先队列,因为队列里肯定是N个数,因此只要倒着赋给一个数组ans,再正着输出这个数组就是答案了(我想过利用栈的特性来存储,结果依然是超时了……)。
代码

#include
#include
#include
#include
#include
#include
using namespace std;
const int maxn = 100001;
priority_queue<int> q;
int seqA[maxn]={0};
int seqB[maxn]={0};
int ans[maxn]={0};//不要用stack存储,会超时 
int main(){
	int N;
	while(scanf("%d", &N) != EOF){
		int len = 0;
		for(int i=0;i<N;i++) scanf("%d", &seqA[i]);
		for(int i=0;i<N;i++){
			scanf("%d", &seqB[i]);
			q.push(seqA[0]+seqB[i]);//将序列A的第一个元素与序列B的每个元素相加,得到的数入队 
		}
		for(int i=1;i<N;i++){
			for(int j=0;j<N;j++){
				if(seqA[i]+seqB[j]<q.top()){//如果比队首元素更小
					q.pop(); //弹出原先的队首元素
					q.push(seqA[i]+seqB[j]);//放入优先队列里 
				}
				else break;//因为都是非递减序列,如果当前不满足
			}
		}
		for(int i=N-1;i>=0;i--){
			ans[i] = q.top();
			q.pop();
		}
		for(int i=0;i<N;i++){
			if(i==0) printf("%d", ans[i]);
			else printf(" %d", ans[i]);
		}
		printf("\n");
		memset(seqA, 0, sizeof(seqA));
		memset(seqB, 0, sizeof(seqB));
		memset(ans, 0, sizeof(ans));
	}
	return 0;
}

问题 C: 合并果子(堆)

题目描述
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将 1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为 12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
输入
输入文件fruit.in包括两行,第一行是一个整数n(1 <= n <= 30000),表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数ai(1 <= ai <= 20000)是第i种果子的数目。
输出
输出文件fruit.out包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于231。
样例输入

10
3 5 1 7 6 4 2 5 4 1

样例输出

120

提示
上传者:吕红波
思路
受到上一题的启发,这题做的还是比较快的,直接用优先队列(这里要设置数字小的优先级大,因此声明时要写greater,这和用make_heap()函数形成小顶堆的方式是一样的),然后每次出队两个小数字num1、num2,把它们加起来累计到消耗总体力sum里(sum += num1+num2),然后再把它们的和重新入队(因为这个操作意味着把两个小堆合并成了一个大堆,因此还需要入队)。
至于循环的终止条件,既可以用for循环写for(int i=1;i<=n-1;i++)(因为题目也说了经过n-1次合并之后,就只剩一个堆了),也可以写while(q.size()>1)(因为队列里的每个元素是堆,当q.size()==1时就代表只剩一个果子堆了,所以只要q.size()>1时就说明还有两个及以上的果子堆,就要执行循环)。
代码

#include
#include
#include
#include
#include
#include
using namespace std;
priority_queue<int, vector<int>, greater<int> > q;
int main(){
	int n;
	while(scanf("%d", &n) != EOF){
		for(int i=1;i<=n;i++){
			int tmp;
			scanf("%d", &tmp);
			q.push(tmp);
		}
		int sum = 0;
		while(q.size()>1){//因为队列里放的是果子堆,目标是合成一个大堆,所以当q.size()==1时停止循环 
			int num1 = q.top();
			q.pop();
			int num2 = q.top();
			q.pop();
			sum += num1+num2;//先把两个小的果子堆合并
			q.push(num1+num2);//再把它入队,表明这是一个合并好的大堆 
		}
		printf("%d\n", sum);
		q.pop();//把最后一个堆出队
	}
	return 0;
}

小结

虽然用优先队列解决这类题目非常方便,并且做题的时候第一选择肯定也是用优先队列(用人家造好的轮子),既非常方便,又避免了因为自己写堆操作而导致的错误(从这里可以看出STL的优先队列确实比里的make_heap()方便很多),可以更好地把自己的注意力放在解决问题的逻辑性方面。但是,堆作为一个重要的数据结构,是树的一个分支(堆是一棵完全二叉树),还是需要我们好好理解它的实现方式的,就算不造轮子,我们至少也要把这个别人造的好轮子好好欣赏一下,是叭?

你可能感兴趣的:(《算法笔记》学习日记,队列,数据结构,算法,c++,堆排序)