当前位置:首页>python>Python版_Leetcode_hot100系列(16)--多维动态规划

Python版_Leetcode_hot100系列(16)--多维动态规划

  • 2026-10-21 13:27:19
Python版_Leetcode_hot100系列(16)--多维动态规划

62.不同路径

🎭 题目小剧场

想象你在一个迷宫的左上角,要走到右下角。你只能向右或向下移动,就像在棋盘上走格子,每一步都要做出选择。你需要计算出有多少种不同的走法!

💡 解题思路

动态规划解法:

  1. 确定状态:dpi 表示到达位置(i,j)的不同路径数

  2. 状态转移:dpi = dpi-1 + dpi(从上方或左方到达)

  3. 初始条件:第一行和第一列都为1(只有一种走法)

  4. 逐步计算整个网格

🐍 Python代码实现

class Solution:    def uniquePaths(self, m: int, n: int) -> int:        """        动态规划解法 - 时间复杂度O(m×n),空间复杂度O(n)        """        # 使用一维数组优化空间        dp = [1] * n        for i in range(1, m):            for j in range(1, n):                dp[j] = dp[j] + dp[j-1]        return dp[-1]

📊 复杂度分析

  • 时间复杂度:O(m×n) - 需要遍历整个网格

  • 空间复杂度:O(n) - 使用一维数组优化空间

🌟 算法精髓

这个算法的巧妙之处在于"空间优化"。我们不需要存储整个二维数组,只需要一行即可,因为当前行的计算只依赖于当前行和上一行的数据。

🎯 小白友好贴士

  • dp = [1] * n 初始化第一行,都为1

  • for i in range(1, m) 从第二行开始

  • for j in range(1, n) 从第二列开始

  • dp[j] = dp[j] + dp[j-1] 状态转移方程

  • return dp[-1] 返回右下角的路径数

🔍 算法执行过程示例

示例:m = 3, n = 7初始:dp = [1, 1, 1, 1, 1, 1, 1]第1行(i = 0):保持不变  dp = [1, 1, 1, 1, 1, 1, 1]第2行(i = 1):  j = 1: dp[1] = dp[1] + dp[0] = 1 + 1 = 2  j = 2: dp[2] = dp[2] + dp[1] = 1 + 2 = 3  j = 3: dp[3] = dp[3] + dp[2] = 1 + 3 = 4  j = 4: dp[4] = dp[4] + dp[3] = 1 + 4 = 5  j = 5: dp[5] = dp[5] + dp[4] = 1 + 5 = 6  j = 6: dp[6] = dp[6] + dp[5] = 1 + 6 = 7  dp = [1, 2, 3, 4, 5, 6, 7]第3行(i = 2):  j = 1: dp[1] = dp[1] + dp[0] = 2 + 1 = 3  j = 2: dp[2] = dp[2] + dp[1] = 3 + 3 = 6  j = 3: dp[3] = dp[3] + dp[2] = 4 + 6 = 10  j = 4: dp[4] = dp[4] + dp[3] = 5 + 10 = 15  j = 5: dp[5] = dp[5] + dp[4] = 6 + 15 = 21  j = 6: dp[6] = dp[6] + dp[5] = 7 + 21 = 28  dp = [1, 3, 6, 10, 15, 21, 28]结束:return dp[-1] = 28 ✅

🎯 核心思想总结

  1. 状态定义:明确dp数组的含义

  2. 状态转移:找到递推关系

  3. 初始条件:确定边界值

  4. 空间优化:减少不必要的存储


64.最小路径和

🎭 题目小剧场

想象你在一个数字迷宫的左上角,每走一步都要付出相应的"代价"。你要找到一条从左上角到右下角的路径,使得总代价最小。就像在棋盘上收集金币,每格都有不同数量的金币,你要规划路线收集最多的金币!

💡 解题思路

动态规划解法:

  1. 确定状态:dpi 表示到达位置(i,j)的最小路径和

  2. 状态转移:dpi = gridi + min(dpi-1, dpi)

  3. 初始条件:dp0 = grid0,第一行和第一列需要特殊处理

  4. 逐步计算整个网格

