代码随想录算法训练营第二十九天|LeetCode 134.加油站、135. 分发糖果、860.柠檬水找零、406.根据身高重建队列

目录

134.加油站

135. 分发糖果

860.柠檬水找零

406.根据身高重建队列

感想


134.加油站

文档讲解:代码随想录

视频讲解:

状态:没做出来。

我写的半截代码:

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        for i in range(len(gas)):
            if gas[i]-cost[i] >= 0:
                restore = gas[i] - cost[i] + gas[i+1]
        if restore >= 0: 

        return -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 可为起始索引。

先对着测试用例模拟这个过程:遍历数组,当遇到gas[i]-cost[i] >= 0时,说明i可以作为一个起点,从i出发,接下来还是重复这个过程,restore的油量要大于cost,如果一直没有遇到可以出发的情况就返回-1。

关于过程的重复的实现,我想把if改成while,但是for循环里面套while,变量i到底在哪里++?当for循环遍历到数组最后了,如何控制其回到数组开头,构成环形遍历呢?如果可以环游一圈,怎么将起点i 记录下来呢?

正确思路:

暴力方法:

暴力的方法很明显就是O(n^2)的,关键是要模拟跑一圈的过程。for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历!

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        # 遍历每个加油站作为起点
        for i in range(len(gas)):
            rest = gas[i]-cost[i]  # 剩余油量(到达下一站index后所剩余的油量)
            index = (i+1) % len(cost)  # 设置下一站的索引(使用取模实现环形)
            #  模拟以i为起点行驶一圈
            while rest > 0 and index != i: #当油量足够到达下一站且未绕完一圈时继续行驶
                rest += gas[index] - cost[index]  # 注意是+=, 是累加的
                index = (index + 1)% len(cost)  # 继续移动到下一站(环形处理)
            #  如果以i为起点跑一圈,剩余油量非负,并且回到起始位置
            if rest >= 0 and index == i:
                return i   #  返回成功起点
        # 无解情况
        return -1

代码的逻辑和上面示例的模拟过程不太一样,代码是:离开某站时先消耗,到达下一站时才加油,结果都是一样的。

 while rest > 0 and index != i:  # 模拟以i为起点行驶一圈,如果有rest==0,那么答案就不唯一了。因为如果某次移动后 rest == 0,说明车辆刚好耗尽油量到达某个加油站 j,此时,如果从 j 出发也能完成环路(因为 rest == 0 时油箱为空,相当于从 j 重新开始)。

力扣中暴力解法超时了。

贪心算法:

观察示例解释和上面暴力方法的代码,我也发现了:整体计算过程是 全部的gas - 全部的cost,如果总油量减去总消耗大于等于零那么一定可以跑完一圈,即各个站点的 剩油量rest[i]=gas[i]-cost[i] 相加是>=0才可以。

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

下标: 0    1     2     3     4

gas:    [ 2    5     2     3      5 ]

cost:   [ 1     2    8      2     4 ]

rest:      1     3    -6     1     1

curSum: 1   4   -2(0)  1     2      (从i=0开始累加rest)

totalSum: 1    4   -2    -1   0       (最后totalSum>=0,说明是可以绕一圈的,只要找到那个起始位置就可以)

curSum:  3     6     0   1      2    (从i=3开始累加rest)    

totalSum:3     6     0   1      2

怎么确保[0,i]区间 任何一个位置作为起点 累加到 i 这里都会小于0呢,会不会有大于0的情况?如果>0,那么该起点的前半段和一定小于0,该起点正是相当于新的起始位置i+1啊,逻辑是自洽的!

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

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        curSum = 0   # 当前累计的剩余油量
        totalSum = 0  # 总剩余油量
        start = 0    # 起始位置

        for i in range(len(gas)):
            rest = gas[i]-cost[i]  # 剩余油量
            curSum += rest
            totalSum += rest  # curSum在下面可能会被清0,所以totalSum一定要自己去记录
            if curSum < 0:
                start = i+1 #   # 起始位置更新为i+1
                curSum = 0  # curSum清0,重新开始累加
        if totalSum < 0:
            return -1    # 总剩余油量totalSum小于0,说明一定无法环绕一圈
        else:
            return start  #  返回成功起点
  • 时间复杂度:O(n),仅需一次遍历。

  • 空间复杂度:O(1),仅用常数空间。

这个方法没有 也不需要在找到起始位置后 模拟环绕一圈,直接通过totalSum判断是否可以环绕一圈,通过数学性质确定起点start是否合法。

135. 分发糖果

文档讲解:代码随想录

视频讲解:

状态:没做对。

我的错误代码:

class Solution:
    def candy(self, ratings: List[int]) -> int:
        numbers = len(ratings) # 每人先有一颗糖 ,numbers是糖的总数
        for i in range(len(ratings)):
            # 首尾情况
            if i == 0:
                if ratings[i] > ratings[i+1]:
                    numbers += 1
            elif i == len(ratings)-1:
                if ratings[i-1] < ratings[i]:
                    numbers += 1
            # 中间的情况
            else:
                if ratings[i-1] < ratings[i] or ratings[i] > ratings[i+1]:
                    numbers += 1

        return numbers

