LeetCode刷题笔记(算法思想 四)

LeetCode刷题笔记(算法思想 四)

  • 七、动态规划
    • 斐波那契数列
      • 70. 爬楼梯
      • 198. 打家劫舍
      • 213. 打家劫舍 II
      • 信件错排
      • 母牛生产
    • 矩阵路径
      • 64. 最小路径和
      • 62. 不同路径
      • 63. 不同路径 II
    • 数组区间
      • 303. 区域和检索 - 数组不可变
      • 413. 等差数列划分
    • 分割整数
      • 343. 整数拆分
      • 279. 完全平方数
      • 91. 解码方法
    • 最长递增子序列
      • 300. 最长上升子序列
      • 646. 最长数对链
      • 376. 摆动序列
    • 最长公共子序列
      • 1143. 最长公共子序列
    • 背包问题
      • 0-1 背包问题
      • 416. 分割等和子集
      • 494. 目标和
      • 474. 一和零
      • 322. 零钱兑换
      • 518. 零钱兑换 II
      • 139. 单词拆分
      • 377. 组合总和 Ⅳ
    • 股票交易
      • 309. 最佳买卖股票时机含冷冻期
      • 714. 买卖股票的最佳时机含手续费
      • 123. 买卖股票的最佳时机 III
      • 188. 买卖股票的最佳时机 IV
    • 字符串编辑
      • 583. 两个字符串的删除操作
      • 72. 编辑距离
      • 650. 只有两个键的键盘

七、动态规划

斐波那契数列

70. 爬楼梯

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

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

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶

思路: 假设要爬n=10级台阶,登上第10级台阶是通过两种走法得来:①从9级台阶走一级;②从8级台阶走两级的。而登上第9级台阶又是通过两种走法得来:①从8级台阶走一级;②从7级台阶走两级。不难发现这里存在重复子问题,其递推式为 f(n) = f(n-1) + f(n-2),并且f(1)和f(2)已知,f(1) = 1 上一级台阶只有一种走法;f(2) = 2 上两级台阶有两种走法。可以使用动态规划dp解决此问题。

代码实现:

class Solution:
    def climbStairs(self, n: int) -> int:
        # 动态规划dp
        # 递推式:f(n) = f(n-1) + f(n-2)
        methods = [0,1,2]  # 最前面补个0是为了让 methods[1]=1, methods[2]=2,更直观
        if n > 2:
            for i in range(n-2):
                m = methods[-1]+methods[-2]
                methods.append(m)
        return methods[n]

198. 打家劫舍

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

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

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

0 <= nums.length <= 100
0 <= nums[i] <= 400

思路: 当只有一间房屋时,能偷到的最高金额就是偷这间屋子能得到的钱,当只有两间房屋时,由于房屋相邻不能同时偷窃,只能偷其中的一间房屋,因此选择两间中钱更多的偷窃。当有k间房时(k>2),有两个偷窃方案:1.偷第k间,此时不能偷k-1间,偷窃总金额为 第k间的钱+偷前k-2间所能获得的最大钱;2.不偷第k间,偷窃总金额为 偷前k-1间所能获得的最大钱。这两个方案取最大,就是偷k间房子所能获得的最大金额。动态规划:dp[k] = max(dp[k-2] + nums[k], dp[k-1])。

代码实现:

class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) == 0:
            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)):
            # 第i间房。可以选择偷或者不偷,偷的话就是第i间的钱加上偷前面i-2间最高金额;不偷的话就是偷前面i-1间最高金额
            dp[i] = max(dp[i-2] + nums[i], dp[i-1])

        return dp[len(nums)-1]

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

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

示例 1:

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

思路: 在198题基础上增加了环形的条件,可以将本题的环状排列房间拆成两个单排排列的房间,即可用198的思路。1.不偷第一间房就可以偷最后一间房,即nums[1:];2.偷第一间房就不能偷最后一间房,即nums[:-1]。

代码实现:

class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return 0
        if len(nums) <= 3:
            return max(nums)

        def rob1(nums):
            dp = [0] * len(nums)
            dp[0] = nums[0]
            dp[1] = max(nums[0],nums[1])

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

        p1 = rob1(nums[1:])
        p2 = rob1(nums[:-1])
        return max(p1[-1],p2[-1])

信件错排

题目描述:有 N 个信 和 N 个信封,它们被打乱,求错误装信方式的数量(全装错)。

思路: 定义一个数组dp存储错误方式数量,dp[i]表示 i 个信全装错的方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据i,k是否相等,有两种情况:(1)i == k,交换 i 和 j 的信之后,他俩的信装对了位置,但其余 i-2 封信有 dp[i-2]种错误装信方式。由于 j 有 i-1 种取值,因此有 (i-1) * dp[i-2] 种错误装信方式;(2)i != k,交换 i 和 j 的信后,第 i 个信在正确位置,其余 i-1 封信有 dp[i-1] 种错误方式。由于 j 有 i-1 种取值,因此有 (i-1) * dp[i-1]种错误装信方式。 综上,dp[i] = (i-1) * dp[i-2] + (i-1) * dp[i-1]。