🐍 Python代码实现

class Solution:    def minPathSum(self, grid: List[List[int]]) -> int:        """        动态规划解法 - 时间复杂度O(m×n),空间复杂度O(n)        """        if not grid or not grid[0]:            return 0        m, n = len(grid), len(grid[0])        dp = [0] * n        for i in range(m):            for j in range(n):                if i == 0 and j == 0:                    dp[j] = grid[i][j]                elif i == 0:                    dp[j] = dp[j-1] + grid[i][j]                elif j == 0:                    dp[j] = dp[j] + grid[i][j]                else:                    dp[j] = min(dp[j], dp[j-1]) + grid[i][j]        return dp[-1]

📊 复杂度分析

  • 时间复杂度:O(m×n) - 需要遍历整个网格

  • 空间复杂度:O(n) - 使用一维数组优化空间

🌟 算法精髓

这个算法的巧妙之处在于"原地更新"。我们直接在输入网格上进行修改,或者使用一维数组,避免了额外的空间开销,同时保证了计算的正确性。

🎯 小白友好贴士

  • if not grid or not grid[0] 处理空网格

  • dp = [0] * n 初始化一维数组

  • if i == 0 and j == 0 处理起始点

  • elif i == 0 处理第一行

  • elif j == 0 处理第一列

  • dp[j] = min(dp[j], dp[j-1]) + grid[i][j] 状态转移方程

🔍 �算法执行过程示例

示例:grid = [  [1, 3, 1],  [1, 5, 1],  [4, 2, 1]]初始:dp = [0, 0, 0]第1行(i = 0):  j = 0: dp[0] = grid[0][0] = 1  j = 1: dp[1] = dp[0] + grid[0][1] = 1 + 3 = 4  j = 2: dp[2] = dp[1] + grid[0][2] = 4 + 1 = 5  dp = [1, 4, 5]第2行(i = 1):  j = 0: dp[0] = dp[0] + grid[1][0] = 1 + 1 = 2  j = 1: dp[1] = min(dp[1], dp[0]) + grid[1][1] = min(4, 2) + 5 = 7  j = 2: dp[2] = min(dp[2], dp[1]) + grid[1][2] = min(5, 7) + 1 = 6  dp = [2, 7, 6]第3行(i = 2):  j = 0: dp[0] = dp[0] + grid[2][0] = 2 + 4 = 6  j = 1: dp[1] = min(dp[1], dp[0]) + grid[2][1] = min(7, 6) + 2 = 8  j = 2: dp[2] = min(dp[2], dp[1]) + grid[2][2] = min(6, 8) + 1 = 7  dp = [6, 8, 7]结束:return dp[-1] = 7 ✅验证:最小路径为 1→3→1→1→1,和为7

🎯 核心思想总结

  1. 状态定义:明确dp数组的含义

  2. 状态转移:找到递推关系

  3. 边界处理:特殊处理第一行和第一列

  4. 空间优化:使用一维数组减少空间


5.最长回文子串

🎭 题目小剧场

想象你在玩一个找对称的游戏,给定一串字符,你要找出最长的对称片段。就像照镜子,左边的字符和右边的字符要完全一样,比如 "aba" 或 "abba"。

💡 解题思路

中心扩展法:

  1. 每个字符(或字符间隙)都可以作为回文中心

  2. 从中心向两边扩展,检查是否构成回文

  3. 处理两种情况:奇数长度(单字符中心)和偶数长度(字符间隙中心)

  4. 维护最长回文子串的起始位置和长度

🐍 Python代码实现

class Solution:    def longestPalindrome(self, s: str) -> str:        """        中心扩展法 - 时间复杂度O(n²),空间复杂度O(1)        """        if not s:            return ""        start = 0        max_length = 1        for i in range(len(s)):            # 奇数长度回文(中心为单个字符)            self.expandAroundCenter(s, i, i, start, max_length)            # 偶数长度回文(中心为字符间隙)            self.expandAroundCenter(s, i, i + 1, start, max_length)        return s[start:start + max_length]    def expandAroundCenter(self, s: str, left: int, right: int, start: int, max_length: int):        while left >= 0 and right < len(s) and s[left] == s[right]:            current_length = right - left + 1            if current_length > max_length:                max_length = current_length                start = left            left -= 1            right += 1        return start, max_length

