[Leetcode] [Tutorial] 动态规划


70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例:
输入:n = 3
输出:3

Solution

我们先通过直观的方式思考问题:对于给定的阶梯数n,每次爬1阶或2阶,因此有两种选择,可以分解为以下子问题:

  • 爬上n-1阶,再爬1阶
  • 爬上n-2阶,再爬2阶

所以解就是上述两个子问题的和。

class Solution:
    def climbStairs(self, n: int) -> int:
        if n == 1:
            return 1
        if n == 2:
            return 2
        return self.climbStairs(n - 1) + self.climbStairs(n - 2)

上述直接递归的缺点在于重复计算,那么我们可以保存已经计算的子问题的结果。

class Solution:
    def climbStairs(self, n: int) -> int:
        memo = [0] * (n + 1)
        return self.climb_with_memoization(n, memo)

    def climb_with_memoization(self, n: int, memo: list) -> int:
        if n == 1:
            return 1
        if n == 2:
            return 2
        if memo[n] > 0:
            return memo[n]
        memo[n] = self.climb_with_memoization(n - 1, memo) + self.climb_with_memoization(n - 2, memo)
        return memo[n]

记忆化递归依然使用了递归结构,而递归结构可以转换为迭代结构。我们可以从底部开始计算,逐步构建解,直到达到n。

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        dp = [0] * (n + 1)
        dp[1], dp[2] = 1, 2
        for i in range(3, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]

观察上述代码,我们发现每次计算只使用了前两个状态的值。因此,完全没有必要保存所有的值,只需保存最后两个状态。

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        a, b = 1, 2
        for _ in range(3, n + 1):
            a, b = b, a + b
        return b

118. 杨辉三角

给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

示例:
输入: numRows = 5
输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

Solution

class Solution:
    def generate(self, numRows: int) -> List[List[int]]:
        triangle = [[1] * i for i in range(1, numRows + 1)]
        for i in range(2, numRows):
            triangle[i][0], triangle[i][i] = 1, 1
            for j in range(1, i):
                triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j]
        return triangle

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例:
输入:[2,7,9,3,1]
输出:12

Solution

对于第i个房子,有两种选择:

  • 偷:此时金额为前i-2个房屋的最大金额加上第i个房子的金额。
  • 不偷:此时金额为前i-1个房屋的最大金额。
class Solution:
    def rob(self, nums: List[int]) -> int:
        if not nums:
            return 0

        if len(nums) == 1:
            return nums[0]

        dp = [0] * len(nums)
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2, len(nums)):
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
        return dp[len(nums) - 1]

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例:
输入:coins = [1, 2, 5], amount = 11
输出:3

Solution

对于硬币找零问题,你可能会想到的贪心策略是:在每一步,选择不超过剩余金额的最大硬币面额。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        coins.sort(reverse=True)  # 将硬币按降序排序
        total_coins = 0
        for coin in coins:
            num_coins = amount // coin  # 使用尽可能多的当前面额硬币
            amount -= num_coins * coin
            total_coins += num_coins

            if amount == 0:
                return total_coins
        
        return -1

然而,对于这个问题,贪心算法并不总是有效。但是,这个问题没有“贪心选择属性”。即在每一步做出局部最优选择并不一定会导致全局最优解。

最优解可能需要考虑不仅仅是当前步骤的信息,还需要考虑如何通过整体的考虑和协调来获得最优解,这正是动态规划方法所能提供的。

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [amount + 1] * (amount + 1)
        dp[0] = 0
        
        for i in range(1, amount + 1):
            for coin in coins:
                if coin <= i:
                    dp[i] = min(dp[i], dp[i - coin] + 1)
                    
        return dp[amount] if dp[amount] <= amount else -1

只需要初始化 dp[0] 为 0 即可。这是因为,对于任何 coin(其中 coin 是硬币的面值),其 dp 值会在循环中被自动计算为 1。

279. 完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例:
输入:n = 13
输出:2
解释:13 = 4 + 9

Solution

class Solution:
    def numSquares(self, n: int) -> int:
        squares = [i**2 for i in range(1, int(n**0.5) + 1)]
        dp = [float('inf')] * (n + 1)
        dp[0] = 0
        for i in range(1, n + 1):
            for num in squares:
                dp[i] = min(dp[i - num] + 1, dp[i])
        return dp[n]

