贪心算法的理论基础
定义:
在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。
贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。
如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。
理论基础:
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,希望得到结果是最好或最优的算法。
贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。
这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。
利用贪心策略解题,需要解决两个问题:
(1)该题是否适合于用贪心策略求解;
(2)如何选择贪心标准,以得到问题的最优/较优解。
与动态规划的区别:
贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
(1)在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择。
(2)在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解出这个选择后产生的相应的子问题。
- 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。 - 贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。
- 贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。
- 动态规划主要运用于二维或三维问题,而贪心一般是一维问题。
贪心算法解题:
使用贪心算法求解问题应该考虑如下几个方面:
(1)候选集合A:
为了构造问题的解决方案,有一个候选集合A作为问题的可能解,即问题的最终解均取自于候选集合A。
(2)解集合S:
随着贪心选择的进行,解集合S不断扩展,直到构成满足问题的完整解。
(3)解决函数solution:
检查解集合S是否构成问题的完整解。
(4)选择函数select:
即贪心策略,这是贪心法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。
(5)可行函数feasible:
检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。
典型例题
一、活动安排问题
【问题】
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。
每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi。如果选择了活动i,则它在半开时间区间[si ,fi )内占用资源。若区间[si ,fi )与区间[sj,fj )不相交,则称活动i与活动j是相容的。当 si ≥ fj 或 sj ≥ fi 时,活动i与活动j相容。
活动安排问题就是在所给的活动集合中选出最大的相容活动子集合。
【分析】
将所有的活动按时间顺序排序,从前往后取。
【代码】
#include<bits/stdc++.h>
using namespace std;
struct node
{
int s; //起始时间
int f; //结束时间
int index; //活动的编号
}a[100010];
int n;
int ss,ee;
int cmp(node a,node b)
{
if(a.s!=b.s)
return a.s<b.s;
else
return a.f<b.f;
}
vector<int>v;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].s>>a[i].f;
a[i].index=i;
}
sort(a+1,a+1+n,cmp);
int tmp=1;
v.push_back(a[1].index);
for(int i=2;i<=n;i++)
{
if(a[i].s>=a[tmp].f)
{
v.push_back(a[i].index);
tmp=i;
}
}
for(int i=0;i<v.size();i++)
cout<<v[i]<<" ";
cout<<endl;
return 0;
}
二、背包问题
【问题】
给定一个载重量为M的背包,考虑n个物品,其中第i个物品的重量 ,价值wi (1≤i≤n),要求把物品装满背包,且使背包内的物品价值最大。
有两类背包问题(根据物品是否可以分割),如果物品不可以分割,称为0—1背包问题(动态规划);如果物品可以分割,则称为背包问题(贪心算法)。
【分析】
有3种方法来选取物品:
(1)当作0—1背包问题,用动态规划算法,获得最优值220;
(2)当作0—1背包问题,用贪心算法,按性价比从高到底顺序选取物品,获得最优值160。由于物品不可分割,剩下的空间白白浪费。
(3)当作背包问题,用贪心算法,按性价比从高到底的顺序选取物品,获得最优值240。由于物品可以分割,剩下的空间装入物品3的一部分,而获得了更好的性能。
【代码】
#include<bits/stdc++.h>
using namespace std;
struct node
{
double value,weight,tmp;
int index;
} a[100010];
int n;
double sum;
double ww;
int cmp(node a,node b)
{
return a.tmp>b.tmp;
}
vector<int>v;
int main()
{
cin>>n;
cin>>ww;
for(int i=1; i<=n; i++)
{
cin>>a[i].value>>a[i].weight;
a[i].tmp=a[i].value/a[i].weight;
a[i].index=i;
}
sort(a+1,a+1+n,cmp);
double x=0;
int cnt=1;
for(int i=1; i<=n; i++)
{
if(x<ww)
{
x+=a[i].weight;
sum+=a[i].value;
v.push_back(a[i].index);
cnt=i;
}
}
if(x>ww)
sum-=(x-ww)/a[cnt].weight*a[cnt].value;
cout<<sum<<endl;
return 0;
}
三、删数问题
【问题】
给定n位正整数a,去掉其中任意k≤n个数字后,剩下的数字按原次序排列组成一个新的正整数。对于给定的n位正整数a和正整数k,设计一个算法找出剩下数字组成的新数最小的删数方案。
输入
第1行是1个正整数a,第2行是正整数k。
输出
对于给定的正整数a,编程计算删去k个数字后得到的最小数。
【样例】
输入样例:
178543 4
输出样例:
13
【分析】
【代码】
#include<bits/stdc++.h>
using namespace std;
string s;
int k;
int main()
{
cin>>s>>k;
if (k >= s.size())
s.erase();
else
{
while(k > 0)
{
//寻找最近下降点
int i=0;
for(i=0; (i<s.size()-1)&&(s[i]<=s[i+1]); i++);
s.erase(i, 1);
k--;
}
}
//删除前导数字0
while(s.size()>1&&s[0]=='0')
s.erase(0, 1);
cout<<s<<endl;
return 0;
}
四、多处最优服务次序问题
【问题】
设有n个顾客同时等待一项服务,顾客i需要的服务时间为ti,1≤i≤n,共有s处可以提供此项服务。应如何安排n个顾客的服务次序才能使平均等待时间达到最小?平均等待时间是n个顾客等待服务时间的总和除以n。
给定的n个顾客需要的服务时间和s的值,编程计算最优服务次序。
输入
第一行有2个正整数n和s,表示有n个顾客且有s处可以提供顾客需要的服务。接下来的1行中,有n个正整数,表示n个顾客需要的服务时间。
输出
最小平均等待时间,输出保留3位小数。
【样例】
输入样例
10 2
56 12 1 99 1000 234 33 55 99 812
输出样例
336.000
【分析】
【代码】
#include<bits/stdc++.h>
using namespace std;
int n,m;
int service[100010];
int client[100010];
int sum[100010];
int main()
{
cin>>n>>m;
for(int i=0; i<n; i++)
cin>>client[i];
sort(client,client+n);
int i=0; //顾客的指针
int j=0; //窗口的指针
while(i<n)
{
service[j]+=client[i];
sum[j]+=service[j];
++i,++j;
if(j==m)
j=0;
}
//计算所有窗口服务时间的总和
double t=0;
for(i=0;i<m;++i)
t+=sum[i];
t/=n;
cout<<t<<endl;
return 0;
}
五、ZOJ1025-Wooden Sticks(木棒问题)
【问题】
现有n根木棒,已知它们的长度和重量。要用一部木工机一根一根地加工这些木棒。该机器在加工过程中需要一定的准备时间,是用于清洗机器,调整工具和模版的。
木工机需要的准备时间如下:
(1)第一根木棒需要1min的准备时间;
(2)在加工了一根长为l ,重为w的木棒之后,接着加工一根长为l ’ (l ≤ l’ ),重为 w’ ( w≤w’)的木棒是不需要任何准备时间的,否则需要一分钟的准备时间。
给定n根木棒,找到最少的准备时间。
例如现在有长和重分别为(4,9),(5,2),(2,1),(3,5)和(1,4)的五根木棒,那么所需准备时间最少为2min,顺序为(1,4),(3,5),(4,9),(2,1),(5,2)。
【样例】
输入样例
3
5
4 9 5 2 2 1 3 5 1 4
3
2 2 1 1 2 2
3
1 3 2 2 3 1
输出样例
2
1
3
【分析】
(1)本题仅仅使用贪心算法是不够的,排序之后还要使用动态规划的算法。
(2)按木棒的长度使用贪心算法:利用C++的标准模板库函数sort()实现排序
(3)使用动态规划的方法,计算重量w的最长单调递增子序列的个数。用数组b记录重量w的分组序号。
【代码】
#include<bits/stdc++.h>
using namespace std;
#define maxN 5001
struct stick
{
int l; //木棒的长度
int w; //木棒的重量
};
int n;
stick a[maxN];
stick data[maxN]; //存放所有木棒
int b[maxN]; //数组b表示木棒分组的序号
int cmp(stick a, stick b)
{
//优先按长度排序
if(a.l!=b.l)
return a.l<b.l;
//长度相等时,按重量排序
if (a.l = b.l)
return a.w < b.w;
}
int main()
{
int t;
cin>>t;
while(t--)
{
cin>>n;
for(int i=0; i<n; i++)
cin>>a[i].l>>a[i].w;
sort(a,a+n,cmp);
memset(b,0,sizeof(b));
int i,j,k;
b[0]=1;
for(i=1; i<n; i++)
{
//计算第i个木棒的的分组序号
k=0;
for(j=0; j<i; j++)
if(a[i].w<a[j].w&&k<b[j])
k=b[j];
b[i]=k+1;
}
//查找最大的分组序号(数组b中的最大值)
int maxx=0;
for(i=0; i<n; i++)
if(b[i]>maxx)
maxx=b[i];
cout<<maxx<<endl;
}
return 0;
}
六、ZOJ1161-Gone Fishing(钓鱼问题)
【问题】
约翰有h(1≤h≤16)个小时的时间,在该地区有n(2≤n≤25)个湖,这些湖刚好分布在一条路线上,该路线是单向的。约翰从湖1出发,他可以在任一个湖结束钓鱼。但他只能从一个湖到达另一个与之相邻的湖,而且不必每个湖都停留。
假设湖i(i=1~n—1),以5分钟为单位,从湖i到湖i+1需要的时间用ti(0<ti≤192)表示。例如t3=4,是指从湖3到湖4需要花20分钟时间。
已知在最初5分钟,湖i预计钓到鱼的数量为fi(fi≥0)。以后每隔5分钟,预计钓到鱼的数量将以常数di(di≥0)递减。如果某个时段预计钓到鱼的数量小于或等于di,那么在下一时段将钓不到鱼。为简单起见,假设没有其它的钓鱼者影响约翰的钓鱼数量。
编写程序,帮助约翰制定钓鱼旅行的计划,以便尽可能多的钓到鱼。
【样例】
【输入】
对每组测试例,第一行是n,接下来一行是h。下面一行是n个整数fi(1≤i≤n),然后是一行n个整数di(1≤i≤n),最后一行是n—1个整数ti(1≤i≤n—1)。
【输出】
对每个测试例,输出在每个湖上花费的时间,这是约翰要实现钓到最多的鱼的计划(必须使整个计划在同一行输出)。
接下来一行是钓到的鱼的数量:
如果存在很多方案,尽可能选择在湖1钓鱼所耗费的时间,即使有些时段没有钓到鱼;如果还是无法区分,那就尽可能选择在湖2钓鱼所耗费的时间,以此类推。
input:
2
1
10 1
2 5
2
4
4
10 15 20 17
0 3 4 3
1 2 3
4
4
10 15 50 30
0 3 4 3
1 2 3
0
output
45, 5
Number of fish expected: 31
240, 0, 0, 0
Number of fish expected: 480
115, 10, 50, 35
Number of fish expected: 724
【分析】
(1)数据结构
每个湖预计钓到鱼的数量,定义为数组:
#define NUM 30
int f[NUM];
每个湖预计钓到鱼的数量的递减值,定义为数组:
int d[NUM];
相邻湖之间的旅行时间,定义为数组:
int t[NUM];
钓鱼计划,定义为数组:
int plan[NUM];
湖的个数n,用于钓鱼的时间h,尽可能多的钓鱼数量best。
(2)搜索,在任意一个湖结束钓鱼时的最优钓鱼计划
首先把用于钓鱼的时间h,由小时转换为以5分钟为单位的时间:h=h×60/5;
这样把钓5分钟鱼的时间称为钓一次鱼。由于约翰从湖1出发,可以在任一个湖结束钓鱼,要得到最优解,就需要进行搜索。
(3)采用贪心策略,每次选择鱼最多的湖钓一次鱼
对于每个湖来说,由于在任何时候鱼的数目只和约翰在该湖里钓鱼的次数有关,和钓鱼的总次数无关,所以这个策略是最优的。一共可以钓鱼time次,每次在n个湖中选择鱼最多的一个湖钓鱼。
采用贪心算法构造约翰的钓鱼计划。
可以认为约翰能从一个湖“瞬间转移”到另一个湖,即在任意一个时刻都可以从湖1到湖pos中任选一个钓一次鱼。
【代码】
#include<bits/stdc++.h>
using namespace std;
#define MAXN 30
//每个湖预计钓到鱼的数量,定义为数组:
int f[MAXN];
//每个湖预计钓到鱼的数量的递减值,定义为数组:
int d[MAXN];
//相邻湖之间的旅行时间,定义为数组:
int t[MAXN];
//钓鱼计划,定义为数组:
int plan[MAXN];
//湖的个数n,用于钓鱼的时间h,尽可能多的钓鱼数量best。
int n,h,best;
//选择鱼最多的湖钓鱼的贪心算法实现
//从湖1起到湖pos止,花费时间time(不含路程)的钓鱼计划
void greedy(int pos, int time)
{
if (time <= 0)
return;//时间已经用完
int i, j;
int fish[MAXN];
int p[MAXN];
int t = 0;
for (i = 0; i < pos; ++i)
fish[i] = f[i];
memset(p, 0, sizeof(p));
//在时间time内,选择鱼最多的湖钓鱼;如果鱼都没有了,就把时间放在湖1上
for (i = 0; i < time; ++i)
{
int max = 0; //鱼最多的湖中,鱼的数量
int id = -1;//鱼最多的湖的编号
//查找鱼最多的湖中,鱼的数量和湖的编号
for (j = 0; j < pos; ++j)
if (fish[j] > max)
{
max = fish[j];
id = j;
}
if (id != -1)//找到了,进行钓鱼处理
{
++p[id];
fish[id] -= d[id];
t += max;
}
//没有找到(从湖1起到湖pos全部钓完了),就把时间放在湖1上
else
++p[0];
}
//处理最优方案
if (t > best)
{
best = t;//最优值
memset(plan, 0, sizeof(plan));
for (i = 0; i < pos; ++i)//最优解
plan[i] = p[i];
}
}
int main()
{
while(scanf("%d",&n),n)
{
cin>>h;//钓鱼时间
//每个湖初始钓鱼数量
for(int j=0; j<n; j++)
cin>>f[j];
//每个湖的递减值
for(int j=0; j<n; j++)
cin>>d[j];
//两个湖之间的时间
for(int j=1; j<n; j++)
cin>>t[j];
int time=0;//花费在路程上的时间
h=h*60/5;
best = -1;
for(int k=1; k<=n&&h-time; ++k)
{
greedy(k,h-time);
time+=t[k];
}
for (int i = 0; i < n - 1; ++i)
cout<<plan[i]*5<<" ";
cout<<plan[n-1]*5<<endl;
cout<<"Number of fish expected: "<<best<<endl;
}
return 0;
}