此题也可以用高中排列组合知识,用所有信件的装信方式 减去 装对的方式(有一个装对也叫装对),例如5封信时,所有装信方式为:5! = 120,装对的方式为:C(5,1) * 9 + C(5,2) * 2 + C(5,3) * 1 + 1 = 76,故5封信全装错一共有:120 - 76 = 44 种。

代码实现:

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

母牛生产

题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。

思路: 前三年只有初始母牛能生小牛,因为所有牛都不会死,所以第 N-1 年的所有牛 dp[N-1] 都会活到第N年;成熟的母牛每年生一头小牛,所以第 N-3 年中的所有牛到第N年都会各生一头小牛,一共dp[N-3]头;所以第N年牛的总数是:dp[N] = dp[N-1] + dp[N-3]。此题也可以列举出前几项之后找规律。

代码实现:

class Solution:
    def cow_num(self, n):
        if n <= 0:
            return 0
        if n <= 3:
            return n

        dp = [0] * (n+1)
        dp[0] = 0
        dp[1] = 1
        dp[2] = 2
        dp[3] = 3
        for i in range(4, n+1):
            dp[i] = dp[i-1] + dp[i-3]
        return dp[n]

矩阵路径

64. 最小路径和

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例:

输入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。

思路: 一开始想到dfs,但其实这是典型的动态规划。因为每个单元格只能从自己的左边单元格和自己的上方单元格走到。新建一个dp二维矩阵用于保存每一步的最小路径和,需要考虑四种情况:(1)没有左边和上边元素(grid[0][0]);(2)只有左边元素:dp[x][y] = dp[x][y-1] + grid[x][y];(3)只有上边元素:dp[x][y] = dp[x-1][y] + grid[x][y];(4)上边元素和左边元素都有:dp[x][y] = min(dp[x-1][y] + grid[x][y], dp[x][y-1] + grid[x][y])。最后返回dp[-1][-1]即可。更简单的方法是,不需要新建dp,直接遍历grid对其进行修改即可,因为遍历过的元素都在当前元素的左上方,不会再使用到,这样做可以节省空间,使空间复杂度变为:O(1)。

代码实现:

class Solution:
    def minPathSum(self, grid):
        if len(grid) == 0:
            return 0
        dp = [[0 for _ in range(len(grid[0]))] for _ in range(len(grid))]

        for x in range(len(grid)):
            for y in range(len(grid[0])):
                # 遍历到grid[0][0]时,进入第一个if条件,由于dp每个元素都初始化为0,故dp[0][-1]==0,dp[0][0]==1
                if x == 0:   # 只有左边元素
                    dp[x][y] = dp[x][y-1] + grid[x][y]
                elif y == 0:   # 只有上边元素
                    dp[x][y] = dp[x-1][y] + grid[x][y]
                else:
                    dp[x][y] = min(dp[x][y-1] + grid[x][y], dp[x-1][y] + grid[x][y])

        # return dp[len(grid)-1][len(grid[0])-1]
        return dp[-1][-1]
# 不开辟新的dp空间,直接覆盖原grid,空间复杂度为O(1)
class Solution:
    def minPathSum(self, grid: [[int]]) -> int:
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                if i == j == 0: continue
                elif i == 0:  grid[i][j] = grid[i][j-1] + grid[i][j]
                elif j == 0:  grid[i][j] = grid[i-1][j] + grid[i][j]
                else: grid[i][j] = min(grid[i-1][j], grid[i][j-1]) + grid[i][j]
        return grid[-1][-1]

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?
LeetCode刷题笔记(算法思想 四)_第1张图片
例如,上图是一个7 x 3 的网格。有多少可能的路径?

示例 1:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右

示例 2:

输入: m = 7, n = 3
输出: 28

提示:

1 <= m, n <= 100
题目数据保证答案小于等于 2 * 10 ^ 9

思路: 利用动态规划,构建二维列表dp,由于机器人每次只能向下或向右移动一步,所以二维数组的第一行和第一列都是1,其余的单元格是自己左边单元格与上边单元格的和,最后返回dp[-1][-1]。

排列组合法:由于机器人每次只能向下或向右移动一步,要走到右下角一定是向右走 m-1 步,向下走 n-1 步。也就是总共要走 m-1+n-1 == m+n-2 步,其中有 m-1 步是向右走的。这就是从 m+n-2 步中选择 m-1步向右的走法,即有:C(m+n-2, m-1) 中不同的走法。

代码实现:

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        if m == 0 or n == 0:
            return 0

        dp = [[0 for _ in range(m)] for _ in range(n)]
        for x in range(n):
            for y in range(m):
                if x == 0 or y == 0:
                    dp[x][y] = 1
                else:
                    dp[x][y] = dp[x-1][y] + dp[x][y-1]

        return dp[-1][-1]

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
LeetCode刷题笔记(算法思想 四)_第2张图片

