前言
整理力扣刷题思路。
- 语言:python
- 题库:来自neetcode: link
一、预备知识
1.动态规划(Dynamic Programming)
动态规划是一种有效的计算机算法设计技术,主要用于解决具有以下特点的问题:
- 重叠子问题:问题可以分解为若干个子问题,且子问题的解可以在后续的求解中被重复利用。
- 最优子结构:问题的最优解可以由其子问题的最优解得到。
动态规划的核心思想是将问题分解为子问题,通过逐个求解子问题,逐步构建出整体问题的最优解。它通常用于优化问题,如寻找最优路径、最大值、最小值等。
常见的动态规划问题类型和解题思路
-
最优化问题:
- 这类问题涉及在有限的资源或约束条件下寻找最优解。例如背包问题、旅行商问题等。
- 解题思路:通过逐个求解子问题,逐步构建出最优解。
-
序列比对问题:
- 这类问题需要找到两个序列之间的相似或相同部分。例如DNA序列比对、字符串匹配等。
- 解题思路:动态规划可以用于构建最长公共子序列或最长公共子串等问题的解。
-
树和图的问题:
- 例如二叉树的最小路径和、最小生成树等。
- 解题思路:将问题分解为子问题,并存储子问题的解,避免重复计算。
示例:机器人的路径问题
让我们以一个具体的问题为例,来看看动态规划的解题思路。
问题描述
一个机器人位于一个 m x n 网格的左上角,每次只能向下或向右移动一步,试图达到网格的右下角。问总共有多少条不同的路径?
解题步骤
-
定义数组元素的含义:
- 定义
dp[i][j]
为机器人从左上角走到(i, j)
这个位置时的路径数。 dp[m-1][n-1]
即为所求答案。
- 定义
-
关系数组元素间的关系式:
- 机器人可以从
(i-1, j)
或(i, j-1)
这两个位置走一步到达(i, j)
。 - 因此,
dp[i][j] = dp[i-1][j] + dp[i][j-1]
。
- 机器人可以从
-
初始值:
- 初始化最上面一行和最左边一列的路径数:
dp[0][j] = 1
(机器人只能一直往左走)dp[i][0] = 1
(机器人只能一直往下走)
- 初始化最上面一行和最左边一列的路径数:
-
代码实现:
def uniquePaths(m, n): if m <= 0 or n <= 0: return 0 dp = [[0] * n for _ in range(m)] for i in range(m): dp[i][0] = 1 for j in range(n): dp[0][j] = 1 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[m-1][n-1]
2.背包问题
二、解题思路
1.动态规划
70.climbing-stairs
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
link
class Solution:
def climbStairs(self, n: int) -> int:
if n<=2:
return n
#答案是斐波那契数列,dp[n]=dp[n-1]+dp[n-2]
a,b = 1,1
for _ in range(1,n):
a,b = b, a+b
return b
746.min-cost-climbing-stairs
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
link
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
#dp[i]代表到达第i个台阶的花费,因为最终要到顶部,所以dp的长度比cost大1
dp = [0]*(len(cost)+1)
for i in range(2,len(cost)+1):
dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
return dp[-1]
198.house-robber
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
link
class Solution:
def rob(self, nums: List[int]) -> int:
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-1],dp[i-2]+nums[i])
return dp[-1]
213.house-robber-ii
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
link
class Solution:
def rob(self, nums: List[int]) -> int:
def Rob(nums):
if len(nums)==1:
return nums[0]
dp = [0]*len(nums)
dp[0],dp[1] = nums[0],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[-1]
if len(nums)==1:
return nums[0]
return max(Rob(nums[:-1]), Rob(nums[1:]))
参考:link
将环形变成两个单排
5.longest-palindromic-substring
给你一个字符串 s,找到 s 中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
link
class Solution:
def longestPalindrome(self, s: str) -> str:
self.ans = ''
def helper(i,j):
while 0<=i and j<len(s) and s[i]==s[j]:
if j-i+1>len(self.ans):
self.ans = s[i:j+1]
i -= 1
j += 1
#以当前字符或加上后一个字符为中心,左右延展判断是否回文
for i in range(len(s)):
helper(i,i)
helper(i,i+1)
return self.ans
647.palindromic-substrings
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
link
class Solution:
def countSubstrings(self, s: str) -> int:
self.cnt = 0
n = len(s)
def helper(i,j):
while i>=0 and j<n and s[i] == s[j]:
self.cnt += 1
i -= 1
j += 1
for i in range(n):
helper(i,i)
helper(i,i+1)
return self.cnt
91.decode-ways
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
‘A’ -> “1”
‘B’ -> “2”
…
‘Z’ -> “26”
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,“11106” 可以映射为:
“AAJF” ,将消息分组为 (1 1 10 6)
“KJF” ,将消息分组为 (11 10 6)
注意,消息不能分组为 (1 11 06) ,因为 “06” 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
link
class Solution:
def numDecodings(self, s: str) -> int:
if s[0]=='0':
return 0
n = len(s)
dp = [1]*(n+1)
for i in range(2,n+1):
if s[i-1] == '0' and s[i-2] not in '12':
return 0
if s[i-2:i] in ['10','20']:
dp[i] = dp[i-2]
elif '10' < s[i-2:i] <='26':
dp[i] = dp[i-1] + dp[i-2]
else:
dp[i] = dp[i-1]
return dp[n]
152.maximum-product-subarray
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组
(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
link
class Solution:
def maxProduct(self, nums: List[int]) -> int:
if not nums:
return
mx = mn = out = nums[0]
for num in nums[1:]:
mx,mn = max(num*mx, num*mn, num), min(num*mx, num*mn, num)
out = max(out, mx)
return out
300.longest-increasing-subsequence
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
link
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp = [1]*len(nums) #dp[i]代表以i结尾的最长严格递增子序列的长度
for j in range(len(nums)):
for i in range(j):
if nums[i]<nums[j]:
dp[j] = max(dp[i]+1,dp[j])
return max(dp)
改进
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
tails,cnt = [0]*len(nums), 0
for num in nums:
i,j = 0,cnt
while i<j:
m = (i+j) // 2
if tails[m] < num:
i = m+1
else:
j = m
tails[i] = num
if j == cnt:
cnt += 1
return cnt
参考:link
322.coin-change
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
link
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
n = len(coins)
dp = [[amount+1] * (amount+1) for _ in range(n+1)] # 初始化为一个较大的值,如 +inf 或 amount+1
# 合法的初始化
dp[0][0] = 0 # 其他 dp[0][j]均不合法
# 完全背包:优化后的状态转移
for i in range(1, n+1): # 第一层循环:遍历硬币
for j in range(amount+1): # 第二层循环:遍历背包
if j < coins[i-1]: # 容量有限,无法选择第i种硬币
dp[i][j] = dp[i-1][j]
else: # 可选择第i种硬币
dp[i][j] = min( dp[i-1][j], dp[i][j-coins[i-1]] + 1 )
ans = dp[n][amount]
return ans if ans != amount+1 else -1
改进
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [amount+1] * (amount+1)
dp[0] = 0
for coin in coins:
for j in range(coin,amount+1):
dp[j] = min(dp[j], dp[j-coin] + 1)
ans = dp[-1]
return ans if ans != amount+1 else -1
参考:link
416.partition-equal-subset-sum
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
link
class Solution:
def canPartition(self, nums: List[int]) -> bool:
Sum = sum(nums)
nums = sorted(nums)
if Sum%2 == 1 or nums[-1]>Sum//2:
return False
dp = [[False]*(Sum//2+1) for _ in range(len(nums)+1)]
dp[0][0] = True
for i in range(1,len(nums)+1):
for j in range(Sum//2+1):
if j<nums[i-1]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]]
return dp[-1][-1]
简化
class Solution:
def canPartition(self, nums: List[int]) -> bool:
Sum = sum(nums)
if Sum%2 == 1 or max(nums)>Sum//2:
return False
dp = [False]*(Sum//2+1)
dp[0] = True
for num in nums:
for j in range(Sum//2,num-1,-1):
dp[j] |= dp[j-num]
return dp[-1]
这一题跟上一题一样,也是背包问题,参考:link
139.word-break
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
link
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n = len(s)
dp = [False] * (n+1)
dp[0] = True
for i in range(n):
for j in range(i+1,n+1):
if dp[i] and s[i:j] in wordDict:
dp[j] = True
if j == n:
return True
return dp[-1]
仍然是背包问题