📊 复杂度分析

  • 时间复杂度:O(n²) - n个中心,每个中心最多扩展n次

  • 空间复杂度:O(1) - 只使用常数额外空间

🌟 算法精髓

这个算法的巧妙之处在于"中心思想"。我们不直接枚举所有子串,而是枚举所有可能的中心,然后从中心向两边扩展,大大减少了计算量。

🎯 小白友好贴士

  • if not s: return "" 处理空字符串

  • start = 0, max_length = 1 初始化结果

  • for i in range(len(s)) 遍历所有可能中心

  • expandAroundCenter(s, i, i) 处理奇数长度

  • expandAroundCenter(s, i, i + 1) 处理偶数长度

  • while left >= 0 and right < len(s) and s[left] == s[right] 扩展条件

🔍 算法执行过程示例

示例:s = "babad"初始:start = 0, max_length = 1第1步:i = 0  奇数中心 (0, 0):    left = 0, right = 0, s[0] = 'b' = s[0] = 'b'    current_length = 1, max_length = 1, start = 0    left = -1, right = 1, 停止  偶数中心 (0, 1):    left = 0, right = 1, s[0] = 'b' ≠ s[1] = 'a'    停止第2步:i = 1  奇数中心 (1, 1):    left = 1, right = 1, s[1] = 'a' = s[1] = 'a'    current_length = 1, max_length = 1, start = 0    left = 0, right = 2, s[0] = 'b' ≠ s[2] = 'b'    停止  偶数中心 (1, 2):    left = 1, right = 2, s[1] = 'a' ≠ s[2] = 'b'    停止第3步:i = 2  奇数中心 (2, 2):    left = 2, right = 2, s[2] = 'b' = s[2] = 'b'    current_length = 1, max_length = 1, start = 0    left = 1, right = 3, s[1] = 'a' = s[3] = 'a'    current_length = 3, max_length = 3, start = 1    left = 0, right = 4, s[0] = 'b' ≠ s[4] = 'd'    停止  偶数中心 (2, 3):    left = 2, right = 3, s[2] = 'b' ≠ s[3] = 'a'    停止第4步:i = 3  奇数中心 (3, 3):    left = 3, right = 3, s[3] = 'a' = s[3] = 'a'    current_length = 1, max_length = 3, start = 1    left = 2, right = 4, s[2] = 'b' ≠ s[4] = 'd'    停止  偶数中心 (3, 4):    left = 3, right = 4, s[3] = 'a' ≠ s[4] = 'd'    停止第5步:i = 4  奇数中心 (4, 4):    left = 4, right = 4, s[4] = 'd' = s[4] = 'd'  current_length = 1, max_length = 3, start = 1left = 3, right = 5, 停止偶数中心 (4, 5):left = 4, right = 5, 停止结束:return s[1:1+3] = "bab" ✅验证:最长回文子串为 "bab" 或 "aba",长度为3

🎯 核心思想总结

  1. 中心思想:每个字符和间隙都可以作为中心

  2. 双向扩展:从中心向两边同时扩展

  3. 奇偶处理:分别处理奇数和偶数长度的回文

  4. 实时更新:发现更长回文时立即更新结果


1143.最长公共子序列

🎭 题目小剧场

想象你有两串字符,像两条不同的项链。你要找出这两条项链上最长的相同字符序列,这些字符可以不连续,但必须保持原有的相对顺序。就像在两本书中找出最长的相同句子片段!

💡 解题思路

动态规划解法:

  1. 确定状态:dpi 表示text1的前i个字符和text2的前j个字符的最长公共子序列长度

  2. 状态转移:

    (1) 如果text1[i-1] == text2[j-1]:dpi = dpi-1 + 1

    (2) 如果text1[i-1] != text2[j-1]:dpi = max(dpi-1, dpi)

  3. 初始条件:dp0 = dpi = 0(空字符串的LCS为0)

  4. 逐步计算整个二维数组