网格中的障碍物和空位置分别用 1 和 0 来表示。

说明:m 和 n 的值均不超过 100。

示例 1:

输入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

思路: 本题在62题基础上还是使用动态规划,首先若网格不存在或者起点和终点上有障碍,直接返回0。在初始化上需要注意,dp[0][0] = 1,第一行若当前格子没有障碍,dp的值就是它左边格子的值;第一列若当前格子没有障碍,dp的值就是它上面格子的值。其他位置若无障碍,值为左边格子与上面格子的和。

代码实现:

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        if not obstacleGrid:
            return 0
        if obstacleGrid[0][0] == 1 or obstacleGrid[-1][-1] == 1:
            return 0
        dp = [[0 for _ in range(len(obstacleGrid[0]))] for _ in range(len(obstacleGrid))]
        dp[0][0] = 1

        for i in range(1,len(obstacleGrid)):
            if obstacleGrid[i][0] != 1:
                dp[i][0] = dp[i-1][0]
        for j in range(1,len(obstacleGrid[0])):
            if obstacleGrid[0][j] != 1:
                dp[0][j] = dp[0][j-1]

        for i in range(1,len(obstacleGrid)):
            for j in range(1,len(obstacleGrid[0])):
                if obstacleGrid[i][j] != 1:
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]
        
        return dp[-1][-1]

数组区间

303. 区域和检索 - 数组不可变

给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

示例:

给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

说明:

你可以假设数组不可变。
会多次调用 sumRange 方法。

思路: 动态规划,建立一个一维数组dp保存前缀和,当 i>0 时,从 i 到 j 的和就是:dp[j] - dp[i-1];当 i == 0 时, 从 i 到 j 的和就是:dp[j]。本题一开始提交超时,原因是把全部逻辑都写在了需要多次执行的sumRange函数里,每次执行sumRange都要生成一个dp,十分耗时,需要注意审题,本题给了一个 _init_ 函数,把主要逻辑都写在 _init_ 里即可解决超时问题。

代码实现:

class NumArray:

    def __init__(self, nums: List[int]):
        if not nums:
            return
        self.dp = [0 for _ in range(len(nums))]
        self.dp[0] = nums[0]
        for index in range(1, len(nums)):
            self.dp[index] = nums[index] + self.dp[index-1]
        print(self.dp)

    def sumRange(self, i: int, j: int) -> int:
        return self.dp[j] - self.dp[i-1] if i > 0 else self.dp[j]

# Your NumArray object will be instantiated and called as such:
# obj = NumArray(nums)
# param_1 = obj.sumRange(i,j)

413. 等差数列划分

如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,以下数列为等差数列:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

以下数列不是等差数列。

1, 1, 2, 5, 7

数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P

如果满足以下条件,则称子数组(P, Q)为等差数组:

元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。

函数要返回数组 A 中所有为等差数组的子数组个数。

示例:

A = [1, 2, 3, 4]

返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。

思路: 动态规划,建立一个以一维数组dp记录每个位置对应的等差数组的数目,找规律发现若等差数列后面一个元素还属于这个等差数列,dp[i] = dp[i-1] + 1,所以只用遍历一遍数组A,判断 A[i] - A[i-1] 和 A[i-1] - A[i-2] 是否相等,最后对dp求和,即为整个原数组A拥有的等差数列的个数。

数学公式法:首先遍历原数组A,用数组diff记录相邻两个元素之间的差值;然后遍历diff,用数组L记录相同差值连续的数目,有m个差值相同就说明有一个等差数列长度为 m+1,之后对L里的每个数都利用公式计算等差数列的个数,把每个结果相加就是整个原数组A拥有的等差数列的个数。因为等差数列的条件要求至少有3个数,所以当大等差数列长度为 n 时,长度为3的小等差数列有 n-2 个,长度为4的等差数列有 n-3 个 … 长度为 n 的等差数列有1个。这个规律也是等差数列,利用等差数列求和公式:[ (1 + (n-2)) * (n-2) ] / 2 = [ (n-2) * (n-1) ] / 2。

代码实现:

# 动态规划法:
class Solution:
    def numberOfArithmeticSlices(self, A) -> int:
        if len(A) < 3:
            return 0
        dp = [0 for _ in range(len(A))]
        sum = 0
        for i in range(2,len(A)):
            if A[i] - A[i-1] == A[i-1] - A[i-2]:
                dp[i] = dp[i-1] + 1
                sum += dp[i]
        return sum
# 数学公式法:
class Solution:
    def numberOfArithmeticSlices(self, A) -> int:
        if len(A) < 3:
            return 0
        diff = [0 for _ in range(len(A)-1)]
        for i in range(1,len(A)):
            diff[i-1] = A[i] - A[i-1]
        print(diff)

        L = []
        count = 1
        for j in range(1,len(diff)):
            if diff[j] == diff[j-1]:
                count += 1
            else:
                L.append(count+1)
                count = 1

        sum = 0
        for num in L:
            sum = sum + (num-2) * (num-1) // 2  # 数学公式求和
        return sum

