算法分析与设计6_贪心

贪心

在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。
从贪心算法的定义可以看出,贪心算法不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。
如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。

活动安排问题

活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合,是可以用贪心算法有效求解的很好例子。
该问题要求高效地安排一系列争用某一公共资源的活动。
贪心算法提供了一个简单、漂亮的方法使得尽可能多的活动能兼容地使用公共资源。
设有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相容。
活动安排问题就是在所给的活动集合中选出最大的相容活动子集合。

struct action{
	int s;			//起始时间
	int f;			//结束时间
	int index;		//活动的编号
};
bool cmp(const action &a, const action &b)
{
	if (a.f<=b.f) return true;
	return false;
}
//形参数组b用来记录被选中的活动
void GreedySelector(int n, action a[], bool b[])
{
  b[1] = true;     //第1个活动是必选的
  //记录最近一次加入到集合b中的活动
  int preEnd = 1;
  for(int i=2; i<=n; i++)
    if (a[i].s>=a[preEnd].f)
    {
      b[i] = true;
      preEnd = i;
    }
}
贪心算法的理论基础

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,希望得到结果是最好或最优的算法。
贪心算法是一种能够得到某种度量意义下的最优解的分级处理方法,通过一系列的选择得到一个问题的解,而它所做的每一次选择都是当前状态下某种意义的最好选择。即希望通过问题的局部最优解求出整个问题的最优解。
这种策略是一种很简洁的方法,对许多问题它能产生整体最优解,但不能保证总是有效,因为它不是对所有问题都能得到整体最优解。
利用贪心策略解题,需要解决两个问题:
(1)该题是否适合于用贪心策略求解;
(2)如何选择贪心标准,以得到问题的最优/较优解。

贪心选择性质

贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。
这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
(1)在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择。
(2)在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解出这个选择后产生的相应的子问题。

最优子结构性质

当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。
贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。
贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。
动态规划主要运用于二维或三维问题,而贪心一般是一维问题。

贪心算法的求解过程

使用贪心算法求解问题应该考虑如下几个方面:
(1)候选集合A:为了构造问题的解决方案,有一个候选集合A作为问题的可能解,即问题的最终解均取自于候选集合A。
(2)解集合S:随着贪心选择的进行,解集合S不断扩展,直到构成满足问题的完整解。
(3)解决函数solution:检查解集合S是否构成问题的完整解。
(4)选择函数select:即贪心策略,这是贪心法的关键,它指出哪个候选对象最有希望构成问题的解,选择函数通常和目标函数有关。
(5)可行函数feasible:检查解集合中加入一个候选对象是否可行,即解集合扩展后是否满足约束条件。