🐍 Python代码实现

class Solution:    def longestCommonSubsequence(self, text1: str, text2: str) -> int:        """        动态规划解法 - 时间复杂度O(m×n),空间复杂度O(n)        """        m, n = len(text1), len(text2)        # 使用一维数组优化空间        dp = [0] * (n + 1)        for i in range(1, m + 1):            prev = 0  # 保存dp[i-1][j-1]的值            for j in range(1, n + 1):                temp = dp[j]  # 保存当前的dp[i-1][j]                if text1[i-1] == text2[j-1]:                    dp[j] = prev + 1                else:                    dp[j] = max(dp[j], dp[j-1])                prev = temp        return dp[n]

📊 复杂度分析

  • 时间复杂度:O(m×n) - m和n分别是两个字符串的长度

  • 空间复杂度:O(n) - 使用一维数组优化空间

🌟 算法精髓

这个算法的巧妙之处在于"空间优化"。我们观察到当前状态只依赖于左上方、上方和左方的状态,因此可以用一维数组加上一个临时变量来存储必要信息。

🎯 小白友好贴士

  • m, n = len(text1), len(text2) 获取字符串长度

  • dp = [0] * (n + 1) 初始化一维数组

  • prev = 0 保存左上角的值

  • temp = dp[j] 保存当前格子上方的值

  • if text1[i-1] == text2[j-1] 字符匹配的情况

  • dp[j] = prev + 1 匹配时长度加1

  • dp[j] = max(dp[j], dp[j-1]) 不匹配时取较大值

🔍 算法执行过程示例

示例:text1 = "abcde", text2 = "ace"初始:m = 5, n = 3, dp = [0, 0, 0, 0]第1行(i = 1, text1[0] = 'a'):prev = 0j = 1: text1[0] = 'a' == text2[0] = 'a'temp = dp[1] = 0dp[1] = prev + 1 = 1prev = temp = 0j = 2: text1[0] = 'a' != text2[1] = 'c'temp = dp[2] = 0dp[2] = max(dp[2], dp[1]) = max(0, 1) = 1prev = temp = 0j = 3: text1[0] = 'a' != text2[2] = 'e'temp = dp[3] = 0dp[3] = max(dp[3], dp[2]) = max(0, 1) = 1prev = temp = 0dp = [0, 1, 1, 1]第2行(i = 2, text1[1] = 'b'):prev = 0j = 1: text1[1] = 'b' != text2[0] = 'a'temp = dp[1] = 1dp[1] = max(dp[1], dp[0]) = max(1, 0) = 1prev = temp = 1j = 2: text1[1] = 'b' != text2[1] = 'c'temp = dp[2] = 1dp[2] = max(dp[2], dp[1]) = max(1, 1) = 1prev = temp = 1j = 3: text1[1] = 'b' != text2[2] = 'e'temp = dp[3] = 1dp[3] = max(dp[3], dp[2]) = max(1, 1) = 1prev = temp = 1dp = [0, 1, 1, 1]第3行(i = 3, text1[2] = 'c'):prev = 0j = 1: text1[2] = 'c' != text2[0] = 'a'temp = dp[1] = 1dp[1] = max(dp[1], dp[0]) = max(1, 0) = 1prev = temp = 1j = 2: text1[2] = 'c' == text2[1] = 'c'temp = dp[2] = 1dp[2] = prev + 1 = 2prev = temp = 1j = 3: text1[2] = 'c' != text2[2] = 'e'temp = dp[3] = 1dp[3] = max(dp[3], dp[2]) = max(1, 2) = 2prev = temp = 1dp = [0, 1, 2, 2]第4行(i = 4, text1[3] = 'd'):prev = 0j = 1: text1[3] = 'd' != text2[0] = 'a'temp = dp[1] = 1dp[1] = max(dp[1], dp[0]) = max(1, 0) = 1prev = temp = 1j = 2: text1[3] = 'd' != text2[1] = 'c'temp = dp[2] = 2dp[2] = max(dp[2], dp[1]) = max(2, 1) = 2prev = temp = 2j = 3: text1[3] = 'd' != text2[2] = 'e'temp = dp[3] = 2dp[3] = max(dp[3], dp[2]) = max(2, 2) = 2prev = temp = 2dp = [0, 1, 2, 2]第5行(i = 5, text1[4] = 'e'):prev = 0j = 1: text1[4] = 'e' != text2[0] = 'a'temp = dp[1] = 1dp[1] = max(dp[1], dp[0]) = max(1, 0) = 1prev = temp = 1j = 2: text1[4] = 'e' != text2[1] = 'c'temp = dp[2] = 2dp[2] = max(dp[2], dp[1]) = max(2, 1) = 2prev = temp = 2j = 3: text1[4] = 'e' == text2[2] = 'e'temp = dp[3] = 2dp[3] = prev + 1 = 3prev = temp = 2dp = [0, 1, 2, 3]结束:return dp[3] = 3 ✅验证:最长公共子序列为 "ace",长度为3

