若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。
动态规划法仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量,
一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。
一般用于求最值问题。
适用动态规划方法求解,问题需要满足三个要素:
确定动态规划状态
写出状态转移方程(画出状态转移表)
考虑初始化条件
这是决定整个程序能否跑通的重要步骤,当我们确定好状态转移方程,我们就需要考虑一下边界值,边界值考虑主要又分为三个地方:
- dp数组整体的初始值
- dp数组(二维)i=0和j=0的地方
- dp存放状态的长度,是整个数组的长度还是数组长度加一,这点需要特别注意。
主要有以下三种形式,对于具体问题,我们一定要想清楚到底dp数组里存储的是哪些值,最后我们需要的是数组中的哪些值:
- 返回dp数组中最后一个值作为输出,一般对应二维dp问题。
- 返回dp数组中最大的那个数字,一般对应记录最大值问题。
- 返回保存的最大值,一般是Maxval=max(Maxval,dp[i])这样的形式。
总结几种Python常用的初始化方法
- 对于产生一个全为1,长度为n的数组:
- dp=[1 for _ in range(n)]
- dp=[1]*n
- 对于产生一个全为0,长度为m,宽度为n的二维矩阵:
- dp=[[0 for _ in range(n)] for _ in range(m)]
- dp=[[0]*n for _ in range(m)]
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
# 边界条件
if not nums:
return 0
# 定义dp数组,存放每个状态下最长上升子序列的长度
dp = [1]*len(nums)
# dp = [1 for _ in range(len(nums))]
# i指针依次遍历nums数组中的元素
for i in range(len(nums)):
# j指针依次遍历i指针之前的元素
for j in range(i):
# 比较元素大小,如果当前i元素比前面的某个元素大
if nums[i] > nums[j]:
# 更新当前状态
dp[i] = max(dp[i], 1+dp[j])
return max(dp)
给定一个未经排序的整数数组,找到最长且连续的的递增序列,并返回该序列的长度。
示例 1:
输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
示例 2:
输入: [2,2,2,2,2]
输出: 1
解释: 最长连续递增序列是 [2], 长度为1。
注意:数组长度不会超过10000。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
# 边界条件
if not nums:
return 0
# dp数组
dp = [1]*len(nums)
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
dp[i] = dp[i-1]+1
return max(dp)
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution:
def longestPalindrome(self, s: str) -> str:
# 边界条件
if len(s) < 2:
return s
# dp二维数组,dp[j][i]表示j->i是否回文
dp = [[False for _ in range(len(s))] for _ in range(len(s))]
# 最大回文子串长度
max_len = 1
# 记录最大回文子串的起始位置
start = 0
for i in range(len(s)):
for j in range(i):
# 注意i和j谁大,这里i>=j
if s[j] == s[i]:
# 子串(j+1,i-1)为空或只有一个字符
if i-j < 3:
dp[j][i] = True
else:
dp[j][i] = dp[j+1][i-1]
# 只有整个i->j字符串为回文才更新
# 这个条件必不能少,否则只要首尾字符相等就会更新cur_len
if dp[j][i]:
# 当前回文子串长度
cur_len = i-j+1
# 如果当前回文子串长度大于之前记录的最大长度
if cur_len > max_len:
# 更新最大回文子串长度
max_len = cur_len
# 记录最大回文子串的起始位置
start = j
return s[start:start+max_len]
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1:
输入:
“bbbab”
输出:
4
一个可能的最长回文子序列为 “bbbb”。
示例 2:
输入:
“cbbd”
输出:
2
一个可能的最长回文子序列为 “bb”。
提示:
1 <= s.length <= 1000
s 只包含小写英文字母
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
# 边界条件
if len(s) < 1:
return 0
if len(s) < 2:
return 1
# dp二维数组
dp = [[0 for _ in range(len(s))] for _ in range(len(s))]
# 初始化dp对角线
for i in range(len(s)):
dp[i][i] = 1
# 从dp二维数组的右下角开始,到右上角结束
for i in range(len(s),-1,-1):
for j in range(i+1,len(s)):
if s[j] == s[i]:
# dp[i+1][j-1]表示i->j字符串去掉头尾
dp[i][j] = dp[i+1][j-1]+2
else:
dp[i][j] = max(dp[i+1][j],dp[i][j-1])
return dp[0][len(s)-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’)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/edit-distance
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
# word1长度
m = len(word1)
# word2长度
n = len(word2)
# 边界条件
if m == 0:
return n
if n == 0:
return m
# dp二维数组
dp = [[0 for _ in range(n+1)] for _ in range(m+1)]
# 初始化第一行
for j in range(n+1):
dp[0][j] = j
# 初始化第一列
for i in range(m+1):
dp[i][0] = i
# 从左上角到右下角
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], dp[i][j-1], dp[i-1][j-1]) + 1
return dp[-1][-1]
难度:Hard(面试常考)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
# 边界条件
if not nums:
return 0
if n == 1:
return nums[0]
# dp一维数组
dp = [0 for _ in range(n)]
# 初始化前两个元素
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
# 反向思维,分两种情况,偷了第i个房间或者没偷第i个房间
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
return dp[-1]
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
# 边界条件
if not nums:
return 0
if n <= 2:
return max(nums)
# 打家劫舍1
def helper(array: List[int]):
if len(array) <= 2:
return max(array)
dp = [0]*len(array)
dp[0] = array[0]
dp[1] = max(array[0], array[1])
for i in range(2, len(array)):
dp[i] = max(dp[i-2]+array[i], dp[i-1])
return dp[-1]
# 打家劫舍2,在打家劫舍1的基础上分两种情况
# 第一种,0->n-2
# 第二种,1->n-1
return max(helper(nums[0:-1]), helper(nums[1:n]))
感想:
最难的还是状态转移方程的定义!
参考:
1、动态规划详解(修订版)
2、动态规划