分割整数

343. 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

说明: 你可以假设 n 不小于 2 且不大于 58。

思路: 利用动态规划,设置两层循环,外层循环 i 就是要求的 n (从1开始到n),内层循环 j 从1到 i ,每次循环得到 dp[i] = max(j*(i-j), j*dp[i-j], dp[i]),因为 (i-j) 不一定比 dp[i-j] 大,而 dp[i-j] 是已经算过的最优解,所以要几个对比取最大。

代码实现:

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

279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:

输入: n = 12
输出: 3 
解释: 12 = 4 + 4 + 4.

示例 2:

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

思路: 利用动态规划,首先要计算出不大于 n 的平方数,然后设置两层循环,外层 i 从1到 n,内层取出每个算得的平方数,每次循环计算 dp[i] = min(dp[i], dp[i-square]+1)。注意因为计算每个 dp[i] 都是取min,所以初始化dp数组时要把所有元素初始化为最大,也就是正无穷float(‘inf’);一个技巧是将 dp[0] 设置为0,这样可以简化逻辑,因为 dp[1] = 1 即 dp[0] + 1 = 1。

此题另一种解法见:搜索 -> BFS -> 279. 完全平方数。

代码实现:

class Solution:
    def numSquares(self, n: int) -> int:
        import math
        square_nums = [i**2 for i in range(0, int(math.sqrt(n))+1)] # 求出小于n的平方数
        dp = [float('inf')] * (n+1)  # float('inf')表示正无穷,由于下面要取min,所以要初始化为最大
        dp[0] = 0
        for i in range(1, n+1):
            for square in square_nums:
                if i < square:
                    break
                dp[i] = min(dp[i], dp[i-square]+1)
        return dp[n]

91. 解码方法

一条包含字母 A-Z 的消息通过以下方式进行了编码:

'A' -> 1
'B' -> 2
...
'Z' -> 26

给定一个只包含数字的非空字符串,请计算解码方法的总数。

示例 1:

输入: "12"
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。

示例 2:

输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。

思路: 这道题和70题爬楼梯很像,最后一个字母有可能是由一个数字或两个数字转化而来,设数字串 s 的前 i 个数字揭秘成字母串有 dp[i] 种方式,那么有:dp[i] = dp[i-1] + dp[i-2],dp[i-1]表示最后一个数字解密成一个字母;dp[i-2]表示最后两个数字解密成一个字母。利用条件判断分别处理特殊情况:例如dp[i-2]表示最后两个数字解密成一个字母的情况必须要至少有两个数字;例如最后两个字母大于26,像55、01等是不存在dp[i-2]这种情况的。

代码实现:

class Solution:
    def numDecodings(self, s: str) -> int:
        if len(s) == 0:
            return 0
        dp = [0] * (len(s)+1)
        dp[0] = 1
        for i in range(1, len(s)+1):
            t = int(s[i-1])
            if t >= 1 and t <= 9:
                dp[i] += dp[i-1]  # 最后一个数字解密成一个字母
            if i >= 2:  # 下面这种情况至少要有两个字符
                t = int(s[i-2])*10 + int(s[i-1])
                if t >= 10 and t <= 26:
                    dp[i] += dp[i-2]  # 最后两个数字解密成一个一个字母
        return dp[-1]

最长递增子序列

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。

进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

思路: 动态规划,两层循环,外层循环 i 遍历整个数组,内层循环 j 从0 到 i ,当 nums[j] < nums[i] 时,最长上升子序列长度+1。状态转移方程为:dp[i] = max(dp[i], dp[j] + 1) for j in [0, i) 。

代码实现:

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

646. 最长数对链

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。

给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 :

输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]

注意:

给出数对的个数在 [1, 1000] 范围内。

思路: 因为题目不要求连接数对时按照数组顺序,也就是可以任意从数组中选取合适地数对进行连接,为了避免遗漏,需要先对数组排序(按第一个数)。采用双层循环,当 i < j 且 pairs[i][1] < pairs[j][0] 时,扩展数对链,更新 dp[j] = max(dp[j], dp[i] + 1)。

代码实现:

class Solution:
    def findLongestChain(self, pairs: List[List[int]]) -> int:
        dp = [1] * len(pairs)
        pairs.sort()

        for j in range(len(pairs)):
            for i in range(j):
                if pairs[i][1] < pairs[j][0]:
                    dp[j] = max(dp[j], dp[i]+1)
                    
        return max(dp)

376. 摆动序列

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

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

输入: [1,7,4,9,2,5]
输出: 6 
解释: 整个序列均为摆动序列。

示例 2:

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

示例 3:

输入: [1,2,3,4,5,6,7,8,9]
输出: 2

进阶:
你能否用 O(n) 时间复杂度完成此题?

