14天阅读挑战赛
努力是为了不平庸~
算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法!欢迎记录下你的那些努力时刻(算法学习知识点/算法题解/遇到的算法bug/等等),在分享的同时加深对于算法的理解,同时吸收他人的奇思妙想,一起见证技术er的成长~

贪心是人类自带的能力,贪心算法是在贪心决策上进行统筹规划的统称。

比如一道常见的算法笔试题----跳一跳

有n个盒子排成一行,每个盒子上面有一个数字a[i],表示最多能向右跳a[i]个盒子;
小明站在左边第一个盒子,请问能否到达最右边的盒子?
比如说:[1, 2, 3, 0, 4] 可以到达第5个盒子;
[3, 2, 1, 0, 4] 无法到达第5个盒子;

我们自然而然能产生一种解法:尽可能的往右跳,看最后是否能到达。
本文即是对这种贪心决策的介绍。

正文

贪心算法基础概念

狭义的贪心算法指的是解最优化问题的一种特殊方法,解决过程中总是做出当下最好的选择,因为具有最优子结构的特点,局部最优解可以得到全局最优解;这种贪心算法是动态规划的一种特例。能用贪心解决的问题,也可以用动态规划解决。

而广义的贪心指的是一种通用的贪心策略,基于当前局面而进行贪心决策。以跳一跳的题目为例:
我们发现的题目的核心在于向右能到达的最远距离,我们用maxRight来表示;
此时有一种贪心的策略:从第1个盒子开始向右遍历,对于每个经过的盒子,不断更新maxRight的值。

贪心算法的思考过程

贪心的思考过程类似动态规划,依旧是两步:大事化小小事化了
大事化小:
一个较大的问题,通过找到与子问题的重叠,把复杂的问题划分为多个小问题;
小事化了:
从小问题找到决策的核心,确定一种得到最优解的策略,比如跳一跳中的向右能到达的最远距离

在证明局部的最优解是否可以推出全局最优解的时候,常会用到数学的证明方式。

贪心算法的具体应用

1、纸币找零问题

有1元、2元、5元、10元的纸币分别有a[1], a[2], a[3], a[4]张,要用这些纸币凑出m元,至少要用多少张纸币?

如果是动态规划:
要凑出m元,必须先凑出m-1、m-2、m-5、m-10元,我们用dp[i]表示凑出i元的最少纸币数;
dp[i]=min(dp[i-1], dp[i-2], dp[i-5], dp[i-10]) + 1;
容易知道dp[1]=dp[2]=dp[5]=dp[10]=1
根据以上递推方程和初始化信息,可以容易推出dp[1~m]的所有值。

似乎有些不对?平时我们找零钱有这么复杂吗?
从贪心算法角度出发,当m>10且我们有10元纸币,我们优先使用10元纸币,然后再是5元、2元、1元纸币。
从日常生活的经验知道,这么做是正确的,但是为什么?

假如我们把题目变成这样,原来的策略还能生效吗?

有1元、5元、7元的纸币分别有a[1], a[2], a[3]张,要用这些纸币凑出m元,至少要用多少张纸币?

接下来我们来分析这种策略:
已知对于m元纸币,1,2,5元纸币使用了a,b,c张,我们有a+2b+5c=m;
假设存在一种情况,1、2、5元纸币使用数是x,y,z张,使用了更少的5元纸币(z<c),且纸币张数更少(x+y+z<a+b+c),即是用更少5元纸币得到最优解。
我们令k=5*(c-z),k元纸币需要floor(k/2)张2元纸币,k%2张1元纸币;(因为如果有2张1元纸币,可以使用1张2元纸币来替代,故而1元纸币只能是0张或者1张)
容易知道,减少(c-z)张5元纸币,需要增加floor(5*(c-z)/2)张2元纸币和(5*(c-z))%2张纸币,而这使得x+y+z必然大于a+b+c。
由此我们知道不可能存在使用更少5元纸币的更优解。
所以优先使用大额纸币是一种正确的贪心选择。

对于1、5、7元纸币,比如说要凑出10元,如果优先使用7元纸币,则张数是4;(1+1+1+7)
但如果只使用5元纸币,则张数是2;(5+5)
在这种情况下,优先使用大额纸币是不正确的贪心选择。(但用动态规划仍能得到最优解)

2、服务器任务安排问题

服务器有n个任务要执行,每个任务有开始时间Si秒和结束时间Ti秒,同一时间只能执行一个任务。
问如何安排任务,使得在时间m内尽可能多的完成任务。