如果将每个数字视为一个节点,两个数字之间的差是一个完全平方数,那么这两个数字就是相连的。所以我们的目标是找到从0到n的最短路径。

class Solution:
    def numSquares(self, n: int) -> int:
        # 获取所有小于 n 的完全平方数
        squares = [i**2 for i in range(1, int(n**0.5) + 1)]
        
        queue = deque([(n, 0)])  # (当前值, 步数)
        visited = set()

        while queue:
            num, step = queue.popleft()

            # 对于每一个完全平方数
            for square in squares:
                next_num = num - square
                if next_num < 0:
                    break
                if next_num == 0:
                    return step + 1
                if next_num not in visited:
                    visited.add(next_num)
                    queue.append((next_num, step + 1))

        return n  # 这行理论上不会执行,但为了完整性保留它

139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true

示例 2:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false

Solution

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        if not s:
            return True
        n = len(s)
        succ = [0]
        for i in range(1, n + 1):
            for j in succ:
                if s[j: i] in wordDict:
                    succ.append(i)
                    break
        return True if n in succ else False

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

Solution

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
            
        n = len(nums)
        dp = [1] * n
        for i in range(n):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

这个问题还可以使用贪心策略来解决。

当你遇到一个比当前子序列末尾更大的数字时,尽量将其添加到子序列中。当我们添加数字到子序列中时,也可能会阻止后续数字被添加进来。这意味着我们不仅要考虑长度,还要考虑子序列的最后一个数字的大小。实际上,对于某个长度的子序列,结尾数字越小越好,因为这样我们有更大的机会在未来遇到比它更大的数字。

贪心算法提示我们总是尝试将当前数字添加到已有的最长递增子序列中。但如何快速找到它应该插入的位置呢?这就是二分搜索派上用场的地方。我们可以使用二分搜索来查找该数字应该插入的位置,并根据需要更新当前位置的值。

tails数组的核心思想是:对于长度为i的递增子序列,我们想知道所有这些子序列的结尾中的最小值是多少,因为这样可以使后续数字有更大的机会添加进来。而tails的索引表示子序列的长度,值表示对应长度的递增子序列的最小结尾值。

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        tails = []
        for num in nums:
            # 二分搜索
            left, right = 0, len(tails) - 1
            while left <= right:
                mid = (left + right) // 2
                if tails[mid] < num:
                    left = mid + 1
                else:
                    right = mid - 1
            
            # 如果num大于tails中的所有元素,则直接追加到tails中
            # 否则替换找到的位置
            if left == len(tails):
                tails.append(num)
            else:
                tails[left] = num
        
        return len(tails)

152. 乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

子数组 是数组的连续子序列。

示例:
输入: nums = [2,3,-2,4]
输出: 6

Solution

由于负数和负数的乘积是正数,所以我们同时需要跟踪以当前数字结束的子数组的最大和最小乘积。如果当前数字是负数,那么在乘以当前数字时,最大值会变成最小值,而最小值会变成最大值。

所以我们需要在每个位置记录两个状态:

max_product[i]表示以nums[i]结尾的子数组的最大乘积。
min_product[i]表示以nums[i]结尾的子数组的最小乘积。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        max_product = [0] * len(nums)
        min_product = [0] * len(nums)
        max_product[0], min_product[0] = nums[0], nums[0]
        for i in range(1, len(nums)):
            max_product[i] = max(nums[i], nums[i]*max_product[i-1], nums[i]*min_product[i-1])
            min_product[i] = min(nums[i], nums[i]*max_product[i-1], nums[i]*min_product[i-1])
        return max(max_product)

我们实际上可以不使用数组来保存每个位置的结果,而只需要保存上一个位置的结果即可。

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        if not nums:
            return 0

        prev_max = nums[0]
        prev_min = nums[0]
        global_max = nums[0]

        for i in range(1, len(nums)):
            # 使用临时变量来保存当前状态,因为我们需要前一个状态的prev_max和prev_min
            curr_max = max(nums[i], nums[i] * prev_max, nums[i] * prev_min)
            curr_min = min(nums[i], nums[i] * prev_max, nums[i] * prev_min)
            # 更新全局最大乘积
            global_max = max(global_max, curr_max)
            # 更新前一个数字的状态
            prev_max, prev_min = curr_max, curr_min

        return global_max

416. 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例:
输入:nums = [1,5,11,5]
输出:true

Solution

猜你喜欢

转载自blog.csdn.net/weixin_45427144/article/details/132219846