思路:
LeetCode刷题笔记(算法思想 四)_第3张图片

将数组绘制成折线图,可以清晰看见波峰和波谷,摇摆序列就是要求波峰和波谷的总个数,在图中波峰波谷交替出现。定义 up 表示波峰的个数,down 表示波谷的个数;遍历数组,每次对比当前元素和他前一个元素,更新 up 和 down的值,最终返回up和down中较大的值即为答案。

代码实现:

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        up, down = 1, 1
        if len(nums) < 2: 
            return len(nums)
        for i in range(1, len(nums)):
            if nums[i] > nums[i - 1]:
                up = down + 1
            if nums[i] < nums[i - 1]:
                down = up + 1
        return max(up, down)

最长公共子序列

1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

提示:

1 <= text1.length <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。

思路: 此类题型基本都涉及动态规划,首先构建二维dp,当 text1[i] == text2[j] 时,说明这两个字符是公共的字符,只需考察其子问题:dp[i-1][j-1]的长度即可,在此基础上+1;当 text1[i] != text2[j] 时,说明这两个不是公共的字符,需要考察其两个子问题,dp[i-1][j] 和 dp[i][j-1],取两者之中较大的。

代码实现:

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        m, n = len(text1), len(text2)
        # 构建 DP table 和 base case
        dp = [[0] * (n+1) for _ in range(m+1)]
        # 进行状态转移
        for i in range(1, m+1):
            for j in range(1, n+1):
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = 1 + dp[i-1][j-1]
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])

        return dp[-1][-1]

背包问题

0-1 背包问题

有N件物品和一个容量为V的背包。这些物品有两个属性:重量 w 和价值 v。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思路: 定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品重量不超过 j 的情况下能达到的最大价值。设第 i 件物品重量为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

(1)第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
(2)第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。

第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)。

代码实现:

# 0-1 背包:
# N 表示物品数量,V 表示背包容量,weight数组存储N个物品的重量,value数组存储N个物品的价值
def bag01(N, V, weight, value):
    dp = [[0 for _ in range(V+1)] for _ in range(N+1)]
    for i in range(1, N+1):
        w = weight[i-1]
        v = value[i-1]
        for j in range(1, V+1):
            if j >= w:    # 第i件物品的重量不大于背包还剩的体积
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v)
            else:
                dp[i][j] = dp[i-1][j]
    return dp[-1][-1]

空间优化:
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,dp[j] = max(dp[j], dp[j-w] + v)。

因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。

代码实现:

# 0-1 背包:空间优化
def bag0_1(N, V, weight, value):
    dp = [0 for _ in range(V+1)]
    for i in range(1, N+1):
        w = weight[i-1]
        v = value[i-1]
        for j in range(V, 0, -1):
            if j >= w:    # 第i件物品的重量不大于背包还剩的体积
                dp[j] = max(dp[j], dp[j-w] + v)
            else:
                dp[j] = dp[j]
    return dp[-1]

416. 分割等和子集

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

注意:

每个数组中的元素不会超过 100
数组的大小不会超过 200

示例 1:

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

输入: [1, 2, 3, 5]

输出: false

解释: 数组不能分割成两个元素和相等的子集.

思路: 此题的难点在于将其转化为 0-1背包问题。对于给定的数组中每一个数选或者不选,数字的大小类似于物品的重量,使其能够装满容量为数组总和一半的背包,dp[i][j] 表示在前 i 个物品选或者不选可以装满容量 j 的背包。第 i 个物品有两种选择:不选择装 i 时,dp[i][j] = dp[i-1][j];选择装 i 时,dp[i][j] = dp[i-1][j-nums[i-1]],i,j从1开始。最后可将空间优化,二维数组dp变为一维数组dp。

代码实现:

# 动态规划二维dp
class Solution:
    def canPartition(self, nums) -> bool:
        Sum = sum(nums)
        if Sum % 2 != 0:
            return False
        s = Sum // 2
        dp = [[False for _ in range(s+1)] for _ in range(len(nums)+1)]
        for i in range(len(nums)+1):
            dp[i][0] = True

        for i in range(1, len(nums)+1):
            for j in range(1, s+1):
                if j >= nums[i-1]:
                    dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]]
                else:
                    dp[i][j] = dp[i-1][j]
        return dp[-1][-1]
# 动态规划一维dp
class Solution:
    def canPartition(self, nums) -> bool:
        Sum = sum(nums)
        if Sum % 2 != 0:
            return False
        s = Sum // 2
        dp = [False for _ in range(s+1)]
        dp[0] = True

        for i in range(0, len(nums)):
            for j in range(s, 0, -1):
                if j >= nums[i]:
                    dp[j] = dp[j] or dp[j-nums[i]]
                else:
                    dp[j] = dp[j]
        return dp[-1]

494. 目标和

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:

数组非空,且长度不会超过 20 。
初始的数组的和不会超过 1000 。
保证返回的最终结果能被 32 位整数存下。

