贪心算法
前言
- 贪心算法总是做出在当前看来是最好的选择。
- 也就是说,贪心算法并不是从整体最优上加以考虑,它所做出的选择只是某种意义上的局部最优选择。
- 当然,我们希望贪心算法得到的最终结果也是整体最优的。
- 具有最优子结构性质的问题,可以用动态规划算法来解决,但是用贪心算法更简单,更直接且解题效率更高。
- 总结:问题逐步求解时,每一步做最优选择。
- 即使贪心算法不能得到整体最优解,但其最终结果却是最优解的很好的近似解。
一、活动安排问题
1.问题描述
该问题要求高效地安排一系列争用某一个公共资源的活动。
有𝒏个活动,开始和结束的时间𝒔_𝒊≤𝒇_𝒊,如何安排最多个活动?
贪心策略:按结束时间递增排序,从前向后能安排则安排
2.例 当前有4个活动
3.程序实现
程序4_1
//4_1 P91
template <class Type>//模板类
void GreedySelector(int n, Type s[],Type f[],bool A[])//n活动个数,s开始时间,f结束时间,A活动是否被选中,运行前已对f进行排序
{
A[1]=true;//选入第1个活动,是必选
int j=1;//j记入选入活动的编号,初始值1
for(int i=2;i<=n;i++)//依次检查第2到n个活动
{
if(s[i]>=f[j])//后一个活动的开始时间晚于前一个活动的结束时间,则选入
{
A[i]=true;
j=i;//j记入选入活动的编号
}
else
A[i]=false;//i开始在j结束前,放弃
}
}
推广
贪心策略思路1:
先在第1会场安排最多活动,其次在第2会场安排最多,……,依次类推。
程序见4_2.cpp
考察运行结果
程序4_2
//会场安排问题推广
#include <iostream.h>
template <class T>
bool GS1(int n,T s[],T f[],int A[],int k)//有n个活动,s[]活动开始时间,f[]活动结束时间,A[]记录活动被分配的会场
//A[i]=k表示活动i分配到会场k;分配结束返回false,否则返回true,继续
//bool GS1(int n,int s[],int f[],int A[],int k)
{
int j=1;//j记录活动
while(A[j])//A[]记录活动被分配的会场,初始均为0,若非0则表明已分配到会场,考察下一个
j++;
if(j>n)
return false;
else
A[j]=k;//若j>n表示n个活动都分配了会场,否则为活动j分配会场
for(int i=j+1;i<=n;i++)
{
if(!A[i])//若活动i没有被分配会场
{
if(s[i]>=f[j])//若活动i的开始时间晚于活动j的结束时间,则与活动i安排在同一会场,按贪心策略选入
{
//cout<<"s["<<i<<"]"<<">="<<"f["<<j<<"]"<<"成立"<<endl;
A[i]=k;
j=i;
}
}
}
/*for(int m=1;m<=n;m++)
cout<<"A["<<m<<"]="<<A[m]<<", ";
cout<<endl;*/
return true;
}
void main()
{
int a[]={0,1,3,0,5};//开始时间,前补0,为使第1个活动的下标为1
int b[]={0,4,5,6,7};//结束时间
int c[5]={0};//记录活动被分配的会场 考虑个数要不要修改?
bool sg=true;
int k=1;//从第1个会场开始分配
while(sg)
sg=GS1(4,a,b,c,k++);//n=4,s=a;f=b;A=c;k会场
for(int i=1;i<=4;i++)
cout<<"活动"<<i<<"=会场"
<<c[i]<<endl;
}
贪心策略思路2:
视为区间重叠问题
时间点:起点和终点
按时间点排序
起点分配1个会场,终点回收,保证此会场可重复利用
程序见4_3.cpp
程序4_3
//推广—思路2
#include <iostream.h>
void main()
{
int a[]={0,0,1,3,4,5,5,5,7};//时间点
int b[]={0,0,0,0,1,1,0,1,1};//记起终点,起点记0,终点记1,起终点重叠时,优先记终点
int c[]={0,3,1,2,1,2,4,3,4};//记活动编号
int i,n=4;
int *A=new int[n+1];//记A[i]=k表示第i个活动分配在会场k
int k=0;//记会场数量
int kmax=0;//记最多会场数
for(i=1;i<=2*n;i++)//求所需最多会场数量
{
if(b[i])
k--;//终点回收1个
else
{
k++;//起点分配1个
if(k>kmax)
kmax=k;
}
}
int sm=kmax;//类似栈指针的用途
int *m=new int[sm+1];
for(i=1;i<=kmax;i++)
m[i]=kmax-i+1;//m理解为实现一个栈,存可用会场编号
for(i=1;i<=2*n;i++)//检查时间序列
{
if(b[i])
m[++sm]=A[c[i]];//终点回收1个会场,sm加1
else
A[c[i]]=m[sm--];//起点分配会场,sm减1
}
for(i=1;i<=n;i++)//输出
cout<<"第"<<i<<"活动安排在会场"<<A[i]<<endl;
}
二、贪心算法的基本要素
- 贪心算法是通过一系列的选择来得到问题的解。它所做的每一个选择都是当前状态下局部最好选择,即贪心选择。这种启发式的策略并不总能奏效,然而在许多情况下能达到预期的目的。
- 对于一个具体问题,是否可用贪心算法来解决,取决于问题是否具有以下两个重要的性质:
1、贪心选择性质
2、最优子结构性质
1、贪心选择性质
所谓贪心选择性质是指所求问题的整体最优解是可以通过局部最优的选择,即贪心选择达到的。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
在动态规划算法中,每步所做的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能做出选择。
而在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择。然后再去解做出这个选择后产生的相应子问题。
贪心算法所做的贪心选择可以依赖于以往所做过的选择,但决不能依赖于将来所做的选择,也不依赖于子问题的解。
正是由于这种差别,动态规划算法通常是以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式做出相继的贪心选择,每做一次贪心选择就将所求问题简化为规模更小的子问题。
需要注意的是,每步选择局部最优能保证整体最优,需证明此性质。
2、最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
3、贪心算法和动态规划算法的差异
贪心算法:
先依据贪心策略选择并解一个子问题,再进行下一步。
动态规划算法:
先解决所有的子问题,比较得出一个最优子问题,再进行下一步。
4.物品不可分——0-1背包问题
(1)最优子结构性质:
若(𝒚_𝟏,𝒚_𝟐,⋯,𝒚_𝒏)是最优解,则(𝒚_𝟐,⋯,𝒚_𝒏)是子问题的最优解
(2)递归关系
𝒎(𝒊,𝒋)记背包容量𝒋,可选物品为𝒊,𝒊+𝟏,⋯,𝒏时,0-1背包问题的最优值
𝒎(𝒊,𝒋)
3.计算最优值
(3)计算最优值
程序实现:P72-73
改:
void knapsack(Type *v, int *w, int c, int n, Type **m)
思路:
step1:实现递归出口,填矩阵𝒎的第𝒏行(前3行)
step2:循环,实现递归式,填矩阵𝒎的第(𝒏−𝟏)行到第2行
step3:填𝒎[𝟏][𝒄]
构造最优解
//构造最优解
template <class Type>
void Traceback(Type **m, int *w, int c, int n, int *x)//m表已经填好,已知w、c、n,输出x[],0不装,1装入
{
for(int i=1;i<n;i++)
{
if(m[i][c]==m[i+1][c])
x[i]=0;//相同则不装
else
{
x[i]=1;
c-=w[i];
}
}
x[n]=(m[n][c])?1:0;
}
5.物品可分——0-1背包问题
//物品可分:贪心算法
#include <iostream>
#include <iomanip>
using namespace std;
void Knapsack(int n, int c, int*v, int *w, double *x);
int main()
{
int n=3;//物品数量
int c=50;//背包容量
int *v=new int[n+1];//价值
int *w=new int[n+1];//重量
int i;
double *x=new double[n+1];
for(i=1;i<=n;i++)
x[i]=0;
//将数据按贪心策略排序
v[1]=60;v[2]=100;v[3]=120;
w[1]=10;w[2]=20;w[3]=30;
Knapsack(n,c,v,w,x);
for(i=1;i<=n;i++)
{
if(x[i]>0)
cout<<"物品"<<i<<"装入"<<setiosflags(ios::fixed)<<setprecision(2)<<x[i]<<endl;
else
cout<<"物品"<<i<<"不装入"<<endl;
}
return 0;
}
void Knapsack(int n, int c, int*v, int *w, double *x)
{
int i;
for(i=1;i<=n;i++)
{
if(w[i]>c)
break;
else
{
x[i]=1;
c=c-w[i];
}
}
if(i<=n)
x[i]=(double)c/w[i];
}