对于输入   [1,2,87,87,87,2,1],

分配结果是[1,2, 2, 1, 2,  2, 1]。87比2大,单纯的number+1是不对的,应该有3颗糖,正确的分配应该是[1,2, 3, 1, 3,  2, 1],总共需要13颗糖。

错误原因:只进行了一次正向遍历,无法正确处理所有相邻关系。

正确解法:

这道题目一定是要确定一边之后,再确定另一边,如果两边一起考虑一定会顾此失彼

采用两次贪心的策略:

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

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

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

正确的解法中,不是直接用糖果总数+1,而是用candy数组记录给每个孩子分配的糖果数量,最后再sum求和。所以在第二次遍历中,要从后向前,因为,举个栗子, rating[4]与rating[5]的比较 要利用上 rating[5]与rating[6]的比较结果,即在candy[5]的基础上+1或者不变。

class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        candies = [1] * n # 糖果分配数组,每人先有一颗糖
        
        # 正向遍历
        for i in range(1,n):
            if ratings[i-1] < ratings[i]: # 右边比左边大
                candies[i] = candies[i-1] + 1
       
        # 反向遍历
        for i in range(n-2,-1,-1):
            if ratings[i] > ratings[i+1]:  # 左边比右边大
                # 两个选择中取最大才能保证糖果数量既大于左边的也大于右边的
                # candyVec[i + 1] + 1(比右边大,在右边的基础上加1),
                # candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)
                candies[i] = max(candies[i],candies[i+1]+1)

        return sum(candies)

860.柠檬水找零

文档讲解:

视频讲解:

状态:做出来了。

直接模拟收钱找零的过程。

用字典:

class Solution:
    def lemonadeChange(self, bills: List[int]) -> bool:
        if bills[0] != 5:
            return False
        chargebill = defaultdict(int)
        for i in range(len(bills)):
            if bills[i] == 5:
                chargebill[5] += 1
            elif bills[i] == 10:
                if chargebill[5] == 0:
                    return False
                chargebill[5] -= 1
                chargebill[10] += 1
            else:
                if chargebill[10] != 0 and chargebill[5] != 0:
                    chargebill[10] -= 1
                    chargebill[5] -=1
                elif chargebill[10] == 0 and chargebill[5] >= 3:
                    chargebill[5] -= 3
                else:
                    return False
        return True

直接用两个变量记录五元和十元的数量:

class Solution:
    def lemonadeChange(self, bills: List[int]) -> bool:
        if bills[0] != 5:
            return False
        a5 = 0
        b10 = 0
        for i in range(len(bills)):
            if bills[i] == 5:
                a5 += 1
            elif bills[i] == 10:
                if a5 == 0:
                    return False
                a5 -= 1
                b10 += 1
            else:
                if b10 != 0 and a5 != 0: # 先尝试用10元的找,因为5元的留着还能后续给10元的找零
                    b10 -= 1
                    a5 -=1
                elif b10 == 0 and a5 >= 3:
                    a5 -= 3
                else:
                    return False
        return True

406.根据身高重建队列

文档讲解:代码随想录

视频讲解:

状态:不会做,理解题意,但是没找到程序的切入点,手动在纸上模拟还行。因为身高h并不是升序或降序排列,k也不是升序或降序排列

提示说,做好一边再处理另一边。

按身高h来排序,从大到小排序,身高相同的话则k小的站前面

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

所以在按照身高从大到小排序后,再按照这个顺序依次进行 构造队列。

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

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

举个栗子:

people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]

排序完的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]]  完成排列
class Solution:
    def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
        people.sort(key = lambda x:(-x[0],x[1])) # sort默认是升序排序,先按身高h降序排序,再按k升序排序
        # lambda返回的是一个元组:当-x[0](维度h)相同时,再根据x[1](维度k)从小到大排序
        
        # 根据每个元素的第二个维度k,贪心算法,进行插入
        queue = []
        for p in people:
            queue.insert(p[1],p) # p[1]是当前人的k值,表示他前面应有k个身高≥他的人。
        # 因为是按排序后的身高从高到低插入的,之前插入的人都比当前人高或相等,所以当前人插入到位置p[1],前面正好有p[1]个 ≥ 他的人
        return queue
  • 时间复杂度:O(nlog n + n^2)   (排序:O(n log n) ;插入:每次 insert 是 O(n),要进行n次,所以是 O(n²) )

  • 空间复杂度:O(n)

在 Python 中,list.insert() 是一个用于在列表指定位置插入元素的方法。

list.insert(index, element)

  • index是要插入元素的索引位置element是要插入的元素

  • 该操作直接修改原列表,不返回新列表

  • 时间复杂度:O(n),因为需要移动插入位置后的所有元素

  • 如果 index > len(list),元素会插入到末尾;如果 index < -len(list),元素会插入到开头,索引越界不会报错。

  • 与 append() 区别:append() 只能在末尾添加,insert() 可以在任意位置添加

感想

学习用时:晚上1.2h+中午2h+晚上3h+中午2.5h

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值