思路:
LeetCode刷题笔记(算法思想 四)_第4张图片
本题也是背包问题,用dp[i][j] 表示数组中的前 i 个元素,组成和为 j 的方案数。对于nums[i] ,这道题不是考虑选不选nums[i],而是考虑nums[i]是加还是减,故状态转移方程为:dp[ i ][ j ] = dp[ i - 1 ][ j - nums[ i ] ] + dp[ i - 1 ][ j + nums[ i ] ],可以理解为 nums[i] 执行加会有一个方案数,nums[i] 执行减也会有一个方案数,dp[ i ][ j ]的结果就是把这两种情况的方案数加起来。dp表格的行表示的是数组里的每个数(分别执行加减操作),dp的列应该是数组的数的加减范围,例如[1,1,1,1,1]最大是全加起来 = 5,最小是全减 = -5,dp的列就应该是从 -5到5一共11列。另外初始化有一个需要注意的是nums[0] 可能为0,若nums[0] == 0,dp[0][sum] 需要初始化为2,因为加0减0都得0。

代码实现:

class Solution:
    def findTargetSumWays(self, nums: List[int], S: int) -> int:
        Sum = sum(nums)
        if abs(Sum) < abs(S):
            return 0
        length = 2 * Sum + 1
        height = len(nums)
        dp = [[0 for _ in range(length)] for _ in range(height)]
        # 初始化:
        if nums[0] == 0:
            dp[0][Sum] = 2
        else:
            dp[0][Sum + nums[0]] = 1
            dp[0][Sum - nums[0]] = 1

        for i in range(1, height):
            for j in range(length):
                l = dp[i-1][j - nums[i]] if 0 <= j - nums[i] < length else 0
                r = dp[i-1][j + nums[i]] if 0 <= j + nums[i] < length else 0
                dp[i][j] = l + r
        return dp[height-1][Sum+S]

474. 一和零

在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。

你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

注意:

给定 0 和 1 的数量都不会超过 100。
给定字符串数组的长度不会超过 600。

示例 1:

输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4

解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。

示例 2:

输入: Array = {"10", "0", "1"}, m = 1, n = 1
输出: 2

解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。

思路: 本题是一个多维费用的0-1背包问题,有两个背包大小,0的数量和1的数量,并且直观来看,需要一个三维数组dp。题目问啥,就把啥定义为状态 ,所以 dp[k][i][j] 表示前 k 个字符串,还剩 i 个0 和 j 个1的字符串的最大数量。对于第 k 个字符串,就有选择和不选择两种情况:不选择,dp[k][i][j] = dp[k-1][i][j] ;选择, dp[k][i][j] = dp[k][i-第k个字符串使用0的个数][j-第k个字符串使用1的个数] + 1。

代码实现:

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[[0]*(n+1) for _ in range(m+1)] for _ in range(len(strs)+1)]
        for k in range(1, len(strs)+1):
            zero = strs[k - 1].count('0')
            one = strs[k - 1].count('1')
            for i in range(m+1):
                for j in range(n+1):
                    dp[k][i][j] = dp[k-1][i][j]
                    if i >= zero and j >= one and dp[k][i][j] < dp[k-1][i-zero][j-one] + 1:
                        dp[k][i][j] = dp[k-1][i-zero][j-one] + 1
        return dp[-1][-1][-1]

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1

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

思路: 因为硬币可以重复使用,所以本题是完全背包问题,使用长度为(amount+1)的一维数组dp,dp[amount] 表示凑成总金额所需最少的硬币数。要设置两层循环,外层循环遍历硬币面额,内层循环从 coin 到 amount+1,状态转移方程为 dp[i] = min(dp[i], dp[i-coin]+1),由于方程里是要求min,所以数组初始化成正无穷float(‘inf’),最后判断若 dp[amount] 是正无穷则返回 -1,反之返回dp[amount]。

代码实现:

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf') for _ in range(amount+1)]
        dp[0] = 0

        for coin in coins:
            for i in range(coin, amount+1):
                dp[i] = min(dp[i], dp[i-coin]+1)
        return dp[amount] if dp[amount] != float('inf') else -1

518. 零钱兑换 II

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。

示例 3:

输入: amount = 10, coins = [10] 
输出: 1

注意:

你可以假设:

0 <= amount (总金额) <= 5000
1 <= coin (硬币面额) <= 5000
硬币种类不超过 500 种
结果符合 32 位符号整数

思路: 本题还是完全背包问题,定义 dp[i][j] 表示用前 i 种硬币凑成 j 金额时的硬币组合数。以本题例子来看:

dp[3][5] = dp[i][j] = dp[i-1][j - 0 * coins[i-1]]     面值5不拿
                    + dp[i-1][j - 1 * coins[i-1]]     面值5拿1个
                    + dp[i-1][j - 2 * coins[i-1]]     面值5拿2个
                    + ...  
                    + dp[i-1][j - k * coins[i-1]]     面值5拿k个

