力扣 贪心算法题集 Python

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择,就能得到问题的答案。贪心算法需要充分挖掘题目中条件,没有固定的模式,解决有贪心算法需要一定的直觉和经验。

贪心算法不是对所有问题都能得到整体最优解。能使用贪心算法解决的问题具有「贪心选择性质」。「贪心选择性质」严格意义上需要数学证明。能使用贪心算法解决的问题必须具备「无后效性」,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

从力扣官网对贪心算法的介绍来看,学贪心算法真就需要题海战术。记录一下有价值的题目吧 (持续更新中)

最长回文子串

【问题描述】

        给你一个字符串 s,找到 s 中最长的回文子串。

        如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

【样例】

输入输出
s = "babad""bab"
s = "cbbd""bb"

【解析及代码】

Manacher 算法解决

class Solution:
    def longestPalindrome(self, s):
        s = "#".join(s.join("^$"))
        # 以对应字符为中心 最长回文串的半径
        p = [0] * len(s)
        center, border = 0, 0
        for i in range(1, len(s) - 1):
            # 利用回文串的对称性进行赋值, 利用 min 防止越界
            p[i] = min(p[max(0, 2 * center - i)], max(0, border - i))
            # 中心扩展法
            while s[i - p[i] - 1] == s[i + p[i] + 1]: p[i] += 1
            # 更新回文串中心, 回文串右端点 ("#")
            if i + p[i] > border: center, border = i, i + p[i]
        i = max(range(len(s)), key=p.__getitem__)
        j = i - p[i]
        return s[j + (j & 1): i + p[i] + 1: 2]

盛水最多的容器

【问题描述】

        给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。

        找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。

        说明:你不能倾斜容器。

【样例】

输入输出
height = [1,8,6,2,5,4,8,3,7]49
height = [1,1]1

【解析及代码】

使用双指针,分别指向两端,保证容器的宽度最大。然后看两个指针对应的高度,哪个小就移动哪个,每次都计算一次,然后比较取最优

class Solution(object):
    def maxArea(self, height):
        max_area = 0
        # 初始化双指针
        l, r = 0, len(height) - 1
        while l < r:
            # 和当前最优解比较
            max_area = max(max_area, (r - l) * min(height[l], height[r]))
            if height[l] <= height[r]:
                l += 1
            else:
                r -= 1
        return max_area

跳跃游戏

【问题描述】

        给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

        数组中的每个元素代表你在该位置可以跳跃的最大长度。

        判断你是否能够到达最后一个下标。

【样例】

输入输出
nums = [2,3,1,1,4]true
nums = [3,2,1,0,4]false

【解析及代码】

思路很简单,直接看代码的注释吧

class Solution(object):
    def canJump(self, nums):
        # 可到达的最远点索引
        max_idx = 0
        for i, j in enumerate(nums):
            # 确认当前点 (索引 i) 可到达
            if max_idx >= i:
                max_idx = max(i + j, max_idx)
                if max_idx >= len(nums) - 1: return True
        return False

买卖股票的最佳时机Ⅱ

【问题描述】

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

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

【样例】

输入输出
prices = [7,1,5,3,6,4]7
prices = [1,2,3,4,5]4
prices = [7,6,4,3,1]0

【解析及代码】

比方说现在给定的 prices = [1, 2, 3, 4],最优的结果肯定是第一天买,最后一天卖出最好:4 - 1 = 3。当然这也等同于:- 1 + 2 - 2 + 3 - 3 + 4 = (- 1 + 2) + (- 2 + 3) + (- 3 + 4) = 3

所以说我们可以只扫描一次 prices,只要相邻两价格呈递增关系,就直接把差价算成利润

class Solution(object):
    def maxProfit(self, prices):
        return sum(prices[i + 1] - prices[i]
                   for i in range(len(prices) - 1) if prices[i] < prices[i + 1])

 

加油站

【问题描述】

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

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

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

【样例】

输入输出
gas = [1,2,3,4,5], cost = [3,4,5,1,2]3
gas = [2,3,4], cost = [3,4,3]-1

