贪心入门

1.何为贪心

贪心算法实际上指的是把问题划分成一个一个的子问题,然后针对当前的子问题,求出局部最优解,然后将子问题的最优解合并,最终获得总问题的最优解。 
值得注意的是,在对问题求解时,贪心算法总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它做出的仅是在某种意义上的局部最优解。

P.S:贪心子问题是独立的,有区别于动态规划(这个以后讨论动规的时候再聊)。
2.如何判断贪心

从上面这段话中不难看出,一个问题能够通过贪心来获取最优解的前提是:
问题可以被划分成多个子问题。证明可以通过子问题的最优解可以获得最终的最优解。子问题必须具有无后效性,也就是说,当前问题的求解的过程并不会影响之前的子问题的结果。3.贪心策略的制定

1.制定最优解策略,从最初状态开始。 
2.循环解决子问题,逐步缩小问题规模。针对每一个子问题,都运用局部最优解策略获取结果。 
3.对每个子问题的解进行处理,获得最终结果

贪心问题的求解代码都不会很长,但是对于贪心策略的制定确是要费点心力。 
一般来说,多用点数据验证验证,就能减少很多不必要麻烦。
例:NYOJ 71 独木舟上的旅行 

洛谷1094 纪念品分组

一:选择不相交区间

NYOJ 106 会场安排问题

描述 
学校的小礼堂每天都会有许多活动,有时间这些活动的计划时间会发生冲突,需要选择出一些活动进行举办。小刘的工作就是安排学校小礼堂的活动,每个时间最多安排一个活动。现在小刘有一些活动计划的时间表,他想尽可能的安排更多的活动,请问他该如何安排。

输入 
第一行是一个整型数m(m<100)表示共有m组测试数据。 
每组测试数据的第一行是一个整数n(n<1000)表示该测试数据共有n个活动。 
随后的n行,每行有两个正整数Bi,Ei(0<=Bi,Ei<10000),分别表示第i个活动的起始与结束时间(Bi<=Ei)

输出 
对于每一组输入,输出最多能够安排的活动数量。 
每组的输出占一行 
样例输入 


1 10 
10 11 

1 10 
10 11 
11 20 
样例输出 

2

提示 
注意:如果上一个活动在t时间结束,下一个活动最早应该在t+1时间开

首先,把题目中的信息转换为模型。 
给你n个区间,求得不相交区间的数目。(P.S.也就是一句话的事~~题目太经典,无需多言)

下面我们来分析这道题是否能用贪心的方法来做,让我们回顾贪心的三个关键点

1.问题可以分解为多个子问题。

那么当前问题可不可以? 当然可以,选择不相交区间的过程,完全可以分解为:给定一个区间,按照某种规则对接下来的多个区间进行比较,然后得出最优结果的过程。 
这里的规则指的是:对于当前区间来说,优先选择离当前区间距离最近而且区间最短的下一个区间。

2.总问题的最优解可由各个子问题的最优解得到。

这个也很好理解,由于每个子问题求得的永远是离当前区间距离最近而且区间最短的下一个区间,而下一个区间也是按照这个规则来寻找另外一个区间的。所以对于最优解集合的解释就是“对于每一个区间来说,后面的那个区间就是离它最近(空白少)而且区间最短(占用少)的”,这也就是说,满足总体问题最优解的“尽可能多的选择区间”

3.子问题具有”无后效性“,当前子问题的求解不会影响到之前子问题的解。

这个题目乍一看貌似是不满足无后效性的,因为给定的数据顺序混乱,似乎每一次寻找都要进行一次遍历,但是这个过程可以通过排序的方式解决。这种处理原始数据,使得处理过程更加简单快捷的方法,叫做预处理。

综上,这道题(或者说这类问题)确实是可以通过贪心策略来取得最终的解得。 
那么,预处理过程应该怎么排序呢? 
我们再来聊一聊最优解的判定规则,也就是两点: 
1.区间最短(长度) 
2.间隔最小(两端位置)

那么我们是按照哪一种指标来进行排序呢?答案很明显,长度肯定是不靠谱的。因为单单通过长度进行排序,我们并不能对两端位置进行控制,并没有减少我们对于数据的判定过程,反而可能会加大难度。