需要三层循环 i,j,k,状态转移方程为:dp[i][j] += dp[i-1][j - coins[i-1]*k] 。此处可以进行空间优化,将三层循环变为两层,如下图两式相减得:
LeetCode刷题笔记(算法思想 四)_第5张图片

把k干掉,此时状态转移方程为:当 j >= coins[i-1] 也就是要凑的金额大于等于当前面值时,dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]];当当前面值已经比要凑的金额大了,当前面值的硬币用不上时,dp[i][j] = dp[i-1][j] 。

代码实现:

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [[0 for _ in range(amount+1)] for _ in range(len(coins)+1)]
        dp[0][0] = 1

        for i in range(1, len(coins)+1):
            for j in range(0, amount+1):
                if j >= coins[i-1]:  # 要凑的金额大于等于当前面值
                    dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]]
                else:   # 当前面值已经比要凑的金额大了,当前面值的硬币用不上
                    dp[i][j] = dp[i-1][j]

        return dp[-1][-1]

139. 单词拆分

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
     注意你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

思路:在这里插入图片描述

初始化一维数组dp,长度为len(s)+1,dp[i] 表示s的前 i 位是否可以用wordDict中的单词表示,dp[0] = True,空字符可以被表示,其余初始化为False。两层循环遍历所有子串,若 dp[i] = True 且 s[i:j]在wordDict中,dp[j] = True;因为dp[i] = True说明s的前 i 位可以用wordDict表示,则s[i:j]在wordDict中说明s的前 j 位可以表示。

代码实现:

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False for _ in range(len(s)+1)]
        dp[0] = True
        for i in range(len(s)):
            for j in range(i+1, len(s)+1):
                if dp[i] and s[i:j] in wordDict:
                    dp[j] = True
        return dp[-1]

377. 组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4

所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

进阶:
如果给定的数组中含有负数会怎么样?
问题会产生什么变化?
我们需要在题目中添加什么限制来允许负数的出现?

思路: 求组合的问题通常要想到回溯法和动态规划,由于本题并没有问具体的组合方式,所以考虑使用动态规划。dp[i]表示对于给定的由正整数组成且不存在重复数字的数组,和为 i 的组合的个数。初始化 dp[0] = 1,和为0的组合数也算1种。由于顺序不同的序列被视为不同的解,当target = 3时,可以分为:(1)3 = 1 + 2,其中2可以看成用 [1,2,3] 组成之和为2的组合数:{1,1,1},{1,2},共两种;(2)3 = 2 + 1,其中1可以看成用 [1,2,3] 组成之和为1的组合数:{2,1},共一种;(3)3 = 3 + 0,其中0可以看成用 [1,2,3] 组成之和为0的组合数:{3,{}},共一种;所以3一共有 2+1+1 = 4 种组合方式,同理可以分解其他数字,发现 dp[3] = dp[2] + dp[1] ; dp[4] = dp[3] + dp[2] + dp[1]。因为最终要求的是 dp[target],所以对target的循环应该在外层循环,内层遍历nums,当 i >= num 时,状态转移方程为:dp[i] += dp[i-num]。

代码实现:

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0 for _ in range(target+1)]
        dp[0]= 1

        for i in range(1, target+1):
            for num in nums:
                if i >= num:
                    dp[i] += dp[i-num]

        return dp[-1]

股票交易

309. 最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:

输入: [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

思路: 有关股票问题推荐看这篇文章:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/solution/yi-ge-fang-fa-tuan-mie-6-dao-gu-piao-wen-ti-by-lab/

代码实现:

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

        n = len(prices)
        dp = [[0, 0] for _ in range(n)]
        dp[0][0] = 0   # 表示第0天,不持股,初始化为0
        dp[0][1] = -prices[0]   # 表示第0天,买了一股

        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
            dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])

        return dp[-1][0]

714. 买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

注意:

0 < prices.length <= 50000.
0 < prices[i] < 50000.
0 <= fee < 50000.

代码实现:

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        if not prices:
            return 0
            
        n = len(prices)
        dp = [[0, 0] for _ in range(n)]
        dp[0][0] = 0  # 表示第0天,不持股,初始化为0
        dp[0][1] = -prices[0] - fee  # 表示第0天,买了一股并扣了手续费
        for i in range(1, n):
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)

        return dp[-1][0]

123. 买卖股票的最佳时机 III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1] 
输出: 0 
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。

代码实现:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices:
            return 0
        n = len(prices)
        times = 2
        dp = [[[0 for _ in range(2)] for _ in range(times+1)] for _ in range(n)]

        for i in range(0,n):
            for j in range(2,0,-1):
                if i == 0:
                    dp[i][j][0] = 0  # 第0天,还有j次,不持股,利润为0
                    dp[i][j][1] = -prices[i]  # 第0天,还有j次,持股,利润为-prices[i]
                    continue
                dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
                dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])

        return dp[-1][times][0]

188. 买卖股票的最佳时机 IV

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [2,4,1], k = 2
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

