2.2 一直往前!贪心法
贪心发就是遵循某种规则,不断贪心地选取当前最优策略的算法设计方法。
1.硬币问题
有1元,5元,10元,50元,100元,500元的硬币各C1,C5,C10,C50,C100,C500枚。现在要用这些硬币支付A元,最少需要多少枚硬币?嘉定本体至少存在一种支付方案。
限制条件:
0<=C1,C5,C10,C50,C100,C500 <= 10^9
0<=A <= 10^9
样例:
输入:
C1= 3,C5 = 2,C10 = 1,C50 = 3,C100 = 0,C500 = 2,A = 620
输出:
6(500元硬币1枚,50元硬币2枚,10元硬币1枚,5元硬币2枚,合计6枚)
问题比较贴近生活,想象一下,可以得到如下的方法:
首先尽可能用500元硬币
剩余部分尽可能用100元硬币
剩余部分尽可能用50元硬币
剩余部分尽可能用10元硬币
剩余部分尽可能用5元硬币
剩余部分尽可能用1元硬币
简言之,优先使用面值最大的硬币
穷竭搜索和动态规划是从多种策略中选取最优解,而贪心算法则不痛,遵循某种规则,不断地选取当前最优策略。并且,如果问题能够用贪心算法求解,那么通常是非常高效的。
解题程序如下:
#include <iostream> using namespace std; const int V[6] = {1, 5, 10, 50, 100, 500}; // 定义面值 int C[6] = {3, 2, 1, 3, 0, 2}; // 各面值的数量 int A = 620; // 要凑齐面值 int main() { int ans = 0; for(int i = 5; i >= 0; i--) { int t = min( A/V[i], C[i]); //尽可能多地使用大面值 A -= t * V[i]; ans += t; } cout << "ans: " << ans << endl; return 0; }
2.区间调度问题
有n项工作,每项工作分别在Si时间开始,在Ti时间结束,对于每项工作,都可以选择参与与否,如果选择参与,那么自始至终都必须全程参与。此外,参与工作的时间段不能重叠。
目标是参与尽可能多的工作,那么最多能参加多少项工作呢?
限制条件:
1<= N <= 100000
1<= Si <= Ti <= 10^9
样例:
输入:
N= 5, s = { 1, 2, 4, 6, 8},t = { 3, 5, 7, 9, 10};
输出:
3(选取工作1,3,5)
本题采用贪心算法可以有如下的几种方法:
A在可选的工作中,每次选取开始时间最早的工作。
B在可选的工作中,每次选取结束时间最早的工作
C在可选的工作中,每次都选取用时最短的工作
只有算法B是正确的,其他的都可以找到对应的反例,或者说某些情况下,他们呢选取的工作并非最优。
#include <iostream> using namespace std; const int MAX_N = 5; int S[MAX_N] = {1, 2, 4, 6, 8}; int T[MAX_N] = {3, 5, 7, 9, 10}; // 用于对工作排序的pair数组 pair<int, int> itv[MAX_N]; int main() { // 对pair进行赋值,并且排序 for(int i = 0; i < MAX_N; i++) { itv[i].first = T[i]; itv[i].second = S[i]; } sort(itv, itv + MAX_N); // 排序时按照最早结束时间排序,因此只需查找其最早开始时间满足条件即可 int ans = 0, t = 0; for( int i = 0; i < MAX_N; i++) { if(t < itv[i].second) { ans++; t = itv[i].first; } } cout << "ans: " << ans << endl; return 0; }
3. 字典序最小问题
给定长度为N的字符串S,要构造一个长度为N的字符串T,起初,T是一个空串,随后反复进行下列任意操作:
从S的头部删除一个字符,加入到T的尾部
从S的尾部删除一个字符,加入到T的尾部
目标是要构造字典序尽可能小的字符串T
S=”CDB” T = “ABC”
|
————————
开头 | | 末尾
S=”DB” T=”ABCC” S=”CD” T=”ABCB”
限制条件:
1<=N <= 2000
字符串S只包含大写英文字母
样例:
输入: N= 6 S = “ACDBCB”
输出: ABCBCD
从字典序的性质上看,无论T的末尾有多大,只要前面部分的较小就可以,所以可以使用贪心算法:
不断取S的开头和末尾中较小的一个字符放到T的末尾
完整算法:
按照字典序比较S和S翻转后的字符串S’
如果S较小,就从S的开头取出一个字符,追加到T的末尾
如果S’ 较小,就从S’ 的开头取出一个字符,追加到T的末尾 (如果相同,则取哪个都可以)
#include <iostream> using namespace std; int N = 6; char S[6] = {'A', 'C', 'D', 'B', 'C', 'B'}; int main() { int a = 0, b = N - 1; while( a <= b) { bool left = false; for( int i = 0; a + i <= b; i++) { if(S[a+i] < S[b-i]) { left = true; break; } else if(S[a+i] > S[b-i]) { left = false; break; } } if(left) putchar(S[a++]); else putchar(S[b--]); } cout << endl; return 0; }
4.其他问题
Saruman’sArmy
直线上有N个点,点i的位置是Xi,从这N个点中选择若干个,给他们图上标记。对每一个点,其距离为R以内的区域里必须有带有标记的点(自己本身带有标记的点,可以认为与其距离为0的地方有一个带有标记的点)。在满足这个条件的情况下,希望能为尽可能少的点添加标记。请问要有多少点被添加标记。
可知图中的三个黑色点,在R的范围内覆盖了所有的空心点。
限制:
1<= N <= 1000
0<= R <= 1000
0<= Xi <= 1000
样例:
输入:
N= 6 R = 10
X= { 1, 7, 15, 20, 30, 50}
输出:
3(如上图所示)
从最最左边的点开始考虑,对于这个点,到距离R内的区域内必须要有带有标记的点。带有标记的这个点,一定再此点的右侧
从最左边的点开始,距离R以内的最远点,标记本点;然后先右继续找R距离内最远的点。从此点右边的一点开始,重复上述过程即可。
#include <iostream> using namespace std; int N = 6; int R = 10; int X[6] = { 1, 7, 15, 20, 30, 50}; int main() { sort(X, X + N); int i = 0, ans = 0; while( i < N) { // s是没有覆盖的最左边的点的位置 int s = X[i++]; // 一直向右,找到距离s的距离大于R的点 while( i < N && X[i] <= s+R) i++; // p是新加上标记的点的位置 int p = X[ i-1]; // 一直向右前进直到距离p的距离大于R的点 while(i < N && X[i] <= p+R) i++; ans++; } cout << "ans: " << ans << endl; return 0; }
FenceRepair:
农夫约翰为了修理栅栏,要将一块很长的模板切割成N块。准备切成的木板长度为L1,L2,……,Ln,未切割的木板长度恰好为切割后木板长度的总和。每次切割木板时,需要的开销为木板的长度。例如长度为21的木板要切割为长度为5,8,8,的三块木板。长21的木板切割成为13和8的木板时,开销为21。再将长度为13的木板切割为长度5和8的板时,开销为13.于是合计开销为34.按照题目要求将木板切割的最小开销是多少?
输入:
N= 3 L= {8,5,8}
输出:
34(对应题目中的例子)
由于木板的切割顺序不确定,自由度很高。这样可以用略微奇特的贪心法求解。
切割的过程其实对应了一个二叉树,每次切割为两块,最终的叶子节点对应最终的木板的长度,开销的合计就是:木板的长度*节点的深度 的总和。
最短的板与次短的板的节点应该是兄弟节点,最短板应当是深度最大的叶子节点之一。类似于霍夫曼树。
#include<iostream> using namespace std; typedef long long ll; int N = 3; int L[3] = {8, 5, 8}; void swap(int i, int j) { /* int tmp = L[i]; L[i] = L[j]; L[j] = tmp; */ int tmp = i; i= j; j= tmp; } int main() { ll ans = 0; while( N>1) { int mii1 = 0, mii2 = 1; if(L[mii1] > L[mii2]) swap(mii1, mii2); // 令两个值指向当前最小与次小 for(int i = 2; i < N; i++) // 找最小和次小的节点 { if(L[i] < L[mii1]) { mii2 = mii1; mii1 = i; } else if(L[i] < L[mii2]) { mii2 = i; } } // 将两块板子合并 int t = L[mii1] + L[mii2]; ans += t; //由于先给mii1赋值,如果它正好是N-1单元的,mii2的会被重复 if(mii1 == N-1) swap(mii1, mii2); L[mii1] = t; L[mii2] = L[N-1]; N--; } cout << "ans: " << ans << endl; return 0; }
By Andy @ 2013-08-28