🎯 核心思想总结

  1. 状态定义:明确dp数组的含义

  2. 状态转移:找到递推关系

  3. 空间优化:使用一维数组减少空间

  4. 边界处理:正确处理空字符串情况


72.编辑距离

🎭 题目小剧场

想象你是一位文字编辑师,要把单词 "word" 变成 "word1"。你有三种操作:插入一个字符、删除一个字符、替换一个字符。每次操作都算一步,你的目标是找到最少的操作步骤。

💡 解题思路

动态规划解法:

  1. 确定状态:dpi 表示word1的前i个字符转换成word2的前j个字符所需的最少操作数

  2. 状态转移:

    (1) 如果word1[i-1] == word2[j-1]:dpi = dpi-1(无需操作)

    (2) 如果word1[i-1] != word2[j-1]:dpi = min(dpi-1 + 1, dpi + 1, dpi-1 + 1)

  3. 初始条件:dp0 = j(插入j个字符),dpi = i(删除i个字符)

  4. 逐步计算整个二维数组

🐍 Python代码实现

class Solution:    def minDistance(self, word1: str, word2: str) -> int:        """        动态规划解法 - 时间复杂度O(m×n),空间复杂度O(m×n)        """        m, n = len(word1), len(word2)        # 创建dp数组        dp = [[0] * (n + 1) for _ in range(m + 1)]        # 初始化边界条件        for i in range(m + 1):            dp[i][0] = i  # 删除i个字符        for j in range(n + 1):            dp[0][j] = j  # 插入j个字符        # 填充dp数组        for i in range(1, m + 1):            for j in range(1, n + 1):                if word1[i-1] == word2[j-1]:                    dp[i][j] = dp[i-1][j-1]                else:                    dp[i][j] = min(                        dp[i-1][j] + 1,    # 删除                        dp[i][j-1] + 1,    # 插入                        dp[i-1][j-1] + 1   # 替换                    )        return dp[m][n]

📊 复杂度分析

  • 时间复杂度:O(m×n) - m和n分别是两个单词的长度

  • 空间复杂度:O(m×n) - 需要二维数组存储状态

🌟 算法精髓

这个算法的巧妙之处在于"状态定义"。通过定义清晰的子问题,将复杂的字符串转换问题分解为简单的字符比较问题,从而找到最优解。

🎯 小白友好贴士

  • m, n = len(word1), len(word2) 获取单词长度

  • dp = [[0] * (n + 1) for _ in range(m + 1)] 创建dp数组

  • dp[i][0] = i 删除i个字符

  • dp[0][j] = j 插入j个字符

  • if word1[i-1] == word2[j-1] 字符匹配的情况

  • dp[i][j] = dp[i-1][j-1] 匹配时无需操作

  • dp[i][j] = min(...) 不匹配时取最小操作数

🔍 算法执行过程示例

