【趣学算法】-Day2-贪心算法
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-贪心算法相关推荐
- 趣学算法系列-贪心算法
趣学算法系列-贪心算法 声明:本系列为趣学算法一书学习总结内容,在此推荐大家看这本算法书籍作为算法入门, 原作者博客链接,本书暂无免费电子版资源,请大家支持正版,更多的案例分析请查看原书内容. 第二章 ...
- python贪心算法最短路径_dijkstra算法(贪心算法)——解决最短路径问题
最短路径 给定一张带权图和其中的一个点(作为源点),求源点到其余顶点的最短路径 基本思想 1)源点u,所有顶点的集合V,集合S(S中存有的顶点,他们到源点的最短路径已经确定,源点u默认在S中),集合V ...
- 3.Python算法之贪心算法思想
贪心算法 1.什么是贪心算法 2.贪心算法的特点和思路 3.贪心算法的缺点 4.贪心算法的基本思路 5.贪心算法的基本过程 6.贪心算法解决"找零"问题 6.贪心算法解决" ...
- 回溯算法和贪心算法_回溯算法介绍
回溯算法和贪心算法 回溯算法 (Backtracking Algorithms) Backtracking is a general algorithm for finding all (or som ...
- 五大算法之三--贪心算法
一.基本概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解. 贪心算法没有固定的 ...
- java调度问题的贪心算法_贪心算法——换酒问题
知识回顾 贪心算法 (greedy algorithm),又称贪婪算法. 是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法. 贪心算法在 有最优子 ...
- 任务分配算法c语言程序,程序员算法基础——贪心算法
原标题:程序员算法基础--贪心算法 前言 贪心是人类自带的能力,贪心算法是在贪心决策上进行统筹规划的统称. 比如一道常见的算法笔试题跳一跳: 有n个盒子排成一行,每个盒子上面有一个数字a[i],表示最 ...
- 数据结构与算法之美(十四)算法思想——贪心算法
目录 贪心算法介绍 贪心算法例子 1. 背包 2. 分糖果 3. 钱币找零 4. 区间覆盖 5. 区间覆盖的延伸:任务调度.教师排课 贪心算法经典应用 1. 霍夫曼编码 2. 最小生成树算法 3. 最 ...
- 贪心算法适用条件_【算法】贪心算法
概念&&介绍 贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择.也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解.所以说只有证明局部最优解在全局最优解 ...
- 【趣学算法】Day2 贪心算法——最优装载问题
14天阅读挑战赛 努力是为了不平庸~ 算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法! ❤️一名热爱Java的大一学生,希望与各位大佬共同学习进步❤️
最新文章
- 使用 git 管理 portage tree
- 刀模图是什么意思_“吃鸡”光子公布神秘图,海岛图上有44个坐标,暗示信号值取消?...
- WPF学习笔记(4):获取DataGridTemplateColumn模板定义的内容控件(转)
- 基于消息队列的分布式事务解决方案
- sql 增加链接服务器,SQL server利用脚本添加链接服务器,可设置别名
- Rotation Rose各部分的名称
- python命令提示符窗口在哪里_详解python命令提示符窗口下如何运行python脚本
- 郑州智慧岛大数据管委会_数据科学融通应用数学 ‖ 智慧岛大讲堂
- 怎么更好掌握Web前端技术?JS的跨域是怎么回事?
- 【数据结构笔记42】哈希表应用:文件中单词词频统计
- moxa串口卡Linux驱动,moxa多串口驱动下载
- 宏碁台式计算机u盘启动,宏基台式机bios设置u盘启动方法
- flvjs is not defined
- Manjaro 清理垃圾
- 几个常见的逻辑训练及参考答案
- 用Matlab实现蒙特卡洛法求心形线面积
- 将Carte部署为Windows服务
- 数据库连接查找不到数据库_查找具有受保护的健康信息的数据库
- Altium Designer出现collision警告
- 使用nvm下载安装Node