初学python的我开始Leetcode题-16
提示:100道LeetCode热题-16主要是多维动态规划相关,包括五题:不同路径、最小路径和、最长回文子串、最长公共子序列、编辑距离。由于初学,所以我的代码部分仅供参考。
前言
之前做过动态规划,这里扩展到多维情况~
提示:以下是本篇文章正文内容,下面结果代码仅供参考
题目1:不同路径
1.题目要求:
题目如下:
一个机器人位于一个
m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7 输出:28示例 2:
输入:m = 3, n = 2 输出:3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 3. 向下 -> 向右 -> 向下示例 3:
输入:m = 7, n = 3 输出:28示例 4:
输入:m = 3, n = 3 输出:6提示:
1 <= m, n <= 100
- 题目数据保证答案小于等于
2 *
代码框架已经提供如下:
class Solution(object):
def uniquePaths(self, m, n):
"""
:type m: int
:type n: int
:rtype: int
"""
2.结果代码:
class Solution(object):def uniquePaths(self, m, n):""":type m: int:type n: int:rtype: int"""dp = [[1] * n for _ in range(m)]for i in range(1, m):for j in range(1, n):dp[i][j] = dp[i-1][j] + dp[i][j-1]return dp[-1][-1]
说明:
首先,我需要明确题目在问什么。我们有一个m行n列的网格,机器人从左上角(Start)出发,每次只能向右或向下移动一步,目标是到达右下角(Finish)。我们需要计算所有可能的不同路径的数量。机器人需要从左上角(0,0)移动到右下角(m-1,n-1)。在这个过程中:
需要向右移动 (n-1) 次。
需要向下移动 (m-1) 次。
总共的移动步数为 (m-1 + n-1) = m + n - 2 步。
因此,问题转化为:在 m + n - 2 步中,选择 (m-1) 步向下移动(或 (n-1) 步向右移动)的组合数,因此可以使用组合数的方法。虽然组合数学的方法很高效,但本单元是动态规划(DP)的方法,这在某些变种问题(如网格中有障碍物)中可能更灵活:
定义 dp[i][j]
为从 (0,0) 到 (i,j) 的路径数。
递推关系:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
(因为可以从上方或左方到达)边界条件:
dp[0][j] = 1
(第一行只能一直向右)dp[i][0] = 1
(第一列只能一直向下)
题目2:最小路径和
1.题目要求:
题目如下:
给定一个包含非负整数的
m x n
网格grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径 1→3→1→1→1 的总和最小。示例 2:
输入:grid = [[1,2,3],[4,5,6]] 输出:12提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 200
代码框架已经提供如下:
class Solution(object):
def minPathSum(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
2.结果代码:
class Solution(object):def minPathSum(self, grid):""":type grid: List[List[int]]:rtype: int"""if not grid or not grid[0]:return 0m, n = len(grid), len(grid[0])dp = [0] * ndp[0] = grid[0][0]for j in range(1, n):dp[j] = dp[j-1] + grid[0][j]for i in range(1, m):dp[0] += grid[i][0]for j in range(1, n):dp[j] = min(dp[j], dp[j-1]) + grid[i][j]return dp[-1]
说明:
这是一个典型的动态规划问题。我们可以定义 dp[i][j]
为从 (0, 0)
到 (i, j)
的最小路径和。
递推关系:
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
可以从上方
(i-1, j)
或左方(i, j-1)
到达(i, j)
,选择其中较小的路径和加上当前格子的值。
边界条件:
dp[0][0] = grid[0][0]
(起点)第一行
dp[0][j]
只能从左方到达:dp[0][j] = dp[0][j-1] + grid[0][j]
第一列
dp[i][0]
只能从上方到达:dp[i][0] = dp[i-1][0] + grid[i][0]
题目3:最长回文子串
1.题目要求:
题目如下:
给你一个字符串
s
,找到s
中最长的 回文 子串。示例 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。示例 2:
输入:s = "cbbd" 输出:"bb"提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
代码框架已经提供如下:
class Solution(object):
def longestPalindrome(self, s):
"""
:type s: str
:rtype: str
"""
2.结果代码:
class Solution(object):def longestPalindrome(self, s):""":type s: str:rtype: str"""if not s:return ""start, end = 0, 0for i in range(len(s)):# 奇数长度left1, right1 = self.expand_around_center(s, i, i)# 偶数长度left2, right2 = self.expand_around_center(s, i, i + 1)if right1 - left1 > end - start:start, end = left1, right1if right2 - left2 > end - start:start, end = left2, right2return s[start:end + 1]def expand_around_center(self, s, left, right):while left >= 0 and right < len(s) and s[left] == s[right]:left -= 1right += 1return left + 1, right - 1
以上是中心扩展法:
回文可以有两种形式:
奇数长度:中心是一个字符,如 "aba"。
偶数长度:中心是两个字符,如 "abba"。
因此,我们需要对每种可能的中心进行扩展:
对于每个字符
s[i]
,以s[i]
为中心扩展。对于每对字符
s[i]
和s[i+1]
,以s[i]
和s[i+1]
为中心扩展。
实现步骤:
初始化最长回文子串的起始和结束索引。
遍历每个字符作为中心:
扩展奇数长度的回文。
扩展偶数长度的回文。
更新最长回文子串的索引。
返回对应的子串。
如果要动态规划的话,空间复杂度O():
class Solution(object):def longestPalindrome(self, s):""":type s: str:rtype: str"""if not s:return ""n = len(s)# dp[i][j] 表示 s[i..j] 是否为回文dp = [[False] * n for _ in range(n)]start, max_len = 0, 1 # 至少有一个字符# 长度 1for i in range(n):dp[i][i] = True# 长度 2for i in range(n - 1):if s[i] == s[i + 1]:dp[i][i + 1] = Truestart = imax_len = 2# 长度 >= 3for length in range(3, n + 1): # 当前考察的子串长度for i in range(n - length + 1):j = i + length - 1if s[i] == s[j] and dp[i + 1][j - 1]:dp[i][j] = Truestart = imax_len = lengthreturn s[start: start + max_len]
1. 思路与状态定义
设 dp[i][j]
表示子串 s[i … j]
是否为回文,其中 0 ≤ i ≤ j < n
。
则:
dp[i][j] = True
当且仅当s[i] == s[j]
且
如果j - i + 1 > 2
,还需满足dp[i+1][j-1] == True
。
2. 递推公式
if s[i] == s[j]:if j - i <= 2: # 长度 1 或 2dp[i][j] = Trueelse:dp[i][j] = dp[i+1][j-1]
else:dp[i][j] = False
3. 填表顺序
按长度递增(从 1 到 n)
先填所有长度为 1、2、3 … n 的子串,确保i+1
和j-1
已经计算过。
4. 边界初始化
所有长度为 1 的子串都是回文:
dp[i][i] = True
记录当前最长回文子串的起始索引及长度即可。
题目4:最长公共子序列
1.题目要求:
题目如下:
给定两个字符串
text1
和text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回0
。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 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, text2.length <= 1000
text1
和text2
仅由小写英文字符组成。
代码框架已经提供如下:
class Solution(object):
def longestCommonSubsequence(self, text1, text2):
"""
:type text1: str
:type text2: str
:rtype: int
"""
2.结果代码:
class Solution(object):def longestCommonSubsequence(self, text1, text2):m, n = len(text1), len(text2)# dp[i][j] 表示 text1[:i] 与 text2[:j] 的 LCS 长度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] = dp[i - 1][j - 1] + 1else:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])return dp[m][n]
这是个经典 二维 DP 问题。
设 dp[i][j]
表示 text1[0 .. i-1]
与 text2[0 .. j-1]
的最长公共子序列(LCS)长度。
状态转移:
如果
text1[i-1] == text2[j-1]
,则这个字符一定在 LCS 中dp[i][j] = dp[i-1][j-1] + 1
否则,取“跳过 text1 最后一个字符”或“跳过 text2 最后一个字符”的最大值
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
边界:dp[0][*] = dp[*][0] = 0
(空串的 LCS 长度为 0)
时间复杂度:O(m * n)
空间复杂度:O(m * n)
,可压缩为 O(min(m, n))
题目5:编辑距离
1.题目要求:
题目如下:
给你两个单词
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')提示:
0 <= word1.length, word2.length <= 500
word1
和word2
由小写英文字母组成
代码框架已经提供如下:
class Solution(object):
def minDistance(self, word1, word2):
"""
:type word1: str
:type word2: str
:rtype: int
"""
2.结果代码:
class Solution(object):def minDistance(self, word1, word2):m, n = len(word1), len(word2)# dp[i][j]: word1[:i] -> word2[:j] 的最小操作数dp = [[0] * (n + 1) for _ in range(m + 1)]# 边界for i in range(m + 1):dp[i][0] = ifor j in range(n + 1):dp[0][j] = jfor 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], # 删除dp[i][j - 1], # 插入dp[i - 1][j - 1] # 替换) + 1return dp[m][n]
说明:
这是经典的 编辑距离 / Levenshtein 距离 问题:
把字符串 word1
转换成 word2
最少需要多少次插入、删除或替换操作。
1. 状态定义
dp[i][j]
表示 word1[0..i-1]
转换成 word2[0..j-1]
所需的最少操作数。
2. 状态转移
考虑 word1[i-1]
与 word2[j-1]
的关系:
相等:不需要操作,
dp[i][j] = dp[i-1][j-1]
不等:取三种可能操作的最小值再 +1
插入:
dp[i][j-1] + 1
删除:
dp[i-1][j] + 1
替换:
dp[i-1][j-1] + 1
3. 边界
dp[0][j] = j
:把空串变成word2[0..j-1]
需要插入 j 次dp[i][0] = i
:把word1[0..i-1]
变成空串需要删除 i 次
总结
针对多维动态规划的五种题型进行了学习,了解了部分有关多维动态规划与python的相关知识,大家加油!