如果是动态规划:
前i秒的完成的任务数,可以由前面1~i-1秒的任务完成数推过来。
我们用dp[i]表示前i秒能完成的任务数
在计算前i秒能完成的任务数时,对于第j个任务,我们有两种决策:
1、不执行这个任务,那么dp[i]没有变化;
2、执行这个任务,那么必须腾出来(Sj, Tj)这段时间,那么dp[i] = max(dp[i], dp[ S[j] ] ) + 1
比如说对于任务j如果是第5秒开始第10秒结束,如果i>=10,那么有dp[i]=max(dp[i], dp[5] + 1);(相当于把第5秒到第i秒的时间分配给任务j)

再考虑贪心的策略,现实生活中人们是如何安排这种多任务的事情?我换一种描述方式:

小明在学校兼职,小明一天兼职的时间有10个小时;
现在有多个兼职岗位,每个岗位有个开始时间和结束时间,小明同一时间只能做一个兼职;
问小明每天最多能做几份兼职?

我们自然而然会想到一个策略:先把结束时间早的兼职给做了!
为什么?
因为先做完这个结束时间早的,能留出更多的时间做其他兼职。
我们天生具备了这种优化决策的能力。

3、分糖果问题

n个小朋友玩完游戏后,老师准备给他们发糖果;
每个人有一个分数a[i],如果比左右的人分数高,那么糖果也要比左右的多,并且每个小朋友至少有一颗。
问老师最少准备多少糖果。

这是一道LeetCode题目。
这个题目不能直接用动态规划去解,比如用dp[i]表示前i个人需要的最少糖果数。
因为(前i个人的最少糖果数)这种状态表示会收到第i+1个人的影响,如果a[i]>a[i+1],那么第i个人应该比第i+1个人多。
即是这种状态表示不具备无后效性。

如果是我们分配糖果,我们应该怎么分配?
答案是:从分数最低的开始。
按照分数排序,从最低开始分,每次判断是否比左右的分数高。
假设每个人分c[i]个糖果,那么对于第i个人有c[i]=max(c[i-1],c[c+1])+1; (c[i]默认为0,如果在计算i的时候,c[i-1]为0,表示i-1的分数比i高)
但是,这样解决的时间复杂度为O(NLogN),主要瓶颈是在排序。
如果提交,会得到Time Limit Exceeded的提示。

我们需要对贪心的策略进行优化:
我们把左右两种情况分开看。
如果只考虑比左边的人分数高时,容易得到策略:
从左到右遍历,如果a[i]>a[i-1],则有c[i]=c[i-1]+1;否则c[i]=1。

再考虑比右边的人分数高时,此时我们要从数组的最右边,向左开始遍历:
如果a[i]>a[i+1], 则有c[i]=c[i+1]+1;否则c[i]不变;

这样讲过两次遍历,我们可以得到一个分配方案,并且时间复杂度是O(N)

4、小船过河问题

n个人要过河,但是只有一艘船;船每次只能做两个人,每个人有一个单独坐船的过河时间a[i],如果两个人(x和y)一起坐船,那过河时间为a[x]和a[y]的较大值。
问最短需要多少时间,才能把所有人送过河。

题目给出关键信息:1、两个人过河,耗时为较长的时间;
还有隐藏的信息:2、两个人过河后,需要有一个人把船开回去;
要保证总时间尽可能小,这里有两个关键原则:应该使得两个人时间差尽可能小(减少浪费),同时船回去的时间也尽可能小(减少等待)。

先不考虑空船回来的情况,如果有无限多的船,那么应该怎么分配?
答案:每次从剩下的人选择耗时最长的人,再选择与他耗时最接近的人。

