[Python-贪心算法]

贪心算法

贪心算法的本质是从每个阶段的局部最优推出全局最优,而且没有固定的算法套路,需要我们手动模拟,如果感觉可以用贪心算法那么就直接冲。

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题

  • 找出适合的贪心策略

  • 求解每一个子问题的最优解

  • 将局部最优解堆叠成全局最优解

贪心算法只有多见题,多做题才能在遇到问题的时候快速找到做题思路,多说无益,直接开始练习。

455. 分发饼干

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

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

解题思路

本题要求我们尽可能多的满足孩子,因此,为了避免浪费,即出现用大饼干喂给小胃口孩子的情况出现。我们是不是应该要让大饼干去满足胃口大的孩子?

因此,我们每一层的局部最优就是让每个饼干都能够满足胃口最接近饼干大小的孩子。

可以先对两个数组进行倒序排布,这样胃口大的孩子与尺寸大的饼干都排布在了前面。下面我们只需要对饼干序列进行遍历,每次都与不同尺寸的饼干进行比较,若遇到饼干尺寸大于孩子胃口的情况,我们就计数一次;反之,就切换到下一个孩子再与饼干比较。根据以上思路,代码如下:

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g = sorted(g, reverse = True)
        s = sorted(s, reverse = True)
        ans = 0 
        index = 0 #用于标识饼干的遍历位置
        for i in range(len(g)):
            if index < len(s) and s[index] >= g[i]:
                ans += 1
                index += 1
        return ans

376. 摆动序列

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

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

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

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

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

解题思路

这个题其实容易想到本质是找列表中的峰值个数的,即只要一个元素大于前一个元素,且大于后一个元素;或者小于前一个元素与后一个元素,都可以被认定为是摆动点。为了方便表示,我们可以用pre来代表前两个元素的差值,cur来代表后两个元素的差值。即pre > 0 and cur < 0 or pre < 0 and cur > 0

列表中还可能会出现平坡,即连续的相等元素出现。这种情况就需要删除元素了,但是如果按上面的思路来的话,那么平坡的元素都会被删除掉,因此我们还需要保留一个平坡元素,即在判断时加上一个等于0的情况。pre >= 0 and cur < 0 or pre <= 0 and cur > 0这样判断,对于列表[1,2,2,2,1]来说,第一个2的pre大于0,cur等于0,不算作摆动点;第二个2的pre与cur都等于0,不算作摆动点;第三个2的pre等于0,cur小于0,算作摆动点。

还有首尾元素没有考虑了,对于首元素,我们可以默认他的pre为0,因为不管他的cur是大于还是小于0都会被算作是摆动点,cur等于0的时候,即头部出现两个连续相等元素时,第一个元素也不会被考虑进去。对于尾元素,我们可以默认他为一个摆动点。因为在只有两个不相等元素时,摆动点的个数为2,而且相等时,由于首元素的判断方式,会将首元素删除掉,所以这样考虑恰好满足题目的要求。因此代码如下:

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        ans = 1 #默认最右边的为1个摆动点
        pre = 0 #假设前面是平坡
        cur = 0 #pre和cur分别为当前节点的前后坡度
        for i in range(len(nums) - 1):
            cur = nums[i + 1] - nums[i]
            if pre >= 0 and cur < 0 or pre <= 0 and cur > 0:
                ans += 1
            pre = cur
        return ans

这样写就是每到一个新节点时,都会更新其pre与cur,这会导致一个问题,在出现单调序列中的平坡元素时,会将平坡元素的最后一个元素算入进去,例如:[1,2,2,3]。这个列表中正确的摆动点应有两个,即1和3,但是按照上面的方法,最后一个2元素也会被算进去,怎么解决呢?我们只有在摆动点时才更新摆动点的前状态,这样就会避免了单调上升或下降时容易将平坡元素考虑进去的问题了。例如在[1,2,2,3]列表中,1是第一个摆动点,这样pre就赋值为了大于0,第一个2不是摆动点,因此pre不动。第二个2的pre和cur都大于0,所以也不会被算为摆动点了。3元素默认为摆动点,所以满足题目要求。

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        ans = 1 #默认最右边的为1个摆动点
        pre = 0 #假设前面是平坡
        cur = 0 #pre和cur分别为当前节点的前后坡度
        for i in range(len(nums) - 1):
            cur = nums[i + 1] - nums[i]
            if pre >= 0 and cur < 0 or pre <= 0 and cur > 0:
                ans += 1
                pre = cur
        return ans

53. 最大子数组和

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

子数组 是数组中的一个连续部分。

解题思路

求连续子序列的最大值时,如果出现了负数,我们就需要重置连续和,因为负数的连续和会拖累后面的连续子序列求和。而且同时,我们还需要在每次循环时都记录下当前最大的数值作为最大值,不断比较。代码如下:

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        result = float('-inf') #最小值
        sum_ = 0 #用于记录连续和
        for i in range(len(nums)):
            sum_ += nums[i]
            if sum_ > result:
                result = sum_
            if sum_ < 0:
                sum_ = 0
        return result