贪心算法的一般流程
//A是问题的输入集合即候选集合
Greedy(A)
{
  S={ };           //初始解集合为空集
  while (not solution(S))  //集合S没有构成问题的一个解
  {
    x = select(A);     //在候选集合A中做贪心选择
    if feasible(S, x)    //判断集合S中加入x后的解是否可行
      S = S+{x};
      A = A-{x};
  }
  return S;

(1)候选集合A:问题的最终解均取自于候选集合A。
(2)解集合S:解集合S不断扩展,直到构成满足问题的完整解。
(3)解决函数solution:检查解集合S是否构成问题的完整解。
(4)选择函数select:贪心策略,这是贪心算法的关键。
(5)可行函数feasible:解集合扩展后是否满足约束条件。

背包问题

给定一个载重量为M的背包,考虑n个物品,其中第i个物品的重量 ,价值wi (1≤i≤n),要求把物品装满背包,且使背包内的物品价值最大。
有两类背包问题(根据物品是否可以分割),如果物品不可以分割,称为0—1背包问题(动态规划);如果物品可以分割,则称为背包问题(贪心算法)。
有3种方法来选取物品:
(1)当作0—1背包问题,用动态规划算法,获得最优值220;
(2)当作0—1背包问题,用贪心算法,按性价比从高到底顺序选取物品,获得最优值160。由于物品不可分割,剩下的空间白白浪费。
(3)当作背包问题,用贪心算法,按性价比从高到底的顺序选取物品,获得最优值240。由于物品可以分割,剩下的空间装入物品3的一部分,而获得了更好的性能。

#include <iostream>
#include <algorithm>
using namespace std;

struct Bag {
	int w, v;
	double c;
}a[1001];

bool cmp(Bag a, Bag b) {
	return a.c >= b.c;
}

double knapsack(int n, Bag a[], double c)
{
	double cleft = c;
	int i = 0;
	double b = 0;
	while (i < n && a[i].w < cleft) {
		cleft -= a[i].w;
		b += a[i].v;
		i++;
	}
	if (i < n) b += 1.0 * a[i].v * cleft / a[i].w;
	return b;
}

int main() {
	int n, c;
	cin >> n >> c;
	for (int i = 0; i < n; i++) {
		cin >> a[i].w >> a[i].v;
		a[i].c = a[i].v / a[i].w;
	}
	sort(a, a + n, cmp);
	cout << knapsack(n, a, c) << endl;
}
删数问题

给定n位正整数a,去掉其中任意k≤n个数字后,剩下的数字按原次序排列组成一个新的正整数。对于给定的n位正整数a和正整数k,设计一个算法找出剩下数字组成的新数最小的删数方案。
输入
第1行是1个正整数a,第2行是正整数k。
输出
对于给定的正整数a,编程计算删去k个数字后得到的最小数。
将该问题记为T,最优解A=xi1xi2 …xim (i1<i2…<im,m=n-k), 在删去k个数后剩下的数字按原次序排成的新数,其最优值记为N。
本问题采用贪心算法求解,采用最近下降点优先的贪心策略:即x1<x2<… <xi-1<xi,如果xi+1<xi (下降点),则删去xi,即得到一个新的数且这个数为n-1位中最小的数N1,可表示为x1x2 …xi-1xi+1… xn。

显然删去1位数后,原问题T变成了对n-1位数删去k- 1个数的新问题T’。
新问题和原问题性质相同,只是问题规模由n减小为n-1,删去的数字个数由k减少为k- 1。
基于此种删除策略,对新问题T’,选择最近下降点的数继续进行删除,直至删去k个数为止。

string a;        //n位数a
int k;
cin>>a>>k;
//如果k≥n,数字被删完了
If (k >= a.size())  a.erase();
else while(k > 0)
{
  //寻找最近下降点
  int i;
  for (i=0; (i<a.size()-1) && (a[i] <= a[i+1]);  ++i);
  a.erase(i, 1);    //删除xi
  k- -;
}
//删除前导数字0
while(a.size() > 1 && a[0] == '0') 
  a.erase(0, 1);
cout<<a<<endl;
木棒加工

现有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)。
输入
输入有多组测试例。输入数据的第一行是测试例的个数T。
每个测试例两行:第一行是一个整数n(1≤n≤5000),表示有多少根木棒;第二行包括n×2个整数,表示l1,w1,l2,w2,l3,w3,…,ln,wn,其中li和wi表示第i根木棒的长度和重量。数据由一个或多个空格分隔。
输出
输出是以分钟为单位的最少准备时间,一行一个。

#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;

struct Stick {
	int w, l;
};

bool cmp(Stick a, Stick b) {
	if (a.l == b.l) return a.w < b.w;//长度相等时,按重量排序
	else if (a.l < b.l) return true;//优先按长度排序
	return false;
}

int LIS(int n, Stick a[]) {
	int b[100];
	memset(b, 0, sizeof(b));
	b[0] = 1;
	for (int i = 1; i < n; i++) {
		int k = 0;
		for (int j = 0; j < i; j++)
			if (a[i].w < a[j].w && k < b[j]) k = b[j];
		b[i] = k + 1;
	}
	int max = 0;
	for (int i = 0; i < n; i++)
		if (b[i] > max) max = b[i];
	return max;
}

int main() {
	int n;
	Stick stick[100];
	cin >> n;
	for (int i = 0; i < n; i++) cin >> stick[i].l >> stick[i].w;
	sort(stick, stick + n, cmp);
	cout << LIS(n, stick) << endl;
	return 0;
}

约翰有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钓鱼所耗费的时间,以此类推。

//从湖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]; 
}
for (i=0; i<n-1; ++i) 
	printf("%d, ", plan[i] * 5);
printf("%d\n", plan[n-1] * 5); 
printf("Number of fish expected: %d\n", best);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值