基于python的leetcode算法介绍之贪心

2023-12-20 16:30:57

零 算法介绍

首先我们需要明确什么算是贪心?

通常,当我们说一个人贪心时,我们是指这个人过分地追求物质财富或利益,常常不顾及他人利益或感受。贪心的人可能会利用他人,甚至不惜损害他人来达到自己的目的。而在算法中,贪心算法指的是一种解决问题的策略,它总是做出在当前状态下看起来最优的选择,期望通过一系列局部最优的选择,达到全局最优的结果。贪心算法的优点是实现简单,运行效率高,但在某些问题上可能无法得到最优解。

贪心算法的基本步骤如下:

  1. 对问题进行分解,将其分解为一系列相互独立的子问题。

  2. 对于每个子问题,找到一个最优解。

  3. 将所有子问题的最优解组合起来,形成问题的最终解。

需要注意的是,贪心算法并不总是能够找到最优解在应用贪心算法时,需要验证问题是否满足贪心选择性质,即通过局部最优的选择,可以导致全局最优的解。如果问题不满足贪心选择性质,那么贪心算法可能无法找到最优解。

贪心算法的应用场景包括:

  1. Dijkstra 算法:用于寻找图中的最短路径。

  2. Huffman 编码:用于构建最优前缀编码。

  3. Kruskal 算法:用于寻找最小生成树。

贪心算法在很多领域都有广泛的应用,但在使用时需要注意其局限性,并在必要时选择其他算法,如动态规划、回溯等。

一 简单示例 找零钱问题

假设你开了间小店,不能电子支付,钱柜里的货币只有 25 分、10 分、5 分和 1 分四种硬币,如果你是售货员且要找给客户 n 分钱的硬币,如何安排才能找给客人的钱既正确且硬币的个数又最少

面对贪心,我们需要首先做一个证明:即证明贪心的方式即为最优解。在这道题目中,我们从最大数开始找零,那么必然可以做到找到零钱数最少【保证每个零钱的数值尽可能大】。

既然得到这个思路,那么题解便自然生成:

def change(n, currency=[25, 10, 5, 1]):
    """
    该程序设计一个找零算法,最后返回应当找回货币的个数
    Args:
        n (int): 待找零钱
        currency (list, optional): 货币的面值有多少. Defaults to [25, 10, 5, 1].

    Returns:
        _type_: _description_
    """
    count, i = 0, 0     # count 用作统计货币个数,i 作为统计链表长度
    while n != 0 and i < len(currency):     # 当还有待找零钱,且货币面值可以继续找零的情况下进行循环
        count += n // currency[i]           # 当前面值货币可以获得几个
        n %= currency[i]                    # 剩余待找货币的面值
        i += 1                              # 判断下一个等级面值的货币的面值
    return count

print(change(43))
# 6

Leetcode例题与思路

接下来,我们列举关于Leetcode的五道例题,并通过贪心的方式进行求解:

605. 种花问题

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组 flowerbed 表示花坛,由若干 01 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ****,能否在不打破种植规则的情况下种入 n ****朵花?能则返回 true ,不能则返回 false

解题思路

这道题目,我们考虑一下如何证明?是否如果当遇到连续的3个0,我们即刻在其中中间的花圃中种植一朵花。而循环遍历整个花田即可保证不能再种植新的花朵了。但需要注意这道题目中的个例:即边界的时候只要两个连续的0就可以了,这该怎么解决呢?详情请看题解:

题解

class Solution:
    def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool:
        for i in range(len(flowerbed)):
            left = 0 if i == 0 else flowerbed[i-1]                   # 对左边界的特殊处理
            right = 0 if i == len(flowerbed)-1 else flowerbed[i+1]   # 对右边界的特殊处理
            if flowerbed[i] == 0 and left == 0 and right == 0:       # 当前节点为空,且左右为空的情况下
                flowerbed[i] = 1                                     # 在当前节点种花
                n -= 1                                               # 剩余花朵减一
        return n <= 0