【解析及代码】

照上面的样例讲一下思路:当处在 0 号加油站时,开往下一个加油站会使油量 -2 (由 1 - 3 得),同理得各个加油站的前进开销为:[-2, -2, -2, 3, 3]

如果我们从 0 号加油站出发,则会在到 3 号加油站时,油量达到最低 -6 (见下图蓝色线)

如果把蓝线向上平移 6 个单位,即以 3 号加油站作为起点,则油量永远不会低于0 —— 所以前进开销累积的最小值处,就是起点 (仅当最小值 < 0 时)

当然,前进开销累积到最后,是非负值才会有起点 (即总油量 ≥ 总消耗)

class Solution(object):
    def canCompleteCircuit(self, gas, cost):
        import itertools as it
        # 转为前缀和, 并求取最小值
        res = list(it.accumulate(g - c for g, c in zip(gas, cost)))
        x = min(res)
        return -1 if res[-1] < 0 else (
            (res.index(x) + 1) % len(res) if x < 0 else 0)

分发糖果

【问题描述】

        n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

        你需要按照以下要求,给这些孩子分发糖果:

        1. 每个孩子至少分配到 1 个糖果。

        2. 相邻两个孩子评分更高的孩子会获得更多的糖果。

        请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

【样例】

输入输出
ratings = [1,0,2]5
ratings = [1,2,2]4

【解析及代码】

 将孩子的评分绘制成折线图,会发现折线由若干个峰构成,依据峰谷的位置进行划分

对某个峰,用 up_len 表示递增部分长度,用 down_len 表示递减部分长度 (均不包括峰顶,但均包括峰谷),如:up_len = 2,down_len = 3,则糖果分配是 [1, 2, 4, 3, 2, 1]

得某个峰对糖果的需求为:\sum _ {i=1} ^ {up\_len}i+\sum _ {i=1} ^ {down\_len}i +max(up\_len, down\_len) + 1

这个方法在实现过程中要注意一些边界条件,最终代码如下:

class Solution(object):
    def candy(self, ratings):
        # 记录总糖果数
        sum_ = 0
        # 升序、降序部分长度
        up_len, down_len = 0, 0

        # 计算 1 + 2 + ... + last
        deng_cha = lambda last: (1 + last) * last // 2
        # 计算峰对糖果的需求
        feng_sum = lambda up_len, down_len: deng_cha(up_len) + deng_cha(down_len) + max([up_len, down_len]) + 1

        for i in range(1, len(ratings)):
            if ratings[i] > ratings[i - 1]:
                if down_len:
                    # 经过峰谷,即形成新的峰, 结算上一个峰; 波谷是两个峰共享的, 避免重复计算应 -1
                    sum_ += feng_sum(up_len, down_len) - 1
                    # 置零升序、降序部分
                    up_len, down_len = 0, 0
                # 叠加升序部分
                up_len += 1
            # 叠加降序部分
            elif ratings[i] < ratings[i - 1]:
                down_len += 1
            # 两评分相等时,第一个评分作为峰谷,第二个评分作为新峰的峰顶/峰谷
            else:
                sum_ += feng_sum(up_len, down_len)
                # 置零升序、降序部分
                up_len, down_len = 0, 0

        # 未结算的峰
        sum_ += feng_sum(up_len, down_len)
        return sum_

去除重复字母

【问题描述】

        给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

【样例】

输入输出
s = "bcabc""abc"
s = "cbacdcbc""acdb"

【解析及代码】

字典序与字符串长度无关,Python 的 str 类型比较依据就是字典序,不会的话可以调试找找规律

拿到题首先还是先想 O(n) 的思路,从头到尾扫描一遍字符串,在扫描过程中要加入怎样的操作才可以完成任务?

以 'cabacb' 为例:

  1. 读取 'c':计入新的字符串:'c'
  2. 读取 'a','c' 后面还有,而且 'c' > 'a' (新增逆序区) → 去掉 'c',计入新的字符串:'a'
  3. 读取 'b',前面没出现过,而且 'a' < 'b' (新增顺序区),计入新的字符串:'ab'
  4. 读取 'a',前面出现过,跳过
  5. 读取 'c',前面没出现过,而且新增顺序区,计入新的字符串:'abc'
  6. 读取 'b',前面出现过,跳过

