基础数据结构3.3——堆
题目上添加了超链接,大家点一下题目就会自动跳转到Poj原题界面~~ 冲鸭冲鸭ヾ(◍°∇°◍)ノ゙。
前言:
我觉得想要理解堆,大家需要先学会堆排序,在教学过程中一般是放在树(图)后面的。如果大家理解困难可以先跳过。
堆(heap)是一种特别的完全二叉树,如果完全二叉树中每个节点的值都小于等于其子节点的值,称为最小堆(min heap);反之,如果每个节点的值都大于等于其子节点的值,称为最大堆(max heap)。
C++ STL中的优先队列priority_queue就是堆的实现,对于堆的应用可以直接使用优先队列,需要包含queue头文件。
优先队列应用很广泛,是贪心算法的重要组成部分。例如,在图算法中,使用优先队列解决基于贪心思想的最小生成树等问题。
一般什么时候用到堆?当题目要求我们需要对一组数据多次反复操作(动态改变),并且每次操作(改变)后依旧要求单调有序。
3.3.1 Fence Repair (3253)
题意:将一个长木板进行n-1次切割,切成n个小木板,切木板的花费等于木板的长度。给出最后切割后的长度,求费用最小的切割方案。
小笔记:堆排平均复杂度O(nlogn),题目的主要操作是需要从集合中反复取出最小的两个值,每次都重新排序的话算法复杂度会很高,采用堆就能很好的解决这个问题。
#include <cstdio> #include <queue> using namespace std; int main() { int n; long long a, ans = 0; priority_queue<long long, vector<long long>, greater<long long> > Q; scanf("%d", &n); while (n--) { scanf("%lld", &a); Q.push(a); } while (Q.size() > 1) { long long x = Q.top(); Q.pop(); long long y = Q.top(); Q.pop(); ans += x + y; Q.push(x + y); } printf("%lld\n", ans); return 0; }
3.3.2 Argus (2051)
题意:输入格式Register n p,其中n代表查询序号,p代表周期,每隔p输出一个n,给出1~k间隔,依次打印输出结果,如果同一时间有多个输出,则按n值从小到大输出。
小笔记:这道题坑挺多的,建议大家自己做一下试试。
#include <cstdio> #include <queue> using namespace std; struct Data { int n; //查询序号 int p; //周期 int t; //访问时间 Data(int a, int b) : n(a), p(b), t(b) {} bool operator<(const Data &a) const { return (t > a.t) || (t == a.t && n > a.n); } }; int main() { priority_queue<Data> Q; char s[10]; while (scanf("%s", s) && s[0] != '#') { int n, p; scanf("%d%d", &n, &p); Q.push(Data(n, p)); } int k; scanf("%d", &k); while (k--) { Data a = Q.top(); Q.pop(); printf("%d\n", a.n); a.t += a.p; Q.push(a); } return 0; }
3.3.3 Black Box (1442)
题意:有一个黑盒子处理一些命令,命令有两种
ADD(x):将x放入黑盒子;
GET:从1开始,第i次GET操作是从把黑盒子里面第i小的数字输出。
a[1…m]表示要放入黑盒子的m个数,u表示第i次GET操作时黑盒子里的数字数量。
小笔记:这道题上了难度。堆能保证每加入一个元素,集合里面的元素都保持有序,但是堆只能取堆顶元素,无法取到中间元素。这里就需要将序列分为两个堆,一个最大堆一个最小堆。
#include <cstdio> #include <queue> using namespace std; int main() { priority_queue<int> R; //最大堆 priority_queue<int, vector<int>, greater<int>> L; // 最小堆 int m, n; scanf("%d%d", &m, &n); int a[30005]; for (int i = 0; i < m; i++) scanf("%d", &a[i]); int i = 0; while (n--) { int u; scanf("%d", &u); //先将小于u数量的数放入L while (i < u) L.push(a[i++]); //随时调整,保证R中元素数量为i-1 while (!R.empty() && R.top() > L.top()) { int t = R.top(); R.pop(); R.push(L.top()); L.pop(); L.push(t); } printf("%d\n", L.top()); R.push(L.top()); L.pop(); } return 0; }
3.3.4 Sequence (2442)
题意:m个序列,每个序列包含n个非负整数,从每个序列中各取一个数加到一起,这样的和有n^m个,要求从小到大输出前n个和。
小笔记:最直观的做法是枚举所有的和之后排序,但如此做必然一发TEL啊。用贪心可以解决这个问题,优化第一步,题目是在nm个和中求前n个,所以在计算过程中维持一个数量为n的集合就可以了,我们只需要不断计算得到一个n的最大堆即可。
#include <cstdio> #include <algorithm> #include <queue> using namespace std; int main() { int t; scanf("%d", &t); while (t--) { priority_queue<int> Q; int m, n; scanf("%d%d", &m, &n); int a[105][2005]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) scanf("%d", &a[i][j]); sort(a[i], a[i] + n); for (int j = 0; i && j < n; j++) for (int k = 0; k < n; k++) { if (j) { if (Q.top() > a[i][j] + a[0][k]) Q.pop(); else break; } Q.push(a[i][j] + a[0][k]); } for (int k = n - 1; i && k >= 0; k--) { a[0][k] = Q.top(); Q.pop(); } } for (int i = 0; i < n; i++) printf("%d ", a[0][i]); printf("\n"); } return 0; }
3.3.5 Moo University - Financial Aid (2010)
题意:c头小牛申请补助,每个小牛有分数和需求两个属性,要求按分数选出n头小牛,n为奇数,它们的总需求不超过f,且位于中位数的分数尽可能小。输出符合条件的分数,如果找不到,输出-1。
小笔记:这题好像数据比较弱,二分答案就能AC。实际挺劝退的。
题解:将小牛的信息保存到二元组a中,两个属性分别是分数和需求,将a按分数递减排序,排序后对a的每个位置进行枚举,分别计算前n/2个元素的需求的和lsum与后n/2个元素的需求的和rsum。lsum和rsum要保证值尽量小,求解过程可以通过两个最大堆来实现。
#include <cstdio> #include <queue> #include <algorithm> using namespace std; const int N = 100005; bool cmp(pair<int, int> a, pair<int, int> b) { return a.first > b.first; } int main() { int n, c, f; scanf("%d%d%d", &n, &c, &f); n /= 2; //为了方便,将n/2赋给n pair<int, int> a[N]; for (int i = 0; i < c; i++) scanf("%d%d", &a[i].first, &a[i].second); sort(a, a + c, cmp); //从前往后将a中前n/2个元素d值插入L中并计算lsum值 int lsum[N]; //保存a中某位置前n/2个元素的d的和 int rsum[N]; //保存a中某位置后n/2个元素的d的和 priority_queue<int> R, L; for (int i = 0; i < n; i++) { lsum[n] += a[i].second; L.push(a[i].second); } //对于接下来的元素,和堆顶值进行比较,小于堆顶值则将堆和lsum更新 for (int i = n; i < c - n; i++) { lsum[i + 1] = lsum[i]; if (a[i].second < L.top()) { lsum[i + 1] = lsum[i] + a[i].second - L.top(); L.pop(); L.push(a[i].second); } } //从后往前将a中后n/2个元素d值插入R中并计算rsum值 for (int i = c - 1; i > c - n - 1; i--) { rsum[c - n - 1] += a[i].second; R.push(a[i].second); } //对于接下来的元素,和堆顶值进行比较,小于堆顶值则将堆和rsum更新 for (int i = c - n - 1; i > n - 1; i--) { rsum[i - 1] = rsum[i]; if (a[i].second < R.top()) { rsum[i - 1] = rsum[i] + a[i].second - R.top(); R.pop(); R.push(a[i].second); } } //在满足a[i].second+ lsum [i]+ rsum [i]<f条件情况中取最小的i就是题解,如果所有i都不满足条件,则输出-1 int i; for (i = n; i < c - n && lsum[i] + a[i].second + rsum[i] > f; i++) ; printf("%d\n", (i == c - n) ? -1 : a[i].first); return 0; }