贪心算法(又称贪婪算法)是指,在对 问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部 最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
贪心选择
贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解
最优子结构
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。
贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。 动态规划主要运用于二维或三维问题,而贪心一般是一维问题贪心算法几大经典问题:
1:活动时间安排的问题
这是《算法导论》上的例子,也是一个非常经典的问题。有n个需要在同一天使用同一个教室的活动a1,a2,…,an,教室同一时刻只能由一个活动使用。每个活动ai都有一个开始时间si和结束时间fi 。一旦被选择后,活动ai就占据半开时间区间[si,fi)。如果[si,fi]和[sj,fj]互不重叠,ai和aj两个活动就可以被安排在这一天。该问题就是要安排这些活动使得尽量多的活动能不冲突的举行。例如下图所示的活动集合S,其中各项活动按照结束时间单调递增排序。
考虑使用贪心算法的解法。为了方便,我们用不同颜色的线条代表每个活动,线条的长度就是活动所占据的时间段,蓝色的线条表示我们已经选择的活动;红色的线条表示我们没有选择的活动。
如果我们每次都选择开始时间最早的活动,不能得到最优解:
如果我们每次都选择持续时间最短的活动,不能得到最优解:
可以用数学归纳法证明,我们的贪心策略应该是每次选取结束时间最早的活动。直观上也很好理解,按这种方法选择相容活动为未安排活动留下尽可能多的时间。这也是把各项活动按照结束时间单调递增排序的原因。
- #include <iostream>
- using namespace std;
- void GreedyChoose(int len,int *s,int *f,bool *flag);
- int main(int argc, char* argv[])
- {
- 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;
- }
- }
得出结果是 0 3 7 10,也就是对应的时间段
值得说明一下,虽然贪心算法不是一定可以得到最好的解 ,但是对于这种活动时间的问题,他却得到的总是最优解,这点可以用数学归纳法证明,在这里,体现出来的贪心策略是:每一个活动时间的挑选总是选择最优的,就是刚好匹配的,这样得出的结果也就是最优的了!由于这个算法很简单,在这里就没有注释了!
类似这种题还有个区间覆盖问题,就是说很多个区间,其中有些是相互覆盖着的,要求去除多余的区间,使剩下的区间占用长度最大,实际就是这个题,只是问法变换了而已!接下来让我们看线性覆盖的问题,跟上面的相反!
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 = (3-2);
- 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;
- }
运行结果为13,显然这是我们需要的结果,这里注明一下,上面图表中数据有点问题,实际以程序中给出的为主!
3、数字组合问题!
设有N个正整数,现在需要你设计一个程序,使他们连接在一起成为最大的数字,例3个整数 12,456,342 很明显是45634212为最大,4个整数 342,45,7,98显然为98745342最大
程序要求:输入整数N 接下来一行输入N个数字,最后一行输出最大的那个数字!
题目解析:拿到这题目,看起要来也简单,看起来也难,简单在什么地方,简单在好像就是寻找哪个开头最大,然后连在一起就是了,难在如果N大了,假如几千几万,好像就不是那么回事了,要解答这个题目需要选对合适的贪心策略,并不是把数字由大排到小那么简单,网上的解法是将数字转化为字符串,比如a+b和b+a,用strcmp函数比较一下就知道谁大,也就知道了谁该排在谁前面,不过我觉得这个完全没必要,在这里我采用一种比较巧妙的方法来解答,不知道大家还记得冒泡排序法不,那是排序最早接触的一种方法,我们先看看它的源代码:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int array[10];
for(int i=0;i<10;i++)
cin>>array[i];
int temp;
for(i=0; i<=9 ; ++i)
for(int j=0;j<10-1-i;j++)
if(array[j] > array[j+1] )
{
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
for(i=0;i<10;i++)
cout<<array[i]<<" ";
cout<<endl;
system("pause");
return 0;
相信这种冒泡已经很熟悉了,注意看程序中最核心的比较规则是什么,是这一句if(array[j] > array[j+1] ) 他是以数字大小作为比较准则来返回true或者是false,那么我们完全可以改变一下这个排序准则,比如23,123,这两个数字,在我们这个题中它可以组成两个数字 23123和12323,分明是前者大些,所以我们可以说23排在123前面,也就是23的优先级比123大,123的优先级比23小,所以不妨写个函数,传递参数a和b,如果ab比ba大,则返回true,反之返回false,函数原型如下:
bool compare(int Num1,int Num2)
{
int count1,count2;
int MidNum1 = Num1,MidNum2 = Num2;
while( MidNum1 )
{
++count1;
MidNum1 /= 10;
}
while( MidNum2 )
{
++count2;
MidNum2 /= 10;
}
int a = Num1 * pow(10,count2) + Num2;
int b = Num2 * pow(10,count1) + Num1;
return (a>b)? true:false;
}
好了,我们的比较准则函数也已经完成了,只需要把这个比较准则加到关键的地方,这个题就算完成了,最终代码如下:
#include <iostream>
#include <cmath>
using namespace std;
bool compare(int Num1,int Num2);
int main(int argc, char* argv[])
{
int N;
cout<<"please enter the number n:"<<endl;
cin>>N;
int *array = new int [N];
for(int i=0;i<N;i++)
cin>>array[i];
int temp;
for(i=0; i<=N-1 ; ++i)
{
for(int j=0;j<N-i-1;j++)
if( compare(array[j],array[j+1]) )
{
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
cout<<"the max number is:";
for( i=N-1 ; i>=0 ; --i)
cout<<array[i];
cout<<endl;
delete [] array;
system("pause");
return 0;
}
bool compare(int Num1,int Num2)
{
int count1=0,count2=0;
int MidNum1 = Num1,MidNum2 = Num2;
while( MidNum1 )
{
++count1;
MidNum1 /= 10;
}
while( MidNum2 )
{
++count2;
MidNum2 /= 10;
}
int a = Num1 * pow(10,count2) + Num2;
int b = Num2 * pow(10,count1) + Num1;
return (a>b)? true:false;
}
可以看见这样很巧妙的改编冒泡排序就解决了这个问题,当然也可以用其他的排序算法,或者干脆用一个仿函数作为set容器的排序准则,insert进去就可以了,但是这个程序有一点小问题,假如输入的数字中有两个濒临越界的数据,合并在一起就越界了,那样就只能用字符串的形式进行比较!
在贪心算法里面最常见的莫过于找零钱的问题了,题目大意如下,对于人民币的面值有1元 5元 10元 20元 50元 100元,下面要求设计一个程序,输入找零的钱,输出找钱方案中最少张数的方案,比如123元,最少是1张100的,1张20的,3张1元的,一共5张!
解析:这样的题目运用的贪心策略是每次选择最大的钱,如果最后超过了,再选择次大的面值,然后次次大的面值,一直到最后与找的钱相等,这种情况大家再熟悉不过了,下面就直接看源代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=7;
int Count[N]={3,0,2,1,0,3,5};
int Value[N]={1,2,5,10,20,50,100};
int solve(int money)
{
int num=0;
for(int i=N-1;i>=0;i--)
{
int c=min(money/Value[i],Count[i]);
money=money-c*Value[i];
num+=c;
}
if(money>0) num=-1; //说明找不开钱
return num;
}
int main()
{
int money;
cin>>money;
int res=solve(money);
if(res!=-1) cout<<res<<endl;
else cout<<"NO"<<endl;
}
5、买卖股票的最佳时机(求数组中的最大子数组)(见博客)