代码随想录算法训练营第34天 | 134. 加油站(逼近) 135. 分发糖果 + 406. 根据身高重建队列(两个维度) 860.柠檬水找零(模拟)

代码随想录系列文章目录

贪心篇



134. 加油站

题目链接
这道题我觉得挺难的,绕回个圈输出下标,暴力解法就不放在这里了

思路1

直接从全局进行贪心选择,情况如下:
首先对数组进行一个遍历,维护一个差的和cursum += 每对gas[i] - cost[i]的差, 维护一个min_,记录一下每对gas[i] - cost[i] 差最大的情况

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

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

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

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        cursum = 0
        min_ = 100001
        for i in range(len(gas)):
            rest = gas[i] - cost[i]
            cursum += rest
            if cursum < min_:
                min_ = cursum
        
        if cursum < 0: return -1   #情况1
        if min_ >= 0: return 0     #情况2
        #情况3
        for i in range(len(gas)-1, -1, -1):
            rest = gas[i] - cost[i]
            min_ += rest
            if min_ >= 0: 
                return i
        return -1

思路2(代码短)

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

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

i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。
在这里插入图片描述
那么为什么一旦[i,j] 区间和为负数,起始位置就可以是j+1呢,j+1后面就不会出现更大的负数?

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

而且j之前出现了多少负数,j后面就会出现多少正数,因为耗油总和是大于零的(前提我们已经确定了一定可以跑完全程)。

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

也就是当前累加rest[i]和 curSum一旦小于0, 起始位置更新为i+1,curSum从0开始

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        cursum = 0
        totalsum = 0
        start = 0
        for i in range(len(gas)):
            cursum += gas[i] - cost[i]
            totalsum += gas[i] - cost[i]
            if cursum < 0:   #当前累加rest[i]和 curSum一旦小于0
                start = i+1  #起始位置更新为i+1
                cursum = 0   #curSum从0开始
        if totalsum < 0: return -1
        return start

135. 分发糖果

题目链接

思路简述,ratings[i], 如果大于左右两边的,分到的糖果candynums[i], 也应该大于左右俩边的。我们应该维护一个和ratings 等长的数组candynums,表示分发的糖果数。

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

1.从前向后遍历,确定右边大于左边的情况
右边评分比左边大,右边的孩子就多一个糖果

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

2.从后往前遍历(充分利用上前一次的比较结果),确定左边大于右边的情况
如果 ratings[i] > ratings[i + 1],此时candynums[i](第i个小孩的糖果数量)就有两个选择了,一个是candynums[i + 1] + 1(从右边这个加1得到的糖果数量),一个是之前从前向后遍历一遍之后的candynums[i](之前比较右孩子大于左孩子得到的糖果数量)。
那么又要贪心了,局部最优:取candynums[i + 1] + 1 和 candynums[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的 全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。

class Solution:
    def candy(self, ratings: List[int]) -> int:
        candynums = [1] * len(ratings) #糖果数组,初试化保证每个小孩至少有一个
        
        #从前向后遍历,确定右边大于左边时的糖果数组
        for i in range(1,len(candynums)):
            if ratings[i] > ratings[i-1]:
                candynums[i] = candynums[i-1] + 1
        #从后向前遍历,确定左边大于右边时的糖果数组
        for i in range(len(candynums)-2, -1, -1):
            if ratings[i] > ratings[i+1]:
                candynums[i] = max(candynums[i], candynums[i+1]+1)  #保证相邻Ratings大的,分到的糖多
        
        return sum(candynums)

406. 根据身高重建队列

题目链接
本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后在按照另一个维度重新排列。所以本题和135分发糖果有点像,都属于有两个维度的问题

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

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

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

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

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

以图中{5,2} 为例:
在这里插入图片描述
按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。

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

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

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

class Solution:
    def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
        # 先按照h维度的身高顺序从高到低排序。确定第一个维度
        # lambda返回的是一个元组:当-x[0](维度h)相同时,再根据x[1](维度k)从小到大排序
        people.sort(key=lambda x: (-x[0], x[1]))
        que = []
	
	# 根据每个元素的第二个维度k,贪心算法,进行插入
        # people已经排序过了:同一高度时k值小的排前面。
        for p in people:
            que.insert(p[1], p)
        return que

860.柠檬水找零

题目链接
这道题其实可以直接用模拟去做,维护好5元和10元的数量就好了

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

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

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

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

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

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

class Solution:
    def lemonadeChange(self, bills: List[int]) -> bool:
        #模拟去做
        five, ten = 0, 0
        for bill in bills:
            if bill == 5:
                five += 1
            elif bill == 10:
                if five < 1: return False
                else:
                    five -= 1
                    ten += 1
            else:
                if ten > 0 and five > 0:
                    ten -= 1
                    five -= 1
                elif five >= 3:
                    five -= 3
                else: return False
        return True
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值