最终结果是 'abc'

class Solution(object):
    def removeDuplicateLetters(self, s):
        stringio = ''
        for i, char in enumerate(s):
            if char not in stringio:
                # 删除部分字符
                while stringio:
                    front = stringio[-1]
                    # 不产生逆序区 / 不是重复字符
                    if front < char or front not in s[i:]: break
                    stringio = stringio[:-1]
                # 追加新字符
                stringio += char
        return stringio

按要求补齐数组

【问题描述】

        给定一个已排序的正整数数组 nums ,和一个正整数 n 。从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums 中某几个数字的和来表示。

        请返回 满足上述要求的最少需要补充的数字个数

【样例】

输入输出
nums = [1,3], n = 61
nums = [1,5,10], n = 202
nums = [1,2,2], n = 50

【解析及代码】

最重要的两个条件:nums[i] ≥ 1,按照升序排列

假设输入是:nums = [2, 5], n = 30

  • 因为第一个数不是1,所以需要补入“1”
  • 第一个数是2,则可覆盖区间由 [1, 1] 变为 [1, 3]
  • 第二个数是5,如果直接加上,则可覆盖区间是 [1, 3] | [5, 3+5],缺失了4,所以先不加上。如果这时加上的数是4,则可覆盖区间变为 [1, 3] | [4, 3+4] = [1, 7],当然,小于4的数也可以,但是推算会发现4是最优的选择:记可覆盖区间的右边界为 reachable,最优的选择即为 reachable + 1
  • 再次读取第二个数,可覆盖区间由 [1, 7] 变为 [1, 12]
  • 此时数组里的数都用过了,但是还没有达到 [1, 30],所以按照步步最优补上:[13, 26]

最终补齐的数组是:[1, 2, 4, 5, 13, 26],return 4

class Solution(object):
    def minPatches(self, nums, n):
        # 需要补上的数字数, 指针
        need, pin = (0, 1) if nums[0] == 1 else (1, 0)
        # 可覆盖区间为 [1, reachable]
        reachable = 1
        while pin < len(nums):
            # 读取当前指针指向的数
            next_ = nums[pin]
            # 假如可覆盖为 [1, 3], 添加 4 的收益最大
            require = reachable + 1
            # 如果添加 5, 则无法覆盖 4
            if next_ > require:
                # 添加收益最大的 4, 指针不动
                need += 1
                reachable += require
            # 如果添加 2/3 等, 则直接叠加
            else:
                pin += 1
                reachable += next_
            if reachable >= n:
                return need
        # 当原始数组已遍历过
        while reachable < n:
            # 总是添加最优
            need += 1
            reachable += reachable + 1
        return need

递增的三元子序列

【问题描述】

        给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。

        如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false

【样例】

输入输出
nums = [1,2,3,4,5]true
nums = [5,4,3,2,1]false
nums = [2,1,5,0,4,6]true

【解析及代码】

用 p1 指向已搜索区域的最小值,用 p2 指向已搜索区域的次小值,这两个值无需关注顺序先后

class Solution(object):
    def increasingTriplet(self, nums):
        if len(nums) < 3: return False
        p1, p2 = nums[0], 1 << 32
        # 保持 p1 < p2
        for p3 in nums[1:]:
            if p2 < p3: return True
            # p2 取次小值
            if p3 > p1:
                p2 = min(p2, p3)
            # p1 取最小值
            else:
                p1 = p3
        return False

以 [7, 6, 3, 4, 1, 2, 5] 为例来推一遍

操作读取值 p3最小值 p1次小值 p2
初始化7inf
166inf
233inf
3434
4114
5212
65 (True)12

