贪心算法记录 - 上

目录

455. 分发饼干

376. 摆动序列

53. 最大子数组和

121. 买卖股票的最佳时机

122. 买卖股票的最佳时机 II(可以买卖多次)

55. 跳跃游戏(是否能到)

45. 跳跃游戏 II (最少几步)

1005. K 次取反后最大化的数组和

134. 加油站

暴力法

贪心

763. 划分字母区间


455. 分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。

⚠️ 倒序遍历的 range 为

(len(g) - 1, -1, -1)
# 正序
(len(g))

⚠️ 遍历胃口值,不可以遍历饼干:饼干的索引是在满足条件下再移动的,遍历的胃口值是固定移动的

⚠️ 需要判断 j >= 0(当饼干遍历结束 即 j < 0 时,遍历g直至结束, 不再判断 if / cnt 不再增加)。否则g = [1,2,3], s = [] 时报错。单纯的设定 j > 0 在s只有一个元素时报错

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        #饼干索引
        j, cnt = len(s)-1, 0 
#遍历胃口值,不可以遍历饼干:饼干的索引是在满足条件下再移动的,遍历的胃口值是固定移动的
        for i in range(len(g)-1, -1, -1): #左闭右开的,所以-1,倒序遍历,所以-1
            if j>=0 and g[i]<=s[j]: #先执行i_s >= 0,防止异常
                j -= 1
                cnt += 1
        return cnt

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

  • 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

算法思想:

局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列

判断条件:prediff(num[i]-num[i-1]); curdiff(num[i+1]-num[i])。要求prediff>=0 and curdiff<0 或prediff<=0 and curdiff>0

  • 上下有平坡(删左边):prediff = 0 and curdiff<0/prediff = 0 and curdiff>0

  • 首尾元素:以上判断需要元素数量>=3,在2个的情况下不成立,但在2个元素不相同的情况下也计为2个摆动                                                                                                                   加虚拟头节点:prediff=0-->有一个摆动                                                                             默认数组最右有个摆动:答案初始为1-->有一个摆动。   

   ​​​​​​

  • 单调坡度有平坡                                                                                                               只需要在 这个坡度 摆动变化的时候,更新prediff就行,这样prediff在 单调区间有平坡的时候 就不会发生变化,造成我们的误判:将prediff = curdiff的更新写在if语句里

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        prediff, curdiff = 0, 0
        ans = 1
        if len(nums)<2: ans =len(nums) 
#边界处理
        for i in range(0,len(nums)-1):
#最后一个节点已经算是一个摆动,计算到倒数第二个就可以
            curdiff = nums[i+1] - nums[i]
            if (prediff>=0 and curdiff <0) or (prediff<=0 and curdiff>0):
                ans+=1
                prediff = curdiff#
        return ans

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。

暴力

class Solution:
    def maxSubArray(self, nums):
        result = float('-inf')  # 初始化结果为负无穷大
        count = 0
        for i in range(len(nums)):  # 设置起始位置
            count = 0
            for j in range(i, len(nums)):  # 从起始位置i开始遍历寻找最大值
                count += nums[j]
                result = max(count, result)  # 更新最大值
        return result

贪心

局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。

全局最优:选取最大“连续和”

⚠️ 两个变量

  • cnt做加法记录子数组和,当为负时置0
  • ans和cnt比较后,记录最大值,防止加一个数后反而变小

两个变量初始值:cnt = 0,做加法;cnt = -inf 初始化为无穷小,防止数组元素都为负

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        ans = -inf
        cnt = 0
        for i in range(len(nums)):
            cnt += nums[i]
            if cnt>ans:
                ans = cnt
            if cnt < 0:
                cnt = 0
#若当前连续和小于0,及时抛弃 防止拖累下面的和
        return ans

121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

取最左最小值,取最右最大值,那么得到的差值就是最大利润。

一直往后遍历,而minprice停在左边的最小值上,price在取到右边最大值时,得最大利润。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        minprice = inf
        maxprofit = 0
        for price in prices:
            minprice = min(minprice, price) #取左边最小值
            maxprofit = max(maxprofit, price-minprice) #取最大区间利润
        return maxprofit

122. 买卖股票的最佳时机 II(可以买卖多次)

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

局部最优:收集每天的为正的利润,全局最优:求得最大利润。

假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。

相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。

此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!

那么根据 prices 可以得到每天的利润序列:(prices[i] - prices[i - 1]).....(prices[1] - prices[0])。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        ans = 0
        for i in range(1,len(prices)):
            # up = prices[i]-prices[i-1]
            #if up > 0:
            #    ans+=up
            ans += max(prices[i]-prices[i-1],0)
        return ans

55. 跳跃游戏(是否能到)

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

贪心算法

局部最优解:每次取最大跳跃步数(取最大覆盖范围)

整体最优解:最后得到整体最大覆盖范围,看是否能到终点。

当前位置元素如果是 3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢?

跳几步无所谓,关键在于可跳的覆盖范围

