前言
- 终于开始刷贪心了,感觉cf很多题都喜欢出贪心,所以决定也练练
- 贪心貌似也有挺系统的分类和一些经典例题,把这些刷了应该就有提升了把
总结
- 贪心是指将问题通过当前最优解来求解得到最终的最优解,即局部最优解可以推出总体最优解。
- 给我的感觉就是一种思维题,通过一定技巧把题目旁敲侧击地解决了,但是如果没接触过或者想不出来,基本就很难了,但是贪心的代码实现并不长,所以说贪心即使很基础的算法也是很难的算法。
- 总得来说贪心算法难就难在每道题的贪心策略都不同,是否能贪心也很难看出来。
- 一般在解决贪心题的时候可以把他放到生活中的例子来,想想如果是生活中碰到这题,你会怎么解决让自己最舒服。
普通贪心
-
【例题】
-
- 题意:你有可以装k个宝贝容量的包,现在给你一些宝贝,每个宝贝价值p,有m个,问你最多可以装多少价值的宝贝
- 思路:将价值排序,先选大的,选到v = 0为止
- 代码如下
#include <algorithm> #include <cstdio> #include <utility> #include <vector> using namespace std; int n, v; int main() { #ifndef ONLINE_JUDGE freopen("test.in", "r", stdin); freopen("test.out", "w", stdout); #endif while (scanf("%d", &v), v) { scanf("%d", &n); vector<pair<int, int>> q; for (int i = 0; i < n; i++) { int p, m; scanf("%d%d", &p, &m); q.push_back(make_pair(p, m)); } sort(q.begin(), q.end()); int i = n - 1, ans = 0; while (i >= 0 && v) { if (q[i].second < v) { ans += q[i].first * q[i].second; v -= q[i].second; } else { ans += q[i].first * v; v = 0; } i--; } printf("%d\n", ans); } }
-
- 题意:你有1、5、10、50、100的钞票,给你各个钞票的张数,问你要凑齐某价格最多和最少需要多少张,如果凑不出来输出-1
- 思路:最少的话就从大面值的钞票开始贪心,如果最后剩余需要凑的钱不为0,则凑不齐,至于最多的话,贪心可以这样想:要凑用得最多,那言外之意就是我剩余得最少,所以可以凑最少的贪心策略去凑所有钞票的总价格 - 需要凑的价格,最后剩下的钞票数就是需要凑的价格,固剩下的钞票数量就是最多的答案
- 总结:这道题的寻找最多的那个答案,一开始死活想不出来,后来看了大佬的思路恍然大悟,所以有所感悟:有时候一个问题顺着想可能很难,那不妨试试逆着来想,所谓正难则反
- 代码如下
#include <bits/stdc++.h> using namespace std; int corn[] = {1, 5, 10, 50, 100}, t, n, nums[6] = {0}; int main() { #ifndef ONLINE_JUDGE freopen("test.in", "r", stdin); freopen("test.out", "w", stdout); #endif scanf("%d", &t); while (t--) { int tmp = n, indx = 4, Max = 0, Min = 0, sum = 0, csum = 0; scanf("%d", &n); for (int i = 0; i < 5; i++) { scanf("%d", nums + i); sum += nums[i] * corn[i]; csum += nums[i]; } while (tmp && indx >= 0) { int num = tmp / corn[indx]; if (num > nums[indx]) { num = nums[indx]; } tmp -= num * corn[indx]; Min += num; indx--; } if (tmp != 0) { printf("-1 -1\n"); continue; } //需要凑的最大数目 tmp = sum - n, indx = 4; while (tmp && indx >= 0) { int num = tmp / corn[indx]; if (num > nums[indx]) { num = nums[indx]; } tmp -= num * corn[indx]; Max += num; indx--; } printf("%d %d\n", Min, csum - Max); } return 0; }
-
- 题意:打龙了, 你目前要打n条龙,你的力量是s,你打一个龙的话你的力量必须比龙大,如果你打死一条龙你也可以获得相应的力量,问你是否能把所有的龙都打完
- 思路:一道很简单的贪心,先从力量小的龙开始打,看看能不能打完就好了
- 代码如下
#include <algorithm> #include <cstdio> #include <utility> #include <vector> #define all(x) x.begin(), x.end() using namespace std; vector<pair<int, int>> dr; int n, s; int main() { #ifndef ONLINE_JUDGE freopen("test.in", "r", stdin); freopen("test.out", "w", stdout); #endif scanf("%d%d", &s, &n); for (int i = 0; i < n; i++) { int x, y; scanf("%d%d", &x, &y); dr.push_back(make_pair(x, y)); } bool win = true; sort(all(dr)); for (int i = 0; i < n; i++) { if (s > dr[i].first) { s += dr[i].second; } else { win = false; puts("NO"); break; } } if (win) puts("YES"); return 0; }
-
- 题意:一个木段,现要锯成 L 1 , L 2 , L 3 … … L n L_1, L_2, L_3 …… L_n L1,L2,L3……Ln的长度,但是据一段木段需要消耗相应的体力,例如要把长为8的木段锯成 3 和 5 的话需要消耗3 + 5 = 8的体力,现在问你最少要消耗多少体力锯好
- 错误的思路:一开始想不出来,单纯的觉得是一个简短的排序不等式的的贪心题,只要先据出最大的目标值,然后一直据下去就是最优解,但这是不对的,最多也就过个他给的样例。
- 正确的思路①:仔细想想,如果是没按要求切成题目要求目标长度切,那样的话当然是希望一切下去可以让后面的体力没这么多,比如要分成2 2 3 3,这样的话应该是先 5 5 再 2 3 5 最后就 2 3 2 3这样就是最小的,消耗为20, 如果是按照先前的想法就是21,所以先前的思路显然是不对的。那这个思路怎么实现呢,dfs?显然很难实现
- 正确的思路②:回忆上上题的总结,正难则反,反过来想,现在把问题改成拼成一个完整的木段,消耗最小,这样的话就是拼当前最小的两小木段,最终的答案就是最小的
- 代码如下(注释的部分是一开始错误的思路,交上去才发现思路不对)
#include <algorithm> #include <cstdio> #include <cstring> #include <queue> using namespace std; using ll = long long; int n, arr[50010], sum[50010]; priority_queue<int> heap; int main() { #ifndef ONLINE_JUDGE freopen("test.in", "r", stdin); freopen("test.out", "w", stdout); #endif scanf("%d", &n); for (int i = 0; i < n; i++) { // scanf("%d", &arr[i]); int x; scanf("%d", &x); heap.push(-x); } int ans = 0; while (heap.size()) { int x = -heap.top(); heap.pop(); int y = -heap.top(); heap.pop(); ans += x + y; if (heap.size()) heap.push(-(x + y)); } printf("%d", ans); /* sort(arr, arr + n); for (int i = 0; i < n; i++) { sum[i + 1] = sum[i] + arr[i]; } ll ans = 0; for (int i = n - 1; i >= 1; i--) { ans = ans + arr[i] + sum[i] - sum[0]; } printf("%lld\n", ans); */ return 0; }
-
- 题意:钓鱼家有一个锅和一个鱼竿,他有 n 条鱼要钓,钓一条鱼需要 k 时间,他的任务就是把这些鱼都钓上来,并且把它们都煮了,第i条鱼的煮熟所需要的时间为 t i t_i ti,他钓鱼的时候不能往锅里放鱼并且要等他掉好k时间才可做下一件事,他可以在煮鱼的时候钓鱼,他可以同时拥有很多鱼,即他钓好后可以放在身边,因为锅一次只能煮一条鱼。现在问你他至少要花多少时间煮好n条鱼。
- 思路:
- 一开始看错题,wa了一下午,
英语太差的锅。回归正题,显然第一条鱼是钓的时间是不可避免的,全部煮鱼的时间也是不可避免的。可以优化的就只有钓鱼多出来的时间了。 - 注意到只要我煮的鱼时间足够长,那么我就可以在煮鱼的这个空档里面钓足够多的鱼,即可以钓
t
i
/
k
t_i / k
ti/k条,如果鱼快煮好了,即再过
x
(
x
<
k
)
x(x < k)
x(x<k)的时间就煮好鱼了,不难发现这里的
x
=
t
i
%
k
x = t_i \% k
x=ti%k,那么这时候是继续钓鱼呢,还是选择去等鱼煮好然后放下一条(前提是你手上有钓好的鱼)
- 如果你选择继续钓鱼,那时间就会多花 k − t i % k k - t_i \% k k−ti%k的时间
- 如果你选择去等待的话,那你手上就必须要有鱼,没鱼怎么办,那只能选择上个选择,即继续钓鱼
- 画图出来可能比较直观
- 对于这个配图,即便是 t i < k t_i < k ti<k公式依旧成立
- 显然,根据图,我们贪心的策略就是在煮鱼的空档里尽可能多地钓鱼,比如上图,就是钓2条,然后去等待鱼煮好就放下一条,假设这样下去我们总共在钓鱼的空档里钓的鱼数量为 n u m num num
- 正如上述第一条鱼钓的时间是不可避免的,所以接下来只剩下n - 1条鱼需要我们在煮鱼的空档时钓上来,假设 n u m ≥ n − 1 num \ge n - 1 num≥n−1,即我们在空档里钓的鱼比我们需要钓的鱼还多,那最终答案就是 k + ∑ t i k + \sum{t_i} k+∑ti
- 若 n u m < n − 1 num < n - 1 num<n−1,即我们在空档的时候钓的鱼不够我们需要钓的,那我们就要在煮鱼的空档多钓一条了,如上图就是钓3条,这样的操作需要我们进行 n − 1 − n u m n - 1 - num n−1−num次,很显然,我们希望总时间尽可能地短,那就必须让 k − t i % k k - t_i \% k k−ti%k尽可能地小
- 对于上述的操作,代码里面只要用个数组mor记录 k − t i % k k - t_i \% k k−ti%k,然后再ans里面加上前 n − 1 − n u m n - 1 - num n−1−num小的 k − t i % k k - t_i \% k k−ti%k就好了
- 一开始看错题,wa了一下午,
- 这题说得有点多了,因为自己一开始真的觉得有点难
- 但是听说还有另一种解法就是一种我所不知道的带反悔的贪心法
- 具体做法就是将鱼根据他们的煮的时间从大小排序,并且按顺序这样钓上来
- 然后幻想自己顺畅无阻地把0到n - 1的鱼一个一个地煮并且可以在每条鱼煮好后可以马上放下一条鱼(即幻想自己每条鱼都能在其空档期间充分钓足够的鱼为下一次煮鱼做准备)
- 而现实却不尽人意,所以需要每次记录在煮鱼的空档期间总共可以钓的鱼 n u m num num,如果 n u m num num小于我们幻想钓的鱼的数量就反悔,即穿越时空去我在前面其中一个煮鱼的空档里面多钓一条鱼,这个空档的条件依旧是和我的思路一样,即 k − t i % k k - t_i \% k k−ti%k尽可能地小
- 这个需要优先队列,具体代码也放出来吧,但是这代码不是我自己写的,是直接复制某大佬的
懒得自己再写了转载传送门
- 代码如下
#include <algorithm> #include <cstdio> #include <cstring> using namespace std; using ll = long long; int t, n; ll k, tim[100005], mor[100005]; int main() { #ifndef ONLINE_JUDGE freopen("test.in", "r", stdin); freopen("test.out", "w", stdout); #endif scanf("%d", &t); while (t--) { scanf("%d%lld", &n, &k); for (int i = 0; i < n; i++) { scanf("%lld", tim + i); } ll ans = k, num = 0; for (int i = 0; i < n; i++) { ans += tim[i]; num += tim[i] / k; mor[i] = k - tim[i] % k; } if (num < n - 1) { sort(mor, mor + n); for (int i = 0; i < n - 1 - num; i++) { ans += mor[i]; } } printf("%lld\n", ans); memset(mor, 0, sizeof mor); memset(tim, 0, sizeof tim); } return 0; } //下面是大佬的代码 #include <bits/stdc++.h> #define INF 0x3f3f3f3f #define debug(x) cout << #x << " = " << x << endl; using namespace std; typedef long long LL; const int mx = 1e5+7; int a[mx]; priority_queue<int> q; int main(){ int t, n, k; scanf("%d", &t); while (t--){ while (!q.empty()) q.pop(); scanf("%d%d", &n, &k); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); sort(a+1, a+n+1, greater<int>()); LL ans = k, num = 1; for (int i = 1; i <= n; i++){ ans += a[i]; num += a[i] / k; q.push(a[i] % k); if (num < i){ ans += k-q.top(); q.pop(); } } printf("%lld\n", ans); } return 0; }
区间贪心
-
【例题】
-
- 题意:给你一些区间,让你选出尽量多的不相交的区间
- 思路:把所有区间(x, y)根据y来从小到大排序,然后从第一区间开始,排除和他相交的区间,即对比前一个选定的区间的y和当前要对比的区间的x,如果x < y则不选,否则回到上一步
- 看代码容易懂
#include <algorithm> #include <cstdio> #include <utility> #include <vector> using namespace std; int n; int main() { #ifndef ONLINE_JUDGE freopen("test.in", "r", stdin); freopen("test.out", "w", stdout); #endif while (scanf("%d", &n), n) { vector<pair<int, int>> coo; int x, y; for (int i = 0; i < n; i++) { scanf("%d%d", &x, &y); coo.push_back(make_pair(y, x)); } sort(coo.begin(), coo.end()); int num = 0, last = -0x3f3f3f3f; for (int i = 0; i < n; i++) { if (last <= coo[i].second) { num++; last = coo[i].first; } } printf("%d\n", num); } }