每次读取,都会选择 输出、改变 p1、改变 p2,也就是 p1 和 p2 不会同时改变

  1. 改变 p1 时,p1 只会变成更小的值,仍然满足p1 < p2,当 p2 < p3 时仍可输出正确结果
  2. 改变 p2 时,需判断 p3 > p1,p2 取 min([p2, p3]),此时 p3 在位置上一定在 p1 后,也就是改变后的 p2 也一定在 p1 后
  3. 数值上:p1 < p2 恒成立;位置上:一定存在 x 满足 p1 < x < p2,x 的位置在 p2 前

鸡蛋掉落-两枚鸡蛋

【问题描述】

        给你 2 枚相同 的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。

        已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都 会碎 ,从 f 楼层或比它低 的楼层落下的鸡蛋都 不会碎 。

        每次操作,你可以取一枚 没有碎 的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

        请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

【样例】

输入输出
n = 22
n = 10014

【解析及代码】

以第二个样例推导一下为什么答案是 14:

  • 从 14 层丢下,如果碎了就从 1~13 依次丢,如果没碎则继续以下步骤
  • 从 27 层丢下,如果碎了就从 15~26 依次丢,如果没碎则继续以下步骤
  • 从 39 层丢下,如果碎了就从 28~38 依次丢,如果没碎则继续以下步骤
  • ……

最后可以发现,如果鸡蛋一直没碎,那么我们需要把鸡蛋从 14、27、39、50、60、69、77、84、90、95、99、102、104、105 楼丢下,而它们的差分数列刚好是等差数列

不难归纳出,楼层总数 n 与答案 x 存在以下关系:

n \leq \frac{(x+1)x}{2}

x = \left \lceil \sqrt{2n + .25} - .5 \right \rceil

class Solution(object):
    def twoEggDrop(self, n):
        import math
        return int(math.ceil(math.sqrt(2 * n + .25) - .5))

  

鸡蛋掉落

【问题描述】

        给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。

        已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。

        每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

        请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

【样例】

输入输出
k = 1, n = 22
k = 2, n = 63
k = 3, n = 144

【解析及代码】

当鸡蛋个数 k = 1 时,显然答案等于 n

当鸡蛋个数 k = 2 时,根据上一题的结果可知答案等于 x = \left \lceil \sqrt{2n + .25} - .5 \right \rceil

当鸡蛋个数 k = 3 时,可以与上一题采用相似的思路

f(k, t) 表示鸡蛋个数为 k、操作次数为 t 时所能测量的最大的楼层

e.g., f(3, 5) =f(2, 4) + 1 + f(2, 3) + 1 + f(2, 2) + 1 + f(2, 1)+ 1 + f(3, 1)

其中的 1 表示 决策点:以第一个一个 1 为例,在相应的位置丢鸡蛋,碎了则在其楼层之下测试,可测量的最大楼层为 f(2, 4);如果没碎则在其楼层之上测试,可测量的最大楼层为 f(3, 4)

再枚举几个例子并进行验证后,不难归纳出以下规律:

f(k, t) = f(k, t-1) + f(k-1, t-1) + 1

f(k, t) = t + \sum_{i=1}^{t-1}f(k-1, i)

当给定 k 个鸡蛋时,利用第一条规律,我们可以以最快的速度依次计算 f(k, 1), f(k, 2), \cdots , f(k, n)

将每次计算的结果与 n 比较,便可得到最小的操作次数

class Solution(object):
    def superEggDrop(self, k, n):
        from functools import lru_cache
        import math

        @lru_cache(maxsize=None)
        def f(k, t):
            ''' k: 鸡蛋个数
                t: 操作次数
                return: n 的最大值'''
            if k == 1: return t
            if k == 2: return (t + 1) * t // 2
            # 鸡蛋个数大于 2
            return t + sum(f(k - 1, i) for i in range(1, t))

        if k == 1 or n == 1: return n
        if k == 2: return int(math.ceil(math.sqrt(2 * n + .25) - .5))
        # 鸡蛋个数大于 2: f(k, 1) = 1
        reach = 1
        for t in range(1, n):
            # f(k, t+1) = f(k, t) + f(k-1, t) + 1
            reach += f(k - 1, t) + 1
            if reach >= n: return t + 1

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

荷碧TongZJ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值