示例:word1 = "horse", word2 = "ros"初始:m = 5, n = 3dp = [  [0, 1, 2, 3],  [1, 0, 0, 0],  [2, 0, 0, 0],  [3, 0, 0, 0],  [4, 0, 0, 0],  [5, 0, 0, 0]]第1行(i = 1, word1[0] = 'h'):  j = 1: word1[0] = 'h' != word2[0] = 'r'    dp[1][1] = min(dp[0][1] + 1, dp[1][0] + 1, dp[0][0] + 1)       = min(1 + 1, 1 + 1, 0 + 1) = 1  j = 2: word1[0] = 'h' != word2[1] = 'o'    dp[1][2] = min(dp[0][2] + 1, dp[1][1] + 1, dp[0][1] + 1)       = min(2 + 1, 1 + 1, 1 + 1) = 2  j = 3: word1[0] = 'h' != word2[2] = 's'    dp[1][3] = min(dp[0][3] + 1, dp[1][2] + 1, dp[0][2] + 1)       = min(3 + 1, 2 + 1, 2 + 1) = 3第2行(i = 2, word1[1] = 'o'):  j = 1: word1[1] = 'o' != word2[0] = 'r'    dp[2][1] = min(dp[1][1] + 1, dp[2][0] + 1, dp[1][0] + 1)       = min(1 + 1, 2 + 1, 1 + 1) = 2  j = 2: word1[1] = 'o' == word2[1] = 'o'    dp[2][2] = dp[1][1] = 1  j = 3: word1[1] = 'o' != word2[2] = 's'    dp[2][3] = min(dp[1][3] + 1, dp[2][2] + 1, dp[1][2] + 1)       = min(3 + 1, 1 + 1, 2 + 1) = 2第3行(i = 3, word1[2] = 'r'):  j = 1: word1[2] = 'r' == word2[0] = 'r'    dp[3][1] = dp[2][0] = 2  j = 2: word1[2] = 'r' != word2[1] = 'o'    dp[3][2] = min(dp[2][2] + 1, dp[3][1] + 1, dp[2][1] + 1)       = min(1 + 1, 2 + 1, 2 + 1) = 2  j = 3: word1[2] = 'r' != word2[2] = 's'    dp[3][3] = min(dp[2][3] + 1, dp[3][2] + 1, dp[2][2] + 1)       = min(2 + 1, 2 + 1, 1 + 1) = 2第4行(i = 4, word1[3] = 's'):  j = 1: word1[3] = 's' != word2[0] = 'r'    dp[4][1] = min(dp[3][1] + 1, dp[4][0] + 1, dp[3][0] + 1)       = min(2 + 1, 4 + 1, 3 + 1) = 3  j = 2: word1[3] = 's' != word2[1] = 'o'    dp[4][2] = min(dp[3][2] + 1, dp[4][1] + 1, dp[3][1] + 1)       = min(2 + 1, 3 + 1, 2 + 1) = 3  j = 3: word1[3] = 's' == word2[2] = 's'    dp[4][3] = dp[3][2] = 2第5行(i = 5, word1[4] = 'e'):  j = 1: word1[4] = 'e' != word2[0] = 'r'    dp[5][1] = min(dp[4][1] + 1, dp[5][0] + 1, dp[4][0] + 1)       = min(3 + 1, 5 + 1, 4 + 1) = 4  j = 2: word1[4] = 'e' != word2[1] = 'o'    dp[5][2] = min(dp[4][2] + 1, dp[5][1] + 1, dp[4][1] + 1)       = min(3 + 1, 4 + 1, 3 + 1) = 4  j = 3: word1[4] = 'e' != word2[2] = 's'    dp[5][3] = min(dp[4][3] + 1, dp[5][2] + 1, dp[4][2] + 1)       = min(2 + 1, 4 + 1, 3 + 1) = 3结束:return dp[5][3] = 3 ✅验证:horse → rse → rose → ros,共3步操作

🎯 核心思想总结

  1. 状态定义:明确dp数组的含义

  2. 状态转移:考虑插入、删除、替换三种操作

  3. 边界条件:处理空字符串的情况

  4. 最优选择:选择操作数最少的方案

最新文章

随机文章