输入: [3,2,6,5,0,3], k = 2
输出: 7
解释: 在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

代码实现:

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        if n <= 1:
            return 0

        if k >= n//2:   # 实际上k起不到约束作用了,转化为不限制交易次数
            profit = 0
            for i in range(1, len(prices)):
                if prices[i] - prices[i - 1] > 0:
                    profit += prices[i] - prices[i - 1]
            return profit

        else:           # k < n//2,限制交易次数
            times = k
            dp = [[[0 for _ in range(2)] for _ in range(times+1)] for _ in range(n)]
            for i in range(0,n):
                for j in range(times,0,-1):
                    if i == 0:
                        dp[i][j][0] = 0  # 第0天,还有j次,不持股,利润为0
                        dp[i][j][1] = -prices[i]  # 第0天,还有j次,持股,利润为-prices[i]
                        continue
                    dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i])
                    dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i])

            return dp[-1][times][0]

字符串编辑

583. 两个字符串的删除操作

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

输入: "sea", "eat"
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

提示:

给定单词的长度不超过500。
给定单词中的字符只含有小写字母。

思路:
LeetCode刷题笔记(算法思想 四)_第6张图片

动态规划,使用二维数组dp,dp[i][j] 表示word1的前 i 个字符和word2的前 j 个字符匹配的最少删除次数。有两种情况:(1)word1[i-1] 和 word2[j-1] 匹配,说明不用删除,dp[i][j] = dp[i-1][j-1];(2)word1[i-1] 和 word2[j-1] 不匹配,就要考虑删除哪个,dp[i-1][j] 和 dp[i][j-1] 分别表示要删除的次数,取这两个之间较小的一个,并把删除次数+1,即dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1。

代码实现:

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        if not word1 and not word2:
            return 0

        dp = [[float('inf') for _ in range(len(word2)+1)] for _ in range(len(word1)+1)]
        dp[0][0] = 0
        for i in range(len(word1)+1):
            for j in range(len(word2)+1):
                if i == 0 or j == 0:
                    dp[i][j] = i+j
                elif word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i-1][j], dp[i][j-1])+1

        return dp[-1][-1]

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

思路:
LeetCode刷题笔记(算法思想 四)_第7张图片

dp[i][j] 表示word1 到 i 位置转换成word2 到 j 位置需要的最少次数。当 word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];当 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1;其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。第一行,是 word1 为空变成 word2 最少步数,就是插入操作;第一列,是 word2 为空,需要的最少步数,就是删除操作。推荐这篇文章:https://leetcode-cn.com/problems/edit-distance/solution/zi-di-xiang-shang-he-zi-ding-xiang-xia-by-powcai-3/

代码实现:

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        dp = [[float('inf') for _ in range(len(word2)+1)] for _ in range(len(word1)+1)]
        dp[0][0] = 0
        for i in range(len(word1)+1):
            for j in range(len(word2)+1):
                if i == 0 or j == 0:
                    dp[i][j] = i + j
                elif word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    # dp[i-1][j-1] 表示替换操作;dp[i-1][j] 表示删除操作;dp[i][j-1] 表示添加操作
                    dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1

        return dp[-1][-1]

650. 只有两个键的键盘

最初在一个记事本上只有一个字符 ‘A’。你每次可以对这个记事本进行两种操作:

Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的)。
Paste (粘贴) : 你可以粘贴你上一次复制的字符。

给定一个数字 n 。你需要使用最少的操作次数,在记事本中打印出恰好 n 个 ‘A’。输出能够打印出 n 个 ‘A’ 的最少操作次数。

示例 1:

输入: 3
输出: 3
解释:
最初, 我们只有一个字符 'A'。
第 1 步, 我们使用 Copy All 操作。
第 2 步, 我们使用 Paste 操作来获得 'AA'。
第 3 步, 我们使用 Paste 操作来获得 'AAA'。

说明:

n 的取值范围是 [1, 1000] 。

思路: 本题观察规律发现,若 n 为质数,需要的操作次数就是 n;若 n 为合数,需要的操作次数就是合数的两个因子所需的次数之和,比如 dp[20] = dp[4] + dp[5] = dp[2] + dp[10]。另外根据合数的性质,如果一个数是合数,那么它的最小质因数肯定小于等于它的平方根。据此可写出动态规划。

代码实现:

class Solution:
    def minSteps(self, n: int) -> int:
        if n == 1:
            return 0
        dp = [0 for _ in range(n+1)]
        for i in range(2, n+1):
            dp[i] = i
            for j in range(2, int(math.sqrt(n))+1):
                if i % j == 0:
                    dp[i] = dp[j] + dp[i//j]
                    break
        return dp[-1]

================================================================
以上为个人学习笔记总结,供学习参考交流,未经允许禁止转载或商用。

  • 题目全部出自LeetCode官网:https://leetcode-cn.com/
  • 题目分类参考这篇文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E7%9B%AE%E5%BD%95.md

你可能感兴趣的:(算法)