贪心策略取得最优解的条件_L2算法基础第14课 贪心

贪心算法是一种解决问题的策略,每次选择当前看起来最佳的选项。本文通过多个实例,如排队接水、线段覆盖和合并果子问题,探讨贪心算法的适用条件、基本思路和实现框架,并提醒证明贪心策略能否得到全局最优解的重要性。
摘要由CSDN通过智能技术生成

L2-算法基础-第14课 贪心

真正厉害的人,往往都比较“贪心”.

「所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。」 也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。

贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。

示例

在一个山洞里发现了5个宝石

宝石编号12345
宝石体积24397

现在你有个背包, 容量是 10.

状态: 当前背包里已经放了多少宝石, 背包还剩多少体积 策略: 选择宝石的方法

  • 怎么选装下的宝石个数最多呢?

这个比较简单, 直接选体积最小的就可以贪心的装了, 没有反例. 无论当前什么状态, 选择体积最小的都是最优方法.

  • 怎么选装下的体积和最大呢???

贪心啊, 所以选体积最大的 9 然后, 然后没得选了. 贪心啊, 选体积最小的, 这样剩下空间大可以装更多的. 选 2, 然后选 3, 4. 还是总体积还是 9. 可如果选 3 和 7, 能得到体积 10.

贪心策略应该是装满, 可是我们不知道怎么才能装满, 不是先装最小的, 也不是先装最大的. 这个时候可以暴力枚举, 当然最好的方式是后面会学到的动态规划.

贪心只考虑当前状态, 所以实现起来比较简单, 但证明一道题目符合贪心比较难.

要确定一个问题是否适合用贪心算法求解,必须证明每一步所作的贪心选择最终导致问题的整体最优解。证明的大致过程为:首先考察问题的一个整体最优解,并证明可修改这个最优解,使其以贪心选择开始,做了贪心选择后,原问题简化为规模更小的类似子问题。然后用数学归纳法证明通过每一步做贪心选择,最终可得到问题的整体最优解.

贪心算法适用的问题

贪心策略适用的前提是:局部最优策略能导致产生全局最优解。

贪心算法的基本思路

  • 建立数学模型来描述问题。
  • 把求解的问题分成若干个子问题。
  • 对每一子问题求解,得到子问题的局部最优解。
  • 把子问题的解局部最优解合成原来解问题的一个解。

贪心算法的实现框架

    从问题的某一初始解出发;
    while (能朝给定总目标前进一步)
    {
          利用可行的决策,求出可行解的一个解元素;
    }
    由所有解元素组合成问题的一个可行解;

P1223 排队接水

题目描述

有 n 个人在一个水龙头前排队接水,假如每个人接水的时间为 ,请编程找出这 n 个人排队的一种顺序,使得 n 个人的平均等待时间最小。

输入格式

第一行为一个整数 n。

第二行 n 个整数,第 i 个整数 表示第 i 个人的等待时间 。

输出格式

输出文件有两行,第一行为一种平均时间最短的排队顺序;第二行为这种排列方案下的平均等待时间(输出结果精确到小数点后两位)。

输入输出样例
  • 输入 #1复制
10
56 12 1 99 1000 234 33 55 99 812
  • 输出 #1复制
3 2 7 8 1 4 9 6 10 5
291.90
说明/提示

不保证 不重复。

分析

ai 和 bi 且 ai < bi

那么针对这两个元素:就有两种排列情况:

  1. ai排在bi前面那么有总时间:t1 = ai + ai + bi.

  2. bi排在ai前面那么有总时间:t2 = bi + bi + ai.

1 和 2的时间差为 t2 - t1 = (bi + bi + ai) - (ai + ai + bi)

得出 t2 - t1 = bi - ai;

因为 bi 比 ai 大, 所以 t2 也比 t1 大.

于是得出结论:当 ai 在 bi 前面时,时间为最小值。

于是反推回总体,两两相较,那么越小的应该越排在前面,以至于总时间越小

参考代码
#include
using namespace std;

struct water {
    int idx;
 int time;
};
water w[1008];
double t;
int n;


bool compare(water w1 ,water w2) {
    return w1.time }

int main() {
    scanf("%d",&n);
    for(int i=1; i<=n; i++) {
        scanf("%d",&w[i].time);
        w[i].idx=i;
    }

    sort(w+1, w+n+1, compare);
    for(int i=1; i<=n; i++) {
     printf("%d ", w[i].idx);
     t+=w[i].time * (n- i);
 }

    printf("\n");
    printf("%.2f",t/n);

    return 0;
}

上面的题目的贪心策略还算好找, 下面这道题目就有点难了.

P1803凌乱的yyy / 线段覆盖

题目背景

