概念
贪心算法是在每一步的选择中,都选择当时最佳的情况。即局部最优的选择。贪心算法并不能保证总能得到最优解,它总是做出局部最优的选择,但是很多问题确实可以求得最优解(数学归纳证明)。贪心的决策有点像深度学习的的马尔科夫决策都是基于当前的状态做出下一步的action,只不过这个action是贪心的。
算法应用场景
贪心算法通常是自顶向下的设计,做出一个抉择,然后求解剩下的子问题,而不是自底向上地求解出很多子问题,然后再做出抉择。每个贪心算法之下,几乎总有一个更繁琐的动态规划算法。
某个问题是否适合贪心算法,贪心选择性质和最优子结构性质是两个关键要素。
1:贪心选择性质
通过局部最优选择来构造最优解。也就是说,当考虑做何选择时,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。
这一点是贪心算法不同于动态规划之处。在动态规划中,每一步都要做出选择,但是这些选择依赖于子问题的解(比如钢条切割的第一切割点)。因此,解动态规划问题一般是自底向上,从小子问题处理至大子问题。在贪心算法中,我们所做的总是当前看似最佳的选择、然后再解决选择之后所出现的子问题。贪心算法不依赖于将来的选择或者子问题的解。
因此,不像动态规划方法那样自底向上地解决子间题,贪心策略通常是自顶向下地做的,一个一个地做出贪心选择,不断地将给定的问题实例归约为更小的问题。
当然,必须证明每个步骤作出贪心选择能生成局部最优解。
2:最优子结构
最优子结构也是应用于贪心算法的关键要素。在贪心算法中使用最优子结构时,通常是用更直接的方式:如前所述,假设在原问题中做了一个贪心选择而得到了一个子问题。真正要做的是证明将此子问题的最优解与所做的贪心选择合并后,的确可以得到原问题的一个最优解。这个方案意味着要对子问题采用归纳法,来证明每个步骤中所做的贪心选择最终会产生出一个最优解。
算法步骤
贪心算法一般是迭代贪心算法,就是尾递归的形式,它以一个对自身的递归调用再接一次并集操作结尾。一般情况下,可以按照下面的步骤设计贪心算法:
-
将最优化问题转化为这样的形式:对其作出一次选择之后,只剩下一个子问题需要求解;
-
证明作出贪心选择后,原问题总是存在最优解,也就是贪心选择总是安全的;
-
证明作出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解。
算法示例
背包问题:
贪心算法和动态规划不容易区分,背包问题例子如下:
- 01背包问题:有一个贼在偷窃一家商店时发现有n个物品,第i件物品价值为pi 元,重量为wi镑 。此处 pi和 wi都是整数。他希望带走的东西越值钱越好,但他的背包中至多只能装下W磅的东西,W为一整数。应该带走哪几样东西?(这个问题之所以称为01背包问题,是因为每件物品或被带走,或被留下。小愉不能只带走某个物品的一部分或带走两次以上的同一物品。)
- 算法分析: 分数背包问题具有最优子结构性质,01背包问题却不行。在01背包问题中不能用贪心算法解决,当我们考虑是否将一个商品放入背包时,必须比较包含此商品的解与不包含它的子问题的解,然后才能做出选择,这会导致大量的重叠子问题-动态规划的标识。
/*
j为bagSize ,w[i]为货物i的重量,dp[i][j] 表示 在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值
if(j<w[i])
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
*/
#include<iostream>
using namespace std;
#include <algorithm>
int main()
{
int w[5] = { 0 , 2 , 3 , 4 , 5 }; //商品的体积2、3、4、5
int v[5] = { 0 , 3 , 4 , 5 , 6 }; //商品的价值3、4、5、6
int bagV = 8; //背包大小
int dp[5][9] = { { 0 } }; //动态规划表
for (int i = 1; i <= 4; i++) {
for (int j = 1; j <= bagV; j++) {
if (j < w[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
}
//动态规划表的输出
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 9; j++) {
cout << dp[i][j] << ' ';
}
cout << endl;
}
return 0;
}
- 分数背包问题:场景等与上面问题一样,但是窃贼可以带走物品的一部分,而不必做出01的二分选择。可以把01背包问题的一件物品想像成一个金锭,而部分背包问题中的一件物品则更像金砂。
- 算法分析: 两种背包问题都具有最优子结构性质,但分数背包问题可用贪心策路来解决,而01背包问题却不行。为解决分数背包问题,先对每件物品计算其每磅的价值 pi/ wi。按照一种贪心策略,窃贼开始时对具有最大每磅价值的物品尽量多拿一些。如果他拿完了该物品而仍可以取一些其它物品时,他就再取具有次大的每磅价值的物品,一直继续下去,直到不能再取为止,这样,通过按每磅价值来对所有物品排序,贪心算法就可以O(n lgn)时间内运行。
//输入:bagSize 背包大小 map: first 单价 ,second 货物重量
int getMaxWeight(map<int,int> product,int bagSize){
if (bagSize <= 0)
return 0;
int value = 0;
int num = product.size();
auto end = product.end();
end--;
for (int i = num; i > 0;i--,end--){
if (bagSize >= (*end).second)
{
value += ((*end).second)*((*end).first);
bagSize -= (*end).second;
}
else{
value += ((*end).first)*bagSize;
return value;
}
}
}