409. 最长回文串

给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的 最长的回文串

在构造过程中,请注意 区分大小写 。比如 "Aa" 不能当做一个回文字符串。

解题思路

关于这道题目的解题思路稍微有一些隐式。我们需要验证,构建最长的回味字串需要满足什么条件?即:如果回文子串的长度为偶数,则表示字符串中所有元素的个数都为偶数;如果回文子串的长度为奇数,则除了中心元素外,其他元素必须为偶数。

那么构建最长的回味字串的方法是什么?即对每个字符串取最大偶数的个数,如果还剩下数字,则在加1。转换成代码如下所示:

题解

class Solution:
    def longestPalindrome(self, s: str) -> int:
        static = {}
        for i in s:
            static[i] = static[i] + 1 if i in static.keys() else 1
        remain, total = 0, 0
        for count in static.values():
            total_, remain_ = divmod(count, 2)
            remain += remain_
            total += total_
        return total * 2 + (0 if remain == 0 else 1)

121. 买卖股票的最佳时机

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

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

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

解题思路

在这道题目中,我们需要判断什么时候买入卖出是最合适的?难道是最小下标和最大下标?那必然不可行。所以我们需要思考一下怎么贪心才可以呢?这道题目其实有一些动态规划的思想在里面,所以思考起来会稍微复杂一些:

我们如何找到能买到最多钱的时候呢?很简单,就是卖出价格减去之前买入价格的时候差值最大。

换句话说,对每个元素减去之前最便宜的价格,就是当前元素的收益,所以我们仅需要这个值的大小即可。

同时,我们仅需要记录在当前元素之前的最小值,就可以通过线性复杂度来求解这道题目。

题解

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        max_value, min_index = 0, 0
        for i in range(len(prices)):
            if prices[i] < prices[min_index]:
                 min_index = i
            else:
                max_value = max(max_value, prices[i] - prices[min_index])
        return max_value

11. 盛最多水的容器

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

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

返回容器可以储存的最大水量。

**说明:**你不能倾斜容器。

解题思路

这道题目的思想也比较有意思:当我们下标减小的时候一定会减小容纳的水;但当挡板变高的时候就有可能增大容纳的水的体积。

在有了这个假设的前提下,我们从两端移动挡板,向中间移动。由于容纳体积是由短板决定的,所以我们每次移动短板即可。

转换成代码如下:

题解

class Solution:
    def maxArea(self, height: List[int]) -> int:
        i, j, res = 0, len(height) - 1, 0
        while i < j:
            if height[i] < height[j]:
                res = max(res, height[i] * (j - i))
                i += 1
            else:
                res = max(res, height[j] * (j - i))
                j -= 1
        return res

763. 划分字母区间

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

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

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

示例 1:

输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca""defegde""hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。 

解题思路

这道题也是非常有意思的一道题,其核心句其实是**同一字母最多出现在一个片段中。**通过这句话,我们就有确切的分割界限了。但如何做可以通过线性的方式来求解呢?

我给出的思路是统计每个字符出现的区间,如果两个字符串的区间相交,即可融合。于是这就变成了融合区间的题目。由于字符出现的顺序是有序的,所以在合并区间的时候,顺序合并就可以了。

其题解如下:

题解

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        index = {}
        for i in range(len(s)):
            if s[i] in index.keys():
                index[s[i]][1] = i
            else:
                index[s[i]] = [i, i]
        answer = []
        key_list = list(index.keys())
        for i, key in enumerate(key_list):
            if len(answer) == 0:
                answer.append(index[key])
            elif answer[-1][1] > index[key][0]:
                answer[-1][1] = max(index[key][1], answer[-1][1])
            else:
                answer.append(index[key])
        return [i[1] - i[0] + 1 for i in answer]

文章来源:https://blog.csdn.net/qq_44961028/article/details/135109693
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。