再考虑只有一条船的情况,假设有A/B/C三个人,并且耗时A<B<C。
那么最快的方案是:A+B去, A回;A+C去;总耗时是A+B+C。(因为A是最快的,让其他人来回时间只会更长,减少等待的原则

如果有A/B/C/D四个人,且耗时A<B<C<D,这时有两种方案:
1、最快的来回送人方式,A+B去;A回;A+C去,A回;A+D去; 总耗时是B+C+D+2A (减少等待原则)
2、最快和次快一起送人方式,A+B先去,A回;C+D去,B回;A+B去;总耗时是 3B+D+A (减少浪费原则)
对比方案1、2的选择,我们发现差别仅在A+C和2B;
为何方案1、2差别里没有D?
因为D最终一定要过河,且耗时一定为D。

如果有A/B/C/D/E 5个人,且耗时A<B<C<D<E,这时如何抉择?
仍是从最慢的E看。(参考我们无限多船的情况)
方案1,减少等待;先送E过去,然后接着考虑四个人的情况;
方案2,减少浪费;先送E/D过去,然后接着考虑A/B/C三个人的情况;(4人的时候的方案2)

到5个人的时候,我们已经明显发了一个特点:问题是重复,且可以由子问题去解决。
根据5个人的情况,我们可以推出状态转移方程dp[i] = min(dp[i - 1] + a[i] + a[1], dp[i - 2] + a[2] + a[1] + a[i] + a[2]);
再根据我们考虑的1、2、3、4个人的情况,我们分别可以算出dp[i]的初始化值:
dp[1] = a[1];
dp[2] = a[2];
dp[3] = a[2]+a[1]+a[3];
dp[4] = min(dp[3] + a[4] + a[1], dp[2]+a[2]+a[1]+a[4]+a[2]);

由上述的状态转移方程和初始化值,我们可以推出dp[n]的值。

有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为Wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。(意思就是在不超过载重量的情况下最多能装多少)

大体思路:

首先用数组承载每个集装箱的重量

为了方便,给数组排个序

循环小于集装箱数量

方法一:每个重量累加判断是否超过载重量

方法二:定义剩余变量,剩余变量减每个的重量,然后判断每个的重量是否大于剩余重量

#include <iostream>
#include <iomanip>
#include <string.h>
#include <cmath>
#include <algorithm>//算法头文件
using namespace std;void load_problem(double arrs[], int num, double c){//集装箱重量的数组、集装箱数量、载重量 //为数组赋值cout<<"给每个集装箱的重量赋值:"<<endl;for(int i=0; i<num; i++){cin>>arrs[i];} //排序/*第一个参数是数组的首地址,一般写上数组名就可以,因为数组名是一个指针常量第二个参数相对较好理解,即首地址加上数组的长度n(代表尾地址的下一地址)默认可以不填,如果不填sort会默认按数组升序排序。也就是1,2,3,4排序。也可以自定义一个排序函数,改排序方式为降序什么的,也就是4,3,2,1这样。 */sort(arrs,arrs+num);double tsum = 0.0;  //承载已经装的重量 int temp = 0;   //计数器 for(int i=0; i<num; i++){tsum += arrs[i];if(tsum < c){temp++;}else{break;}} cout<<"最多可装下:"<<temp<<endl;
}int main(){double c;//载重量 int num;//集装箱数量 cout<<"请输入载重量和集装箱数量:"<<endl;cin>>c>>num;double arrs[num];//承载每个集装箱重量的数组load_problem(arrs,num,c);return 0;
}

算法二

#include <iostream>
#include <iomanip>
#include <string.h>
#include <cmath>
#include <algorithm>//算法头文件
using namespace std;void load_problem2(double arrs[], int num, double c){//集装箱重量的数组、集装箱数量、载重量 //为数组赋值cout<<"给每个集装箱的重量赋值:"<<endl;for(int i=0; i<num; i++){cin>>arrs[i];} //排序sort(arrs,arrs+num);int temp = 0;double currentSpace = c; //声明剩余空间for(int i=0; i<num; i++){if(currentSpace < arrs[i]){ //剩余空间小于当前重量 break;}currentSpace -= arrs[i];temp++;} cout<<"最多可装下:"<<temp<<endl;
}int main(){double c;//载重量 int num;//集装箱数量 cout<<"请输入载重量和集装箱数量:"<<endl;cin>>c>>num;double arrs[num];//承载每个集装箱重量的数组load_problem2(arrs,num,c);return 0;
}

这是一道贪心和动态规划的结合题目,动态规划的决策过程中用到了贪心的策略。

总结

贪心的学习过程,就是对自己的思考进行优化。
是把握已有信息,进行最优化决策。
这里还有一些收集的贪心练习题,可以实践练习。
这里还有在线分享,欢迎报名。

【趣学算法】-Day2-贪心算法相关推荐

  1. 趣学算法系列-贪心算法

    趣学算法系列-贪心算法 声明:本系列为趣学算法一书学习总结内容,在此推荐大家看这本算法书籍作为算法入门, 原作者博客链接,本书暂无免费电子版资源,请大家支持正版,更多的案例分析请查看原书内容. 第二章 ...

  2. python贪心算法最短路径_dijkstra算法(贪心算法)——解决最短路径问题

    最短路径 给定一张带权图和其中的一个点(作为源点),求源点到其余顶点的最短路径 基本思想 1)源点u,所有顶点的集合V,集合S(S中存有的顶点,他们到源点的最短路径已经确定,源点u默认在S中),集合V ...

  3. 3.Python算法之贪心算法思想

    贪心算法 1.什么是贪心算法 2.贪心算法的特点和思路 3.贪心算法的缺点 4.贪心算法的基本思路 5.贪心算法的基本过程 6.贪心算法解决"找零"问题 6.贪心算法解决" ...

  4. 回溯算法和贪心算法_回溯算法介绍

    回溯算法和贪心算法 回溯算法 (Backtracking Algorithms) Backtracking is a general algorithm for finding all (or som ...

  5. 五大算法之三--贪心算法

    一.基本概念:        所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解.      贪心算法没有固定的 ...

  6. java调度问题的贪心算法_贪心算法——换酒问题

    知识回顾 贪心算法 (greedy algorithm),又称贪婪算法. 是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法. 贪心算法在 有最优子 ...

  7. 任务分配算法c语言程序,程序员算法基础——贪心算法

    原标题:程序员算法基础--贪心算法 前言 贪心是人类自带的能力,贪心算法是在贪心决策上进行统筹规划的统称. 比如一道常见的算法笔试题跳一跳: 有n个盒子排成一行,每个盒子上面有一个数字a[i],表示最 ...

  8. 数据结构与算法之美(十四)算法思想——贪心算法

    目录 贪心算法介绍 贪心算法例子 1. 背包 2. 分糖果 3. 钱币找零 4. 区间覆盖 5. 区间覆盖的延伸:任务调度.教师排课 贪心算法经典应用 1. 霍夫曼编码 2. 最小生成树算法 3. 最 ...

  9. 贪心算法适用条件_【算法】贪心算法

    概念&&介绍 贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解.所以说只有证明局部最优解在全局最优解 ...

  10. 【趣学算法】Day2 贪心算法——最优装载问题

    14天阅读挑战赛 努力是为了不平庸~ 算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法! ❤️一名热爱Java的大一学生,希望与各位大佬共同学习进步❤️

最新文章

  1. 使用 git 管理 portage tree
  2. 刀模图是什么意思_“吃鸡”光子公布神秘图,海岛图上有44个坐标,暗示信号值取消?...
  3. WPF学习笔记(4):获取DataGridTemplateColumn模板定义的内容控件(转)
  4. 基于消息队列的分布式事务解决方案
  5. sql 增加链接服务器,SQL server利用脚本添加链接服务器,可设置别名
  6. Rotation Rose各部分的名称
  7. python命令提示符窗口在哪里_详解python命令提示符窗口下如何运行python脚本
  8. 郑州智慧岛大数据管委会_数据科学融通应用数学 ‖ 智慧岛大讲堂
  9. 怎么更好掌握Web前端技术?JS的跨域是怎么回事?
  10. 【数据结构笔记42】哈希表应用:文件中单词词频统计
  11. moxa串口卡Linux驱动,moxa多串口驱动下载
  12. 宏碁台式计算机u盘启动,宏基台式机bios设置u盘启动方法
  13. flvjs is not defined
  14. Manjaro 清理垃圾
  15. 几个常见的逻辑训练及参考答案
  16. 用Matlab实现蒙特卡洛法求心形线面积
  17. 将Carte部署为Windows服务
  18. 数据库连接查找不到数据库_查找具有受保护的健康信息的数据库
  19. Altium Designer出现collision警告
  20. 使用nvm下载安装Node

热门文章

  1. 使用界面配置静态路由(保姆级教程)
  2. 自然人代开适用于哪些公司业务环境,如何通过代开降低税负?
  3. 【系统性学习】Linux Shell易忘重点整理
  4. 好玩的文件加密方法(自己给文件头部加密)
  5. Altium Designer 18系统参数选项介绍及常规设置
  6. 傻白入门芯片设计,芯片工程师常说的那些“黑话”(七)
  7. php mongodb 集群,搭建MongoDB集群
  8. html 人物介绍 轮播,jQuery卡通人物介绍卡牌轮播切换代码
  9. mongo集群——副本集模式
  10. Oracle 同义词删除不掉