贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
一、贪婪算法可解决的问题通常大部分都有如下的特性:
随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
最后,目标函数给出解的值。
为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。
二、我的第一感受
然后关于贪心,最直观的感受就是选取东西,比如常见的背包:
物品 A B C D E F G
重量 35kg 30kg 6kg 50kg 40kg 10kg 25kg
价值 10 40 30 50 35 40 30
那么我们肯定有三种方法:
⑴每次挑选价值最大的物品装入背包
⑵每次挑选所占重量最小的物品
⑶每次选取单位重量价值最大的物品
要是所有的东西都能任意分割,那么肯定是第三种能够解决问题,可是并不能,所以这三种都不能解决问题。所以一度我把动态规划dp问题和贪心一直搞不清楚。
事实上这个问题根本贪心根本解决不了,太贪了。然后贪心不像其他的常用算法一样有一个伪代码或者一个可以套的公式这根本就是一个思想。。。。。
四、贪心算法的实现框架
从问题的某一初始解出发;
while (能朝给定总目标前进一步)
{
利用可行的决策,求出可行解的一个解元素;
}
由所有解元素组合成问题的一个可行解;
五、贪心策略的选择
因为用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。
六、经典例题
1:活动时间安排的问题
设有N个活动时间集合,每个活动都要使用同一个资源,比如说会议场,而且同一时间内只能有一个活动使用,每个活动都有一个使用活动的开始si和结束时间fi,即他的使用区间为(si,fi),现在要求你分配活动占用时间表,即哪些活动占用该会议室,哪些不占用,使得他们不冲突,要求是尽可能多的使参加的活动最大化,即所占时间区间最大化!
![]()
上图为每个活动的开始和结束时间,我们的任务就是设计程序输出哪些活动可以占用会议室!
#include <iostream>
using namespace std;
void GreedyChoose(int len,int *s,int *f,bool *flag);
int main(int argc, char* argv[])
{
//s数组保存开始时间,f数组保存结束时间
int s[11] ={1,3,0,5,3,5,6,8,8,2,12};
int f[11] ={4,5,6,7,8,9,10,11,12,13,14};
bool mark[11] = {0};
GreedyChoose(11,s,f,mark);
for(int i=0;i<11;i++)
if(mark[i])
cout<<i<<" ";
system("pause");
return 0;
}
void GreedyChoose(int len,int *s,int *f,bool *flag)
{
flag[0] = true;
int j = 0;
for(int i=1;i<len;++i)
if(s[i] >= f[j])
{
flag[i] = true;
j = i;
}
}
其实我很难想通为啥这样安排就是最高效的利用了,确实前一项结束来选择最近开始的那一个去继续执行,我也找不出就是那种前一项结束了,最近的后一项不开始,选更后面的几项再开始的情况。然后就是题目的要求,我是要求参加的活动数量最多,还是总的占用时间最多,我觉得这两个可能情况不一样。每一个活动时间的挑选总是选择最优的,就是刚好匹配的,这样得出的结果也就是最优的了,这样理解似乎也讲得通。。。。。也许是我数学底子不好吧。反正我想说这肯定是浪费的时间最少的解法。
2.贪心实例之线段覆盖(lines cover)
题目大意:
在一维空间中告诉你N条线段的起始坐标与终止坐标,要求求出这些线段一共覆盖了多大的长度。
为了方便说明,我们采用上述表格中的数据代表10条线段的起始点和终点,注意,这里是用起始点为顺序进行排列,和上面的不一样,知道了这些我们就可以着手开始设计这个程序:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int s[10] = {2,3,4,5,6,7,8,9,10,11};
int f[10] = {3,5,7,6,9,8,12,10,13,15};
int TotalLength = f[0]-s[0];
for(int i=1,int j=0; i<10 ; ++i)
{
//如果下一个线段的开始在前一个线段结束后面
if(s[i] >= f[j])
{
//总长就加上这个新的线段长度
TotalLength += (f[i]-s[i]);
j = i;
}
else
{
//否则下一个线段的开始在前一个线段结束前面,也就是有了重叠,两种情况,一是新的线段结束还在前一个线段的里面,那么无视,总长度没变,如果结束在之后,就只要补上出去的部分即可
if(f[i] <= f[j])
continue;
else
{
TotalLength += f[i] - f[j];
j = i;
}
}
}
cout<<TotalLength<<endl;
system("pause");
return 0;
}
3、找零钱问题
#include <iostream>
using namespace std;
#define NUM 8
static const int permoney[] = { 10000, 5000, 2000, 1000, 500, 100, 50, 10 };
void exchange(int* result,float money)
{
int mi = money * 100;
for (int i = 0; i < NUM; i++)
{ //从面值为100的算起
int n = mi/permoney[i]; //需要几张100的
mi = mi%permoney[i]; //还剩下多少钱要找
result[i] = n; //记录下需要几张100的,跳转到下一面值的钱
}
}
void main()
{
float money = 0;
while (cin>>money)
{
if (money == 0.0)
return;
int result[NUM];
exchange(result,money);
for (int i = 0; i < NUM; i++)
{
{
//如果当前需要的面值张数不为0,则输出
cout<<permoney[i]/100.0<<"->"<<result[i]<<endl;
}
}
}
}
实际上这只能给出一种找零钱的方法。。。。很多时候还是dp问题多一点。。。