那么我们按照两端位置进行排序,排序的规则很明显,按照从小到大的顺序进行排序。 
网络上很多人采用以后端点排序的方式,可能会让刚接触的人误以为只能通过后断电排序,其实不然,采用哪个端点排序都是可以的。而不管选择哪一个来排序,其原理和本质都一样,都是为了方便操作,将其有序化。

那么我们的贪心策略可以用伪代码描述为:

while(剩余区间的数目不为0){ 
     if(找到符合条件的下一个区间){        
        当前区间 = 下一个区间;        
        答案数+1;
    }     
    区间数--;
}

代码:

#include<stdio.h>
#include<algorithm>
using namespace std;
#define MAX 10050
struct Node
{
    int x,y;
} map[10005];  /*定义结构体数组*/
int cmp(Node a,Node b)
{
    if(a.y!=b.y)
    {
        return a.y<b.y;
    } 
   return a.x<b.x;
}
int main()
{
    int m = 0;
    scanf("%d",&m);
    while(m--)
    {
        int  num = 0;
        scanf("%d",&num);
        for(int i = 0; i < num; i++)
        {
            scanf("%d%d",&map[i].x,&map[i].y);
        }
        sort(map,map+num,cmp);
        int start = map[0].y;   ///起始位置
        int count=1;    ///答案数
        int num_last = num;///剩余区间数目
        while(num_last > 0)
        {
            if(map[num - num_last].x > start)
            {
                count++;
                start = map[num - num_last].y;
            }
            num_last--;
        }
        printf("%d\n",count);
    }
    return 0;
}

二:区间选点问题

区间选点的问题大致可以描述为: 给定N个区间[a,b],取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以重复)。

关于贪心算法的验证过程就不再赘述,现在思考一下贪心策略的制定。 
对于区间[a1, b1] 、[a2, b2]、 [a3, b3] 来说, 如果想选择最少的点,那么必须选择每个区间的右端点,示意图如下: 

当你每一次都选择区间的最右端,才能保证每一个选的点覆盖的范围都是最广泛的,也就是说选的点才是最少的。

和之前不相交区间的思考方法类似,把区间进行预处理,按照端点的大小排序(同样,按照右端点排序会好理解一点,但是左端点排序一样可以起到作用,初学者不必迷信右端点排序)。 
预处理过后,求解策略的思路和求不相交区间相似,如果下一个区间的左端点不被覆盖,则答案+1,如下:

while(剩余区间的数目不为0){    
    if(找到符合条件的下一个区间)    {
        当前区间 = 下一个区间;        
        答案数+1;    
    }     
    区间数--;
}

例题:NYOJ 1036 非洲小孩

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define MAX 1005
struct Node
{
    int x;
    int y;
};
Node map[MAX];
bool cmp(Node a,Node b){
    if(a.y<b.y) return 1;
    else if(a.y==b.y) {
    	return a.x<b.x;
	}
	else return 0;
}
int main()
{
	int num = 0;
    while(EOF != scanf("%d", &num)){
        memset(map, -1, sizeof(map));
        int a1,b1,a2,b2;
        for(int i = 0; i < num; i++){
            scanf("%d:%d-%d:%d",&a1,&b1,&a2,&b2);
            int key1 = a1*60+b1;
            int key2 = a2*60+b2;
            if(key1 > key2){
                swap(key1, key2);
            }
            map[i].x = key1;
            map[i].y = key2;
        }
        sort(map, map+num, cmp);
        int start = map[0].y;
        int ans = 1;
        for(int i=1;i<num;i++){
            if(map[i].x>start){
            	start=map[i].y;
            	ans++;
			}
		}
 

        printf("%d\n", ans);

	}    
    return 0;

}

三:部分背包问题

部分背包问题虽说是归于背包问题的一种,而且背包问题大多数是通过动态规划的出的结果,但是贪心算法解部分背包,不管是思想还是操作上来说,都是非常简单的。

首先,我们来看一下什么叫做部分背包。

有N个商品,每个商品的重量为WI,价格为:PI,现有一个背包,最多能装M的重量. 
其中(0<=I< N,0< wi<.M). 
问:怎样装能使包中装入的商品价值最高(对于每个商品可以只装该商品的一部分)

那我们就来分析一下条件:: 
1. 商品重量不是无限的。
    那就说明我们不能只拿一种东西,大多数情况都要进行多个物品的选取。