i 每次移动只能在 cover 的范围内移动

            

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        n = len(nums)
        can_reach = 0
        if len(nums) == 1:
            return True
        for i in range(n):
            if i <= can_reach:
# i 只能 <= 当前可到达的范围                
#如果当前下标已经大于可到达的最远处,false(可以用while写,for不可以加条件)
                can_reach = max(nums[i] + i, can_reach)
                if can_reach >= n-1:
                    return True
        return False
class Solution:
    def canJump(self, nums: List[int]) -> bool:
        cover = 0
        if len(nums) == 1: return True
        i = 0
        # python不支持动态修改for循环中变量,使用while循环代替
        while i <= cover:
            cover = max(i + nums[i], cover)
            if cover >= len(nums) - 1: return True
            i += 1
        return False

45. 跳跃游戏 II (最少几步)

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i] 
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

 从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最小步数!

需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖

移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。

⚠️ 到达当前最远范围时,需要判断是否到终点了,决定是否步数加一

  • 如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。
  • 如果当前覆盖最远距离下标就是是集合终点,这一步刚好到,直接输出。
class Solution:
    def jump(self, nums: List[int]) -> int:
        if len(nums)==1:return 0
        cur_cover = 0
        nxt_cover = 0
        cnt = 0
        for i in range(len(nums)):
            nxt_cover = max(nums[i]+i,nxt_cover) # 下一步可到的范围
            if i == cur_cover: # 到达这一步最远范围
                if i !=len(nums)-1: # 还没到终点时才会更新ans和可到范围
                    cnt += 1
                    cur_cover = nxt_cover 
                    if nxt_cover>=len(nums)-1: return cnt
                else: # 到终点直接输出步数
                    return cnt

1005. K 次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。

重复这个过程恰好 k 次。可以多次选择同一个下标 i 。

以这种方式修改数组后,返回数组 可能的最大和 。

局部最优:让绝对值大的负数变为正数,当前数值达到最大。

整体最优:整个数组和达到最大。

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

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

全局最优:整个 数组和 达到最大。

❌我本来的思路:排序,而不是按照绝对值大小降序排序

        导致的问题:当负数都反转完毕,序列无序,无法找最小的正数继续反转,绝对值逆序排序能保证最后一个数永远是正数序列里的最小值。

class Solution:
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        nums.sort(key=abs, reverse=True) # 按绝对值倒序排列数组
        """
        原理:先反转负数才能得最大值,且先反转负数中绝对值大的(即最小负数)
             情况一:没反转完负数k就为0,直接求和
             情况二:负数反转完毕,都为正数,k不为0:
                k为偶数,无需再反转(可将一个数反转偶数次,结果不变),直接求和
                k为基数,还需反转一次,此时都为正数,反转最小数,即nums[-1]
        """
        for i in range(len(nums)): 
            if nums[i] < 0 and k > 0 : # 当元素为负且还需要反转时,反转并将k-1
                nums[i] = -nums[i]
                k-=1
        if k % 2 == 1: # 对k模二有余数时,说明还需反转
            nums[-1] = -nums[-1]
        return sum(nums)

134. 加油站

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

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

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

暴力法

i 遍历找到满足gas[i] > cost[i]的起点

rest 记录剩余油量

index 记录下一个加油站的索引,因为是环形,所以需要不能光是i+1,要做取余操作

while循环模拟从起点跑一圈的过程(环形适合用while)

当剩余油量大于0 且 还没跑完一圈(加油站的索引不等于起点)时,循环,继续向下一个加油站判断油量够不够。

超时

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        for i in range(len(cost)):
            rest = gas[i] - cost[i]  # 记录剩余油量
            index = (i + 1) % len(cost)  # 下一个加油站的索引

            while rest > 0 and index != i:  # 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了)
                rest += gas[index] - cost[index]  # 更新剩余油量
                index = (index + 1) % len(cost)  # 更新下一个加油站的索引

            if rest >= 0 and index == i:  # 如果以i为起点跑一圈,剩余油量>=0,并且回到起始位置
                return i  # 返回起始位置i

        return -1  # 所有起始位置都无法环绕一圈,返回-1

贪心

总油量减去总消耗大于等于零则一定可以跑一圈,主要是求起点

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

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

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:  # 当前累计剩余油量curSum小于0
                start = i + 1  # 起始位置更新为i+1
                curSum = 0  # curSum重新从0开始累计
        
        if totalSum < 0:
            return -1  # 总剩余油量totalSum小于0,说明无法环绕一圈
        return start

763. 划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        hash = [0]*26
        for i in range(len(s)):
            hash[ord(s[i])-ord('a')]= i 
#记录每一个字母的最远位置,ord()返回字符对应的 Unicode 码点(整数值)
        left ,right = 0, 0
        ans = []
        for i in range(len(s)):
            right = max(right, hash[ord(s[i])-ord('a')]) 
#找右边界
            if i == right:
#索引=右边界,遍历到右边界,找到当前段内最远的重复字母
                ans.append(right-left+1)
                left = i+1 
#第二段左边界,从当前位置+1
        return ans

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值