贪心法总结
文章目录
贪心算法(又称贪婪算法)是指,在对 问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑, 算法得到的是在某种意义上的局部最优解 [1] 。 ——百度百科
贪心算法不是对所有问题都能得到整体最优解,贪心法的特点是只考虑当下,得到的是局部(当下的)最优,不能保证整体情况下的最优。另外,贪心策略的选择是关键。
一、贪心类型概述
贪心是一种思想而非一种题型,因为对于涉及贪心的题目而言,没有固定的板型,也没有特定的解法。另外,一道题为什么可以用贪心法来解决,证明贪心正确性的难度远大于代码中的构造。
贪
心
问
题
=
{
1.
区
间
问
题
2.
最
优
装
载
问
题
3.
乘
船
问
题
4.
部
分
背
包
问
题
5.
活
动
安
排
问
题
其
他
.
.
.
贪心问题 = \left\{ \begin{aligned} & 1.区间问题 \\ & 2.最优装载问题 \\ & 3.乘船问题 \\ & 4.部分背包问题 \\ & 5.活动安排问题 \\ & 其他... \end{aligned} \right.
贪心问题=⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧1.区间问题2.最优装载问题3.乘船问题4.部分背包问题5.活动安排问题其他...
一、什么样的问题可用贪心求解?
利用贪心算法求解的问题往往具有两个重要的特性:贪心选择性质和最优子结构性质。(1)贪心选择
所谓贪心选择性质是指原问题的整体最优解可以通过一系列局部最优的选择得到。(2)最优子结构
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。动态规划的问题一般都不可以用贪心来解决。因为得到的不一定是全局最优解。
二、如何证明贪心策略的准确性?
1.反证法:顾名思义,对于当前的贪心策略,否定当前的选择,看看是否能得到最优解,如果不能得到,说明当前贪心策略是正确的;否则,当前策略不正确,不可用。
2.构造法:对于题目给出的问题,用贪心策略时,把问题构造成已知的算法或数据结构,以此证明贪心策略是正确的。 ——引自 https://blog.csdn.net/yanyanwenmeng/article/details/83005293
二、典型贪心问题
2.1 区间问题:
-
样例:
题目描述
给定 N 个闭区间 [ai, bi] 请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。输出选择的点的最小数量。
注:位于区间端点上的点也算作区间内。
输入样例:3
-1 1
2 4
3 5
输出样例:2
-
思路|决策:按右端点排序,查看当前线段的左端点是否大于上一个线段的右端点,如果是,说明两个区间无共同部分,要选的点数加一,更新右端点。
-
代码:
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; const int N = 100010; struct range { int l, r; bool operator <(const range& p) { return r < p.r; } }range[N]; int n; int main() { cin >> n; for (int i = 0; i < n; i ++ ) cin >> range[i].l >> range[i].r; sort(range, range + n); int res = 0, ed = -2e9; for (int i = 0; i < n; i ++ ) { if (range[i].l > ed) { res ++ ; ed = range[i].r; } } cout << res; return 0; }
这题等同上面的区间选点,不信交上面的代码也能过qwq,下面给一份pair数组的吧。
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
PII p[100010];
int n;
bool cmp(PII &a, PII &b)
{
return a.second < b.second;
}
int main()
{
cin >> n;
for (int i = 0; i < n; ++ i)
cin >> p[i].first >> p[i].second;
sort(p, p + n, cmp);
int cnt = 1;
for (int i = 1; i < n; ++ i)
{
if (p[i].first > p[i-1].second) cnt ++;
else p[i].second = p[i-1].second;
}
cout << cnt;
return 0;
}
-
样例:
题目描述
给定 N 个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。
输入格式第一行包含整数 N,表示区间数。
接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。
输出格式输出一个整数,表示最小组数。
输入样例3
-1 1
2 4
3 5
输出样例2
-
思路1:按左端点排序(优先队列),——详情请看 AcWing 906. 区间分组 - AcWing
代码:
#include <bits/stdc++.h> using namespace std; const int N = 100010; struct node { int l, r; bool operator <(const node& p) { return l < p.l; } }a[N]; int main() { int n; cin >> n; for (int i = 0; i < n; ++ i) cin >> a[i].l >> a[i].r; sort(a, a + n); priority_queue<int, vector<int>, greater<int> > heap; for (int i = 0; i < n; ++ i) { if (heap.empty() || heap.top() >= a[i].l) heap.push(a[i].r); else { heap.pop(); heap.push(a[i].r); } } cout << heap.size(); return 0; } 注:如有侵权,请联系我删除,谢谢! 作者:松鼠爱葡萄 链接:https://www.acwing.com/solution/content/14773/ 来源:AcWing
思路2:模拟活动安排,——详情请看 AcWing 906. 区间分组(200 +ms) - AcWing
有若干个活动,第i个活动开始时间和结束时间是[SiSi,fifi],同一个教室安排的活动之间不能交叠,求要安排所有活动,少需要几个教室?
有时间冲突的活动不能安排在同一间教室,与该问题的限制条件相同,即最小需要的教室个数即为该题答案。我们可以把所有开始时间和结束时间排序,遇到开始时间就把需要的教室加1,遇到结束时间就把需要的教室减1,在一系列需要的教室个数变化的过程中,峰值就是多同时进行的活动数,也是我们至少需要的教室数。
代码:
#include <iostream> #include <algorithm> using namespace std; const int N = 100100; int n; int b[2 * N], idx; int main() { cin >> n; for(int i = 0; i < n ; i ++) { int l, r; scanf("%d %d", &l, &r); b[idx ++] = l * 2;//标记左端点为偶数。 b[idx ++] = r * 2 + 1;// 标记右端点为奇数。 } sort(b, b + idx); int res = 1, t = 0; for(int i = 0; i < idx ; i ++) { if(b[i] % 2 == 0) t ++; else t --; res = max(res, t); } cout << res << endl; return 0; } 注:如有侵权,请联系我删除,谢谢! 作者:未来i 链接:https://www.acwing.com/solution/content/8902/ 来源:AcWing
-
样例:
题目描述
给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。输出最少区间数,如果无法完全覆盖则输出 −1−1。
输入格式第一行包含两个整数 ss 和 tt,表示给定线段区间的两个端点。
第二行包含整数 NN,表示给定区间数。
接下来 NN 行,每行包含两个整数 ai,biai,bi,表示一个区间的两个端点。
输出格式输出一个整数,表示所需最少区间数。
如果无解,则输出 −1−1。
输入样例1 5
3
-1 3
2 4
3 5
输出样例2
-
思路|决策:按左端点排序,有几种情况是找不到解的:
-
待覆盖区间的左端点比所有线段的左端点还靠左,区间覆盖失败;
-
待覆盖区间的右端点比所有线段的右端点还靠右,区间覆盖失败;
-
中间的线段衔接不上,如:排序后a1和a2是相邻的,
a2的左端点接不上a1的右端点,区间覆盖失败。
其中,1.和3.可以视为一种情况,通过左端点的处理判断。
-
-
代码:
#include <bits/stdc++.h> using namespace std; const int N = 100010; struct node { int l, r; bool operator <(const node& p) { return l < p.l; } }a[N]; int main() { int s, t, n; cin >> s >> t >> n; bool suc = false; for (int i = 0; i < n; ++ i) cin >> a[i].l >> a[i].r; sort(a, a + n); int res = 0; for (int i = 0; i < n; ++ i) { int j = i, r = -2e9; while (j < n && a[j].l <= s) {//在保证左端依然覆盖区间的情况下,看看右端最远到哪 r = max(r, a[j].r); ++ j; } if (r < s) //情况1:待覆盖区间左端比所有给出的区间还靠左 和 情况2 {//两个区间衔接不上,覆盖失败 res = -1; break; } res ++; if (r >= t)//完成任务,收工 { suc = true; break; } s = r, i = j - 1; } if (!suc) res = -1; cout << res; return 0; }
2.2 部分背包问题
例如,洛谷p2240部分背包问题、p1208混合牛奶,这里以第一个为例:
题目描述
阿里巴巴走进了装满宝藏的藏宝洞。藏宝洞里面有N堆金币,第 i 堆金币的总重量和总价值分别是 m i , v i m_i,v_i mi,vi 。阿里巴巴有一个承重量为 T ( T ≤ 1000 T (T {\leq 1000} T(T≤1000) 的背包,但并不一定有办法将全部的金币都装进去。他想装走尽可能多价值的金币。所有金币都可以随意分割,分割完的金币重量价值比(也就是单位价格)不变。请问阿里巴巴最多可以拿走多少价值的金币?
输入格式第一行两个整数 N, T。
接下来 N行,每行两个整数 m i , v i m_i,v_i mi,vi。
输出格式一个实数表示答案,输出两位小数
按照"比价值"最大,即单位体积价值最大来取就行了。直接附代码如下
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef struct
{
int m, v;
double s;
}node, pile[110];
int n, t;
pile P;
bool cmp(node &a, node &b)
{
return a.s > b.s;
}
int main()
{
cin >> n >> t;
for (int i = 0; i < n; ++ i)
{
cin >> P[i].m >> P[i].v;
P[i].s = (double)P[i].v / P[i].m;
}
sort(P, P + n, cmp);
double res = 0.0;
for (int i = 0; i < n && t; ++ i)
{
if (t >= P[i].m) {
t -= P[i].m;
res += P[i].v;
} else {
res = res + P[i].s * t;
t = 0;
}
}
printf("%.2lf", res);
return 0;
}
2.3 哈夫曼树类
例如,洛谷p1223排队接水、p1090合并果子、p1478陶陶摘苹果,都是一样的,用优先队列或者排序写即可。
2.4 推公式、构造
一、洛谷p1080国王游戏,题目如下:
题目描述
恰逢 H国国庆,国王邀请n位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。
输入格式第一行包含一个整数n,表示大臣的人数。
第二行包含两个整数a和 b,之间用一个空格隔开,分别表示国王左手和右手上的整数。
接下来 n行,每行包含两个整数a 和 b,之间用一个空格隔开,分别表示每个大臣左手和右手上的整数。
输出格式一个整数,表示重新排列后的队伍中获奖赏最多的大臣所获得的金币数。
这题,核心思路包含两个部分:
- 假设大臣之间顺序不能改变,问谁获得的奖赏最多?
- 肯定存在一个顺序,使得在此顺序下,奖赏最多的大臣获得了m个金币;而在其他任意排队顺序下,受奖赏最多的大臣获赏m +(即大于m)个金币。 如何找到这个顺序?
-
对于第一部分,谁获得的奖赏最多?虽然越靠后左手乘积的积累越大,但是每个人右手上的值不同,被除数变大,除数变化未知,不能明确判断哪个特定位置最大,只能从前向后依次遍历取最大值。
-
对于第二部分,如何找到合适的顺序?不加证明地给出:按大臣左右手数字乘积的从小到大来排序。
3 // 三位大臣
1 1 // 这是国王的
2 3 // 大臣1:2 * 3 = 6
7 4 // 大臣2:7 * 4 = 28
4 6 // 大臣3:4 * 6 = 24按照(大臣左手数)a *(大臣右手数)b大小排序:(答案下取整)
1 1 // 国王不变
2 3 // 大臣1获赏:1 / 3 = 0;
4 6 // 大臣3获赏:2 / 6 = 0;
7 4 // 大臣2获赏:8 / 4 = 2;
所以答案为:2
其次需要注意的是,这题需要写高精,而且是高精乘和高精除两个,有个小坑:高精乘法是倒序相乘,而高精除法是正序相除,所以要保证每次乘的时候都是倒序,每次除的时候都是正序。
所用方法:高精 * 低精 和 高精 / 低精
-
累乘结果数组A,答案数组(即最大值)C,
对于A数组从初始到更新:
vector<int> A; A.push_back(1);
//初始for (int i = 1; i <= n; ++ i)//更新 { A = mul(A, AR[i-1].a);//A一直为倒序,为累乘结果数(组) C = more(C, div(A, AR[i].b)); //这个more函数是为了比较谁是最大值,而且C一直是倒序,因为除法完成后需要倒序去 //前导零(这是因为在除法中默认从第一个数开始,如果不够除就把该位置写上0,比如 125 / 40, 下取整后答案为003,去前导零后为3),所以more函数中两个数字的数字 都是倒序的,这样只要从后向前相比较即可。 }
-
高精*低精和高精/低精:
vector <int> mul(vector <int>& A, int b) { vector <int> C; int t = 0; for (int i = 0; i < A.size(); i ++) { t += A[i] * b; C.push_back(t % 10); t /= 10; } while (t) { // 处理最后剩余的 t C.push_back(t % 10); t /= 10; } while (C.size() > 1 && C.back() == 0) C.pop_back();//这个如果乘数均为非零,可以不写。 return C; } vector <int> div(vector <int> A, int b) { vector <int> C; int r = 0; reverse(A.begin(), A.end()); for (int i = 0; i < A.size(); ++ i) { r = r * 10 + A[i]; C.push_back(r / b); r %= b; } reverse(C.begin(), C.end());//得到反序的数 while (C.size() > 1 && C.back() == 0) C.pop_back();//除去前导零 return C; }
-
more函数:
vector<int> more(vector<int>C, vector<int>D) { if (C.size() != D.size()) return (C.size() > D.size()) ? C : D; for (int i = C.size() - 1; i >= 0; -- i) { if (C[i] != D[i]) return (C[i] > D[i]) ? C : D; } return C; }
-
完整代码:
#include <bits/stdc++.h> using namespace std; typedef long long LL; struct Node { int a, b; LL a_b; bool operator <(const Node& p) { return a_b < p.a_b; } }AR[1010]; vector <int> div(vector <int> A, int b) { vector <int> C; int r = 0; reverse(A.begin(), A.end()); for (int i = 0; i < A.size(); ++ i) { r = r * 10 + A[i]; C.push_back(r / b); r %= b; } reverse(C.begin(), C.end()); while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; } vector <int> mul(vector <int>& A, int b) { vector <int> C; int t = 0; for (int i = 0; i < A.size(); i ++) { t += A[i] * b; // t + A[i] * b = 7218 C.push_back(t % 10); // 只取个位 8 t /= 10; // 721 看作 进位 } while (t) { // 处理最后剩余的 t C.push_back(t % 10); t /= 10; } while (C.size() > 1 && C.back() == 0) C.pop_back(); return C; } vector<int> more(vector<int>C, vector<int>D) { if (C.size() != D.size()) return (C.size() > D.size()) ? C : D; for (int i = C.size() - 1; i >= 0; -- i) { if (C[i] != D[i]) return (C[i] > D[i]) ? C : D; } return C; } int main() { int n, a, b; vector<int> A, C, D; A.push_back(1); cin >> n >> AR[0].a >> AR[0].b; for (int i = 1; i <= n; ++ i) { cin >> AR[i].a >> AR[i].b; AR[i].a_b = AR[i].a * AR[i].b; } sort(AR + 1, AR + n + 1); for (int i = 1; i <= n; ++ i) { A = mul(A, AR[i-1].a); C = more(C, div(A, AR[i].b)); } for (int i = C.size() - 1; i >= 0; -- i) cout << C[i]; return 0; }
二、洛谷p1106删数问题
题目描述
键盘输入一个高精度的正整数 N(不超过 250位),去掉其中任意 k个数字后剩下的数字按原左右次序将组成一个新的非负整数。编程对给定的 N和 k,寻找一种方案使得剩下的数字组成的新数最小。
输入格式n(高精度的正整数 )。
k(需要删除的数字个数 )。
输出格式最后剩下的最小数。
注:删除后可以存在前导零的情况,比如0012,即为12。
方法|决策:每次遇到第一个开始下降的值就删除,
如:123542 要求删除2个数
第一个要删除的数:5,因为5是第一个开始下降的值,删除后得:12342
第二个要删除的数:4,与上面是同样的原因,删除后得:1232
证明:略。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 300;
int a[N];
void delet(int len)
{
for (int i = 0; i < len; ++ i)
{
if (a[i] > a[i+1])
{
while (i < len)
{
a[i] = a[i+1];
++ i;
}
break;
}
}
return;
}
string s;
int k; bool flag;
int main()
{
int len = 0;
cin >> s;
for (int i = 0; i < s.size(); ++ i)
a[len ++] = s[i] - '0';
cin >> k;
while (k --)
{
delet(len);
len --;
}
for (int i = 0; i < len; ++ i)
{
if (a[i])
{
flag = true;
while (i < len)
{
cout << a[i];
++ i;
}
}
}
if (!flag) cout << 0;
return 0;
}
三、洛谷p5019铺设道路
题目描述
春春是一名道路工程师,负责铺设一条长度为 n 的道路。铺设道路的主要工作是填平下陷的地表。整段道路可以看作是 n 块首尾相连的区域,一开始,第 i块区域下陷的深度为 d i d_i di 。
春春每天可以选择一段连续区间[L,R],填充这段区间中的每块区域,让其下陷深度减少 1。在选择区间时,需要保证,区间内的每块区域在填充前下陷深度均不为 0 。春春希望你能帮他设计一种方案,可以在最短的时间内将整段道路的下陷深度都变为 0 。
输入格式输入文件包含两行,第一行包含一个整数 n,表示道路的长度。 第二行包含 n个整数,相邻两数间用一个空格隔开,第i个整数为 d i d_i di 。
输出格式输出文件仅包含一个整数,即最少需要多少天才能完成任务。
方法|决策:如果当前下陷深度 h 2 h_2 h2比前一个位置 h 1 h_1 h1更大,那么就填充 h 2 − h 1 h_2 - h_1 h2−h1的深度,如果当前下陷深度小于等于前一个位置,那么就不动。
代码:
#include <bits/stdc++.h>
using namespace std;
int a[100010];
int n, sum;
int main()
{
cin >> n;
for (int i = 1; i <= n; ++ i)
{
cin >> a[i];
if (a[i] > a[i-1]) sum += a[i] - a[i-1];
}
cout << sum;
return 0;
}
四、洛谷p1094纪念品分组
题目描述
元旦快到了,校学生会让乐乐负责新年晚会的纪念品发放工作。为使得参加晚会的同学所获得 的纪念品价值相对均衡,他要把购来的纪念品根据价格进行分组,但每组最多只能包括两件纪念品, 并且每组纪念品的价格之和不能超过一个给定的整数。为了保证在尽量短的时间内发完所有纪念品,乐乐希望分组的数目最少。你的任务是写一个程序,找出所有分组方案中分组数最少的一种,输出最少的分组数目。
输入格式共 n+2 行:
第一行包括一个整数 w,为每组纪念品价格之和的上上限。
第二行为一个整数 n,表示购来的纪念品的总件数 G。
第 3∼n+2 行每行包含一个正整数 P i P_i Pi表示所对应纪念品的价格。
输出格式一个整数,即最少的分组数目。
方法|决策:价格从小到大排序,头尾设置两个指针(类似双指针),一个大的配一个小的为一组,如果这两个之和大于上限,就将小的去掉,仅价格大的一组。
代码:
#include <bits/stdc++.h>
using namespace std;
int a[30010];
int m, n, cnt;
int main()
{
cin >> m >> n;
for (int i = 0; i < n; ++ i) cin >> a[i];
sort(a, a + n);
int i = 0, j = n - 1;
while (i <= j)
{
if (a[i] + a[j] <= m) ++ i;
-- j;
++ cnt;
}
cout << cnt;
return 0;
}
三、一句话总结贪心
贪心算法并不难,难的是证明——鲁迅
贪心很难想,更难证明qwq。 ——沃兹基•洛夫斯基•朔徳
THEEND…