贪心算法
1、基本思想
1.1、概述
我们知道求解最优化问题的算法一般包括一系列的求解步骤,每一步骤都要在许多选择里面做一个决策,然后才能得到问题的一个解。
贪心算法(Greedy Algorithm)总是在每一步骤中做出最优的决策,希望通过一系列的局部最优决策,从而获得问题的全局最优解。
日常例子如:打牌、股票投资、安排课程如何安排使得资源利用率最大化等。
如图下的最短路径问题,两幅图表示了前一步的选择与后一步是否独立
1.2、活动选择问题
动态规划法实现
1、问题描述:
输入:任务的个数、每个任务的起始终止时间
分析:
递归:
#include<iostream>
#include<cstdio>
using namespace std;
int c[100][100]={0};//c[i][j]
int Maxactivities_dp(int i,int j,int (*a)[2]){
if(i>=j){
c[i][j]=0;
return 0;
}
int m=0;
for(int k=i+1;k<j;k++){
if(a[i][1]<=a[k][0]&&a[k][1]<=a[j][0]){//满足就计算是否为最大值
c[i][k]=Maxactivities_dp(i,k,a);
c[k][j]=Maxactivities_dp(k,j,a);
int temp = c[i][k]+c[k][j]+1;
if(temp>m)
m = temp;
}
if(c[i][j]<m)
c[i][j]=m;
}
return m;
}
//打印选择的活动,利用了递推公式
void PrintSelectActivitise(int i,int j,int (*a)[2]){
if(j-i==1)//没有活动
return;
for(int k=i+1;k<j;k++){
if(a[i][1]<=a[k][0]&&a[k][1]<=a[j][0]){
if(c[i][k]+c[k][j]+1==c[i][j])//满足递推关系则表示选择最优活动是ak
PrintSelectActivitise(i,k,a);
cout<<"A"<<k<<" ";
PrintSelectActivitise(k,j,a);
break;//注意一次选择里面只选一个k
}
}
}
int main(){
int n;
cout<<"活动个数:"<<endl;
cin>>n;
cout<<"请输入"<<n<<"个活动(如3 5表示3点到5点活动):"<<endl;
int a[n+2][2];
a[0][0]=-1;
a[0][1]=0;//假想的活动
for(int i=1;i<n+1;i++){
cin>>a[i][0]>>a[i][1];
}
a[n+1][0]=25;
a[n+1][1]=26; //假想的活动
//对活动的结束时间进行升序排序
for(int i=1;i<n+2;i++){//冒泡排序
for(int j=0;j<n+2-i;j++)
if(a[j][1]>a[j+1][1]){
int temp = a[j+1][1];
a[j+1][1]=a[j][1];
a[j][1]=temp;
int temp1 = a[j+1][0];
a[j+1][0] = a[j][0];
a[j][0]= temp1;
}
}
cout<<endl<<Maxactivities_dp(0,n+1,a)<<endl;
PrintSelectActivitise(0,n+1,a);
return 0;
}
迭代:
#include<iostream>
#include<cstdio>
using namespace std;
int c[100][100]={0};//直接所有都初始化为0
int Maxactivities_dp(int start,int end,int(*a)[2]){
if(start>=end||end-start==1)
return 0;
//for(int m=0;m<end;m++){//初始化边界条件
// c[m][m]=0;//因为活动按照结束时间排序的
// c[m][m+1]=0;
//}
for(int i=start;i<end;i++){
for(int j=i+2;j<end+1;j++){
int max=0;
for(int k=i+1;k<j;k++){
if(a[k][0]>=a[i][1]&&a[k][1]<=a[j][0]){
int temp = c[i][k]+c[k][j]+1;
if(max<temp)
max = temp;
}
}
if(c[i][j]<max)
c[i][j]=max;
}
}
return c[start][end];
}
贪心算法实现
贪心算法分析:
从上面的分析,我们有:
这个就是贪心策略。代码实现:
递归:
#include<iostream>
using namespace std;
int record[100]={0};//记录选择的活动
int Maxactivities_greedy(int start,int end,int (*a)[2],int *record){
int m=start+1;
while(m<end&&a[m][0]<a[start][1])
m = m+1;//寻找当前结束时间最小的am,
if(m<end){
record[m]=1;
return 1+Maxactivities_greedy(m,end,a,record);
}
else return 0;
}
迭代法:
//迭代法
int Maxactivities_greedy(int start,int end,int(*a)[2],int *record){
int i=start+1;//第一个活动经过排序必然是最快结束的
record[i] = 1;//记录
int max=1;
for(int m=i+1;m<end;m++){
if(a[i][1]<=a[m][0]){
max +=1;
record[m]=1;//记录
i = m;
}
}
return max;
}
体会贪心算法的设计步骤
步骤:
贪心选择:
2、实战例子
2.1、背包问题(0-1背包、分数背包)
:0-1背包问题无法用贪心算法解决,可以用动态规划实现;分数背包问题可以。
分数背包问题:
假设输入形式:输入物品个数 n,背包容量C,输入物品的序号,重量,价值。
如:
3 4
1 1 2
2 2 5
3 4 3
输出
选择:1,2,总价值:7
贪心算法最重要的是选择正确的贪心策略。在这个问题里,如果我们用贪心算法,明显我们可以想到的贪心策略有如下三种:
- 1、每次选择价值最大的物品,这可以尽可能快的增加背包的总价值,但是,虽然每一步选择获得了背包价值的极大增长,但背包容量却可能消耗的太快,使得装入背包的物品个数减少,从而不能保证目标函数达到最大。
- 2、每次选择重量最轻的物品,因为这可以装入尽可能多的物品,从而增加背包的总价值。但是,虽然每一步选择使背包的容量消耗的慢了,但背包的价值却没能保证迅速的增长,从而不能保证目标函数达到最大。
- 3、以上两种贪心策略或者只考虑背包价值的增长,或者只考虑背包容量的消耗,而为了求得背包问题的最优解,需要在背包价值增长和背包容量消耗二者之间寻找平衡。
正确的贪心策略是选择单位重量价值最大的物品。
知道了贪心策略,我们就可以设计算法了:
//n物品个数;w背包总容量;a2维数组,存储物品编号、物品重量、物品价值(按照物品的价值率降序排序)
void SelectMaxValue(int n,int W,int (*a)[3],int *record){
float value = 0;
int w;//w为实际装多少。
for(int i=0;i<n;i++){
if(W<a[i][1])//最大价值率物品
w=W;
else
w=a[i][1];
record[i]=a[i][0];//记录选择的物品编号
value+=a[i][2]*1.0/a[i][1]*w;
W-=w;
if(W==0)//装满了
break;
}
cout<<"选择物品编号:";
for(int i=0;i<n;i++){
if(record[i]!=0)
cout<<record[i]<<" ";
}
cout<<endl<<"最大价值:"<<value;
}
因为这里可以选择装一部分,也就是我们的第三种贪心算法可以保证装满背包。
0-1背包问题
因为0-1背包问题要么装全部,要么一点不装的限制。我们的三种贪心算法都无法保证最后的结果是最优的。
- 第一种贪心,肯定不正确
- 第二种贪心,肯定不正确
- 第三种贪心,因无法保证背包被装满而导致单位背包空间利用率下降而不正确。
我们用动态规划来解决0-1背包问题:
#include<iostream>
using namespace std;
n物品个数;V背包容量;a2维数组,存储物品编号、物品重量、物品价值
void SelectMaxValue(int n,int V,int(*a)[3],int (*f)[100]){
for(int i=1;i<=n;i++){
for(int j=1;j<=V;j++){
if(j<a[i-1][1])//装不下第i件
f[i][j]=f[i-1][j];
else{//装得下第i件,不代表一定要装才价值大
int temp1 = f[i-1][j];//不装
int temp2 = f[i-1][j-a[i-1][1]]+a[i-1][2];//装
if(temp1>temp2)
f[i][j]=temp1;
else{
f[i][j]=temp2;
}
}
}
}
cout<<"选取的编号是:";
for(int i=n,j=V;i>0;i--){
if(f[i][j]==f[i-1][j-a[i-1][1]]+a[i-1][2]){
cout<<i<<" ";
j-=a[i-1][1];
}
}
cout << endl << "选取的最大价值是:" << f[n][V] << endl;
}
int main(){
int n,W;
cout<<"输入物品个数、背包总容量:"<<endl;
cin>>n>>W;
int a[n][3];
int f[100][100]={0};//默认
cout<<"输入物品编号、物品重量、物品价值:"<<endl;
for(int i=0;i<n;i++){
cin>>a[i][0]>>a[i][1]>>a[i][2];
}
SelectMaxValue(n,W,a,f);
return 0;
}
特殊的0-1背包问题
可用贪心法求解,因为其价值率排行是有规则的。
2.2、哈夫曼编码
参考代码实现:哈夫曼树与哈夫曼编码及代码实现