2. 商品可以被拆分成部分。
    那就说明我们需要考虑的不仅是“这个物品能不能装”?,还要考虑“装多少”?    在这两种情况下,我们需要考虑的是不能是物品的价值,而是性价比。

3. 商品的属性有两个:重量和价格;
    根据这两个属性,我们可以计算出商品的性价比,也就是单价。

4. 要求装入背包中的物品价格最高
    那就是说,我们需要按照性价比从高到低的顺序进行,依次装入我们需要得物品,直到背包装满。    由于是按照性价比从高到低的顺序进行的选取,那么同样重量的背包,这种方法获得的价值是所有方法中最大的。123

所以我们可以大概写出伪代码表示整个求解过程:

sort 先把各个物品按照单价从高到低排序 
for(循环每一个物品){    
    if(物品的重量小于背包剩余的重量)    {        
        背包剩余的重量 -= 物品总重量;        
        总价值 += 物品总价值;    
    }else    {        
        总价值  += 物品单价*背包剩余重量;        
        退出循环;    
    }
}

例题:NYOJ106 背包问题

代码:

 

#include <bits/stdc++.h>
using namespace std;
struct Node{
	int v,w;
}a[11];
bool cmp(Node a,Node b){
	if(a.v!=b.v){
		return a.v>b.v;
	}
	return a.w<b.w;
}
int main(){
	int n,s,m;
	cin>>n;
	while(n--){
		cin>>s>>m;
		for(int i=0;i<s;i++){
			cin>>a[i].v>>a[i].w;
		}
		sort(a,a+s,cmp);
		int ans=0,sum=0,W=0;
		for(int i=0;i<s;i++){
			if(m>=a[i].w){
				ans+=a[i].v*a[i].w;
				m-=a[i].w;
			}
			else{
				ans+=a[i].v*m;
				break;
			}
		}
		cout<<ans<<endl;
		
		
		
	}
	return 0;
} 

四:最小区间覆盖问题

在贪心算法的层面上,我们能够进行解决的区间覆盖,指的就是最小区间覆盖问题。 
问题描述为:

给定n个区间和一个范围[a, b],选择尽量少的区间,使得[a, b]能够被完全覆盖。

贪心的策略我们可以思考一下:

对于当前区间[a,b]来说,选择的下一个区间的左端点值a2一定不会大于b,否则就不能完成“覆盖”这一操作。对于当前区间[a,b]来说,如果有多个区间都满足条件1,那么一定选择右端点最大的区间,否则就不能满足“最小”这一目的。

同样的,我们需要对区间进行排序,并且排序方法还是那样,按照左右端点都无所谓。反正需要的只是一个有序的列表而已。

那么我们可以尝试着写出如下伪代码:

while(剩余区间数目不为0){    
    if(总长度已经超出覆盖范围)    {        
        结束循环;    }   
     for(循环查找符合条件的下一个最大区间);    
    if(找到了)    {        
        答案数+1;        
        总长度 += 最大能切换的区间长度;    
       }else    {        
        表示不能完全覆盖,退出循环,答案数 = 0;    
    }
}

例题:NYOJ12 喷水装置二

代码:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#define INF 1e-6
using namespace std;
int N,n,w,h;
double R;
struct Node{
	int x,ri;
	double l,r;
}a[10010];
double cr(int r){
	double res;
    res = (double)r*r - (double)h*h/4.0;
 
    if(res >= INF) {
        res = sqrt(res);
    } else {
        res = 0;
    }
 
    return res;
}
bool cmp(Node a,Node b){
	if(a.l!=b.l){
		return a.l<b.l;
	}
	return a.r>b.r;
}
int main(){
	cin>>N;
	while(N--){
		cin>>n>>w>>h;
		for(int i=0;i<n;i++){
			cin>>a[i].x>>a[i].ri;
			R=cr(a[i].ri);
			a[i].l=a[i].x-R;
			a[i].r=a[i].x+R;
		}
		sort(a,a+n,cmp);
		double W=0;
		int ans=0;
		while(W<w){
			double maxl=0;
			for(int i=0;i<n&& a[i].l<=W;i++){
				maxl=max(maxl,a[i].r-W);
			}
			if(maxl==0){//中间断层,不能继续拓展 
				ans=0;break;
			}
			else {
				W+=maxl;
				ans++;
			} 
		}
		cout<<ans<<endl;		
	}
}



 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值