122. 买卖股票的最佳时机 II

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

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

返回 你能获得的 最大 利润

解题思路

这个题给了我们每一天的股票价格,让我们求出最大利润。对于这种题目,我们可以按照股票价格画出每天的价格波动图,价格上升的每一段相加就是我们所要求的最大利润。但这也就是说不可能出现买一次股票之后一直不卖出去,留到价格最大再卖出的利润比每一段相加的利润还要大的情况出现。

单说有点抽象,下面画个图来表示。

[Python-贪心算法]_第1张图片

这样看就比较清晰明了了,每一段相加一定要比更少段相加的利润要大,因为中间会有下降。

我们也可以从数学角度来考虑,比如从1到6这一段,他的利润为6-1,是不是也可以表示成(6-3)+(3-5)+(5-1)?即每一段的利润总和,中间如果有小于0的一小段利润,那么肯定买两个股票更合适。如果没有小于0的一小段呢,那么就恰好两者相等。因此,我们完全可以将每小段的大于0的利润相加。

代码如下:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        res = 0 #用于标记最大利润
        for i in range(1,len(prices)):
            det = prices[i] - prices[i - 1]
            if det > 0:
                res += det
        return res

55. 跳跃游戏

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

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

解题思路

我们可以从下标的角度进行分析,数组的值表示当前所能走的最远距离,而下标则是代表当前节点的位置,因此,下标加值就等于当前节点最远能够到达的位置。

我们再来分析下什么时候不能到达最后一个下标。肯定是到达了数组中的0的时候,因此,我们可以将此题的判断标准变为是否一定能到达值为0的节点位置,如果一定能到达节点0,那么肯定到不了终点;反之一定能到达终点。

我们来模拟一下这个过程。对于列表[3,2,1,0,4],其能到达最远的节点位置列表为[3,3,3,3,8],可以看到节点0的下标为3,而之前三项所能达到的最远节点都为3,因此不能到达终点。

那我们就知道了,判断是否能够到达最终位置可以以节点0之前的节点是否有能够超过节点0的最远位置的节点即可。代码如下:

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        if len(nums) == 1:
            return True
        index = nums[0] #第一个元素所能达到的最远位置
        for i in range(len(nums)):
            index = max(i + nums[i],index) #表示所能达到的最远位置
            if index >= len(nums) - 1:
                return True
            if nums[i] == 0:
                if i >= index:
                    return False
                else:
                    continue
        return True

45. 跳跃游戏 II

给定一个长度为 n0 索引整数数组 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
        ans = 0 #用于记录次数
        i = 0 #起始位置
        while i < len(nums):
            sum_ = nums[i + 1] + i + 1 #当前节点的覆盖范围
            index = 1
            for j in range(1,nums[i] + 1):
                if i + j >= len(nums) - 1:
                    return ans + 1
                if nums[i + j] + i + j >= sum_:
                    sum_ = nums[i + j] + i + j
                    index = j
            i += index
            ans += 1

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

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

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

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

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

解题思路

这道题的贪心思路其实还是挺好想的,当k小于负数的个数时,为了得到最大和,我们需要把尽可能多的绝对值大的负数取反。当k大于负数的个数时,我们需要将剩余的次数全用在最小的数上。因此,代码如下:

class Solution:
    def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
        nums = sorted(nums)
        cnt = 0
        #k比负数还要少的情况,直接把尽量多的负数变正即可
        for i in range(len(nums)):
            if nums[i] < 0 :
                cnt += 1
                nums[i] = -nums[i]
                if cnt >= k:
                    break
        #如果k比负数多,先对k对2取余,然后分情况处理
        if k > cnt:
            if (k - cnt) % 2 == 0: #正好是偶次数
                return sum(nums)
            else: #奇次数
                nums = sorted(nums)
                nums[0] = -nums[0]
                return sum(nums)
        else:
            return sum(nums)

134. 加油站

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

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

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

解题思路

首先要明确一点,当路径中的净油量小于0的时候,肯定是回不到起点位置的。因此当路径中的净油量大于0,才会回到起始位置。

又因为路径是闭环的,所以路径中的总净油量等于两段小路径的净油量之和,因此我们在遍历的过程中,一旦遇到了净油量小于0的情况,就要把起始位置后移到该位置的后面,再开始遍历。代码如下:

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        total_sum = 0 #表示总路径中的总净油量
        sum_ = 0 #表示起始节点开始的总净油量
        start = 0 #表示起始位置
        for i in range(len(gas)):
            total_sum += gas[i] - cost[i]
            sum_ += gas[i] - cost[i]
            if sum_ < 0:
                start = i + 1
                sum_ = 0
        if total_sum < 0:
            return -1
        else:
            return start

你可能感兴趣的:(python,贪心算法,开发语言)