快 noip 了,yyy 很紧张!

题目描述

现在各大 oj 上有 n 个比赛,每个比赛的开始、结束的时间点是知道的。

yyy 认为,参加越多的比赛,noip 就能考的越好(假的)。

所以,他想知道他最多能参加几个比赛。

由于 yyy 是蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加 2 个及以上的比赛。

输入格式

第一行是一个整数 n ,接下来 n 行每行是 2 个整数 $a_{i},b_{i} ( a_{i}

输出格式

一个整数最多参加的比赛数目。

输入输出样例
  • 输入 #1复制
3
0 2
2 4
1 3
  • 输出 #1复制

2

说明/提示

对于 20% 的数据,

对于 50% 的数据, 。

对于 70% 的数据, 。

对于 100% 的数据, , 。

分析

这道题目抽象成下面的问题:

在一个数轴上有n条线段,现要选取其中k条线段使得这k条线段两两没有重合部分,问最大的k为多少。

最左边的线段放什么最好?

显然放右端点最靠左的线段最好,从左向右放,右端点越小妨碍越少

其他线段放置按右端点排序,贪心放置线段,即能放就放

参考代码
#include
using namespace std;

int n;
int cnt;

struct line{
    int start;
    int end;
};

line l[1000008];
bool compare(line l1, line l2){
 return l1.end }

int main(){
 int i, tend;

    scanf("%d",&n);
    for(i=0; i     scanf("%d %d", &l[i].start, &l[i].end);
 }
 sort(l, l+n, compare);

 tend = 0;
 for(i=0; i  if(l[i].start>=tend) {
   cnt++;
   tend=l[i].end;
  }
    }

 printf("%d", cnt);

 return 0;
}

复杂点的题目是贪心 + 其他算法的综合. 比如下面这道题目.

P1090 合并果子

题目描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n−1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 种果子,数目依次为 1 , 2 , 9 。可以先将 1 、 2 堆合并,新堆数目为 3 ,耗费体力为 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 ,耗费体力为 12 。所以多多总共耗费体力 = 3+12=15 。可以证明 15 为最小的体力耗费值。

输入格式

共两行。

第一行是一个整数 ,表示果子的种类数。

第二行包含 n 个整数,用空格分隔,第 i 个整数 是第 i 种果子的数目。

输出格式

一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 。

输入输出样例
  • 输入 #1复制
3
1 2 9
  • 输出 #1复制

15

说明/提示

对于30%的数据,保证有 :

对于50%的数据,保证有 ;

对于全部的数据,保证有 。

分析

贪心策略: 先找到两个最小的,进行合并,然后再放回去. 如果每次合并完, 调用sort 进行快排, 找到两个最小的, 会超时. 用堆排序(优先队列)比较好, 其实修改插入排序也能过.

参考代码
#include 
using namespace std;

int n;
int a[10008];
int total, tmp;

void change(int start){
 int i;
 int  tmp = a[start];
 for(i = start+1; i   if(a[i]    a[i-1] = a[i];
  } else
   break;
 }

 a[i-1] = tmp;
}

int main(){
 scanf("%d", &n);
 for(int i = 0; i   scanf("%d", &a[i]);
 }

 sort(a, a+n);

 for(int i = 0; i 1; i++) {
  a[i + 1] = a[i] + a[i + 1];
  total += a[i + 1];
  change(i + 1);
  //sort(a + i + 1, a+n);  //50分
 }

 printf("%d", total);

 return 0;
}

总结

对于贪心题目, 贪心策略可以大胆假设, 小心求证. 证明不出来, 时间充裕的情况, 可以试着找找极端情况下的反例. 时间有限的情况下, 默认自己是对的. 通常贪心的做法也能得点分, 甚至可以多种贪心策略下求出几个解后, 再选最优解.

题单

  • P2240 【深基12.例1】部分背包问题
  • P1223 排队接水
  • P1803 凌乱的yyy / 线段覆盖
  • P1090 合并果子 / [USACO06NOV]Fence Repair G
  • P3817 小A的糖果
  • P1106 删数问题
  • P1478 陶陶摘苹果(升级版)
  • P5019 铺设道路
  • P1208 [USACO1.3]混合牛奶 Mixing Milk
  • P1094 纪念品分组
  • P4995 跳跳!
  • P4447 [AHOI2018初中组]分组
  • P1080 国王游戏

云帆优培订阅号:

5ea8f4901d3b8103788c022fdc5eb375.png

云帆优培服务号:

                    9722b2165614b71e299bb6ba8545b093.png

云帆优培老师联系方式:

云帆老师

微信:

364a9aff2132f51315db706dbd185897.png

db225c47a92a3a8d40df573b71395796.png

云帆优培介绍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值