分享好友 资讯首页 资讯分类 切换频道
代码随性录Day29 本周小结贪心算法,134. 加油站,135. 分发糖果,860.柠檬水找零,406.根据身高重建队列。
2024-12-14 00:29  浏览:89

一说到股票问题,一般都会想到动态规划,其实有时候贪心更有效

在贪心算法:买卖股票的最佳时机II (opens new window)中,讲到只能多次买卖一支股票,如何获取最大利润。

这道题目理解利润拆分是关键点 不要整块的去看,而是把整体利润拆为每天的利润,就很容易想到贪心了。

局部最优:只收集每天的正利润,全局最优:得到最大利润

如果正利润连续上了,相当于连续持有股票,而本题并不需要计算具体的区间。

如图

在贪心算法:跳跃游戏 (opens new window)中是给你一个数组看能否跳到终点。

本题贪心的关键是不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的

那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点

贪心算法局部最优解:移动下标每次取最大跳跃步数(取最大覆盖范围,整体最优解:最后得到整体最大覆盖范围,看是否能到终点

如果覆盖范围覆盖到了终点,就表示一定可以跳过去。

如图

这道题目:贪心算法:跳跃游戏II (opens new window)可就有点难了。

本题解题关键在于以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点

那么局部最优:求当前这步的最大覆盖,那么尽可能多走,到达覆盖范围的终点,只需要一步。整体最优:达到终点,步数最少。

如图

注意图中的移动下标是到当前这步覆盖的最远距离(下标2的位置,此时没有到终点,只能增加第二步来扩大覆盖范围

在贪心算法:跳跃游戏II (opens new window)中我给出了两个版本的代码。

其实本质都是超过当前覆盖范围,步数就加一,但版本一需要考虑当前覆盖最远距离下标是不是数组终点的情况。

而版本二就比较统一的,超过范围,步数就加一,但在移动下标的范围了做了文章。

即如果覆盖最远距离下标是倒数第二点:直接加一就行,默认一定可以到终点。如图: 

如果覆盖最远距离下标不是倒数第二点,说明本次覆盖已经到终点了。如图: 

有的录友认为版本一好理解,有的录友认为版本二好理解,其实掌握一种就可以了,也不用非要比拼一下代码的简洁性,简洁程度都差不多了。

我个人倾向于版本一的写法,思路清晰一点,版本二会有点绕。

这道题目:贪心算法:K次取反后最大化的数组和 (opens new window)就比较简单了,用简单题来讲一讲贪心的思想。

这里其实用了两次贪心

第一次贪心:局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。

处理之后,如果K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。

第二次贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了,全局最优:整个 数组和 达到最大。

例外一位录友留言给出一个很好的建议,因为文中是使用快排,仔细看题题目中限定了数据范围是正负一百,所以可以使用桶排序,这样时间复杂度就可以优化为$O(n)$了。但可能代码要复杂一些了。

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明:

  • 如果题目有解,该答案即为唯一答案。
  • 输入数组均为非空数组,且长度相同。
  • 输入数组中的元素均为非负数。

示例 1: 输入:

  • gas = [1,2,3,4,5]
  • cost = [3,4,5,1,2]

输出: 3 解释:

  • 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
  • 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
  • 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
  • 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
  • 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
  • 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
  • 因此,3 可为起始索引。

示例 2: 输入:

  • gas = [2,3,4]

  • cost = [3,4,3]

  • 输出: -1

  • 解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油。开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油。开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油。你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。因此,无论怎样,你都不可能绕环路行驶一周。

暴力方法

暴力的方法很明显就是O(n^2)的,遍历每一个加油站为起点的情况,模拟一圈。

如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。

暴力的方法思路比较简单,但代码写起来也不是很容易,关键是要模拟跑一圈的过程。

for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while

贪心算法(方法一

直接从全局进行贪心选择,情况如下

  • 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的

  • 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。

  • 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。

不认为这种方式是贪心算法,因为没有找出局部最优,而是直接从全局最优的角度上思考问题

但这种解法又说不出是什么方法,这就是一个从全局角度选取最优解的模拟操作。

所以对于本解法是贪心,我持保留意见

但不管怎么说,解法毕竟还是巧妙的,不用过于执着于其名字称呼。

贪心算法(方法二

可以换一个思路,首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。

每个加油站的剩余量rest[i]为gas[i] - cost[i]。

i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。

如图

那么为什么一旦[0,i] 区间和为负数,起始位置就可以是i+1呢,i+1后面就不会出现更大的负数

如果出现更大的负数,就是更新i,那么起始位置又变成新的i+1了。

那有没有可能 [0,i] 区间 选某一个作为起点,累加到 i这里 curSum是不会小于零呢? 如图

如果 curSum<0 说明 区间和1 + 区间和2 < 0, 那么 假设从上图中的位置开始计数curSum不会小于0的话,就是 区间和2>0。

区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择起始位置了。

那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置

局部最优可以推出全局最优,找不出反例,试试贪心

 

时间复杂度: O(n)

空间复杂度: O(1)

 

时间复杂度: O(n)

空间复杂度: O(1)

 

时间复杂度: O(n)

空间复杂度: O(1)

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

你需要按照以下要求,帮助老师给这些孩子分发糖果

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢

示例 1:

  • 输入: [1,0,2]
  • 输出: 5
  • 解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。

示例 2:

  • 输入: [1,2,2]
  • 输出: 4
  • 解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边如果两边一起考虑一定会顾此失彼

先确定右边评分大于左边的情况(也就是从前向后遍历

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

局部最优可以推出全局最优。

如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1

如图

再确定左孩子大于右孩子的情况(从后向前遍历

遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢

因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。

如果从前向后遍历,rating[5]与rating[4]的比较 就不能用上 rating[5]与rating[6]的比较结果了 。如图

所以确定左孩子大于右孩子的情况一定要从后向前遍历

如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量,一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。

那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

局部最优可以推出全局最优。

所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多

如图

这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。

那么本题我采用了两次贪心的策略

  • 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
  • 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。

 

时间复杂度: O(n)

空间复杂度: O(n)

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。

顾客排队购买你的产品(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1

  • 输入:[5,5,5,10,20]
  • 输出:true
  • 解释
    • 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
    • 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
    • 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
    • 由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2

  • 输入:[5,5,10]
  • 输出:true

示例 3

  • 输入:[10,10]
  • 输出:false

示例 4

  • 输入:[5,5,10,10,20]
  • 输出:false
  • 解释
    • 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
    • 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
    • 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
    • 由于不是每位顾客都得到了正确的找零,所以答案是 false。

提示

  • 0 <= bills.length <= 10000
  • bills[i] 不是 5 就是 10 或是 20

要怎么找零才能保证完成全部账单的找零呢

但仔细一琢磨就会发现,可供我们做判断的空间非常少

只需要维护三种金额的数量,5,10和20。

有如下三种情况

  • 情况一:账单是5,直接收下。
  • 情况二:账单是10,消耗一个5,增加一个10
  • 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5

此时大家就发现 情况一,情况二,都是固定策略,都不用我们来做分析了,而唯一不确定的其实在情况三。

而情况三逻辑也不复杂甚至感觉纯模拟就可以了,其实情况三这里是有贪心的。

账单是20的情况,为什么要优先消耗一个10和一个5呢

因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能

所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。

局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法

力扣题目链接

 

时间复杂度: O(n)

空间复杂度: O(1)

力扣题目链接

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例 1

  • 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
  • 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
  • 解释
    • 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
    • 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
    • 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
    • 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
    • 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
    • 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
    • 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2

  • 输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
  • 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

提示

  • 1 <= people.length <= 2000
  • 0 <= hi <= 10^6
  • 0 <= ki < people.length

题目数据确保队列可以被重建

本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后再按照另一个维度重新排列。

其实如果大家认真做了135. 分发糖果 (opens new window),就会发现和此题有点点的像。

在135. 分发糖果 (opens new window)我就强调过一次,遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。

如果两个维度一起考虑一定会顾此失彼

对于本题相信大家困惑的点是先确定k还是先确定h呢,也就是究竟先按h排序呢,还是先按照k排序呢

如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。

那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面,让高个子在前面。

此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高

那么只需要按照k为下标重新插入队列就可以了,为什么呢

以图中{5,2} 为例

按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。

所以在按照身高从大到小排序后

局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性

全局最优:最后都做完插入操作,整个队列满足题目队列属性

局部最优可推出全局最优,找不出反例,那就试试贪心。

一些同学可能也会疑惑,你怎么知道局部最优就可以推出全局最优呢? 有数学证明么

在贪心系列开篇词关于贪心算法,你该了解这些! (opens new window)中,我已经讲过了这个问题了。

刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心,至于严格的数学证明,就不在讨论范围内了。

如果没有读过关于贪心算法,你该了解这些! (opens new window)的同学建议读一下,相信对贪心就有初步的了解了。

回归本题,整个插入过程如下

排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]]

插入的过程

  • 插入[7,0]:[[7,0]]
  • 插入[7,1]:[[7,0],[7,1]]
  • 插入[6,1]:[[7,0],[6,1],[7,1]]
  • 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
  • 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
  • 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]

此时就按照题目的要求完成了重新排列。

关于出现两个维度一起考虑的情况,我们已经做过两道题目了,另一道就是135. 分发糖果 (opens new window)。

其技巧都是确定一边然后贪心另一边,两边一起考虑,就会顾此失彼

这道题目可以说比135. 分发糖果 (opens new window)难不少,其贪心的策略也是比较巧妙。

对使用某一种语言容器的使用,特性的选择都会不同程度上影响效率

 

时间复杂度: O(n^2)

空间复杂度: O(n)

    以上就是本篇文章【代码随性录Day29 本周小结贪心算法,134. 加油站,135. 分发糖果,860.柠檬水找零,406.根据身高重建队列。】的全部内容了,欢迎阅览 ! 文章地址:http://w.yusign.com/news/180.html 
     资讯      企业新闻      行情      企业黄页      同类资讯      首页      网站地图      返回首页 述古往 http://w.yusign.com/mobile/ , 查看更多   
最新新闻
阳光分期全国客服电话-助力客户高效解决问题
阳光分期全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多拨几
好借优品全国客服电话-助力客户高效解决问题
好借优品全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多拨几
小白易购全国客服电话-助力客户高效解决问题
小白易购全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多拨几
美兴小额贷款全国客服电话-助力客户高效解决问题
美兴小额贷款全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多
爱施德网络小额贷款全国客服电话-助力客户高效解决问题
爱施德网络小额贷款全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;
拍拍分期全国客服电话-助力客户高效解决问题
拍拍分期全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多拨几
天天分期全国客服电话-助力客户高效解决问题
天天分期全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多拨几
中邮消费金融全国客服电话-助力客户高效解决问题
中邮消费金融全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多
招联消费金融全国客服电话-助力客户高效解决问题
招联消费金融全国客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您多
铁路12306全国人工客服电话-助力客户高效解决问题
铁路12306全国人工客服电话;(00861-38960-86246)—解决客户问题:{00861-57307-87089}—用户至上,用心服务。热线容易占线;请您