无关紧要
从这次接触的题目角度看,动态规划是在递归的基础上进行运算效率的优化,也就是通过存储中间计算结果减少重复计算。
递归的问题的关键是找到可以不停迭代的结果相关变量【状态】、迭代过程之间的关系【状态转移方程】。求解过程就是先解决边界位置的小问题,然后不断递推解决上层问题。
虽然预留的时间够长,但我每天2道题的计划下还是因为拖拉呀、理解慢呀、代码出错呀,浪费时间而没有收获,不如以大神写的算法介绍帖为主,学习精神和模式,学会怎么用吧
学习目的
- 完成动态规划相关经典题:5最长回文子串 72编辑距离 198打家劫舍 213打家劫舍2 516最长回文子序列 674最长连续递增序列;
- 学习动态规划算法思维。
Leetcode198. 打家劫舍
问题:
你是一个专业的XT,计划TQ沿街的房屋。每间房内都藏有一定的现金,影响你TQ的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被XT闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够TQ到的最高金额。
示例:
输入:[2,7,9,3,1]
输出:12
解释:TQ 1 号房屋 (金额 = 2), TQ3 号房屋 (金额 = 9),接着TQ 5 号房屋 (金额 = 1)。
TQ到的最高金额 = 2 + 9 + 1 = 12 。
提示:0 <= nums.length <= 1000 <= nums[i] <= 400
思路
- 确定状态,用数组dp记录方便复用
状态:累计DQ金额?或者是最临近房屋是否被D?本题选择截至当前位置的累计DQ金额为状态。 - 确定当前位置是否选择对状态的影响——状态转移方程
对于第k个位置,是是否DQ第k个房屋的状态比较。如果DQ第k个房屋,那位置k-2的累计DQ金额+第k个房屋的金额是总价值,否则不DQ第k个,以k-1个房屋的累计DQ金额为结果,两者进行比较,取较大值。 状态转移方程可表示为:dp[k] = max(dp[k-2]+nums[k], dp[k-1])
带入套路代码:
for i in range(len(nums)):
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
- 边界值和dp相关
dp数组整体的初始值和dp数组i=0和j=0的地方以及dp数组的长度
dp的初始化方法:
dp = [1 for _ in range(n)] 或者dp =[1]*n
dp = [[0]*n for _ in range(m)]或者dp=[[0 for _ in range(n)] for _ in range(m)]
当i=0时,仅一间房,自然只抢那间房,当有两件会抢金额较大的那间,即:
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
- 原始问题的输出
最后一个值?最大的数字?保存的最大值?
对于本题直接返回状态数组dp的最后一个值。
代码实现
class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
if len(nums) == 1:
return nums[0]
elif len(nums) == 2:
return max(nums[0], nums[1])
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-2]+nums[i], dp[i-1])
return max(dp)
Leetcode213. 打家劫舍 II
问题:
你是一个专业的XT,计划TQ沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被XT闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够TQ到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先TQ 1 号房屋(金额 = 2),然后TQ 3 号房屋(金额 = 2), 因为他们是相邻的。
思路:
- 确定状态,用数组dp记录方便复用
状态:累计DQ金额?或者是最临近房屋是否被D?
本题选择截至当前位置的累计DQ金额为状态。 - 确定当前位置是否选择对状态的影响——状态转移方程
相比于前一道题,房屋的线性排布变成了环状排列,可以把问题转成第0到n-1个房屋DQ与1到n个房屋DQ的比较。而两个问题都可以用前一道题的内容计算。
带入套路代码:
for i in range(len(nums)):
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
- 边界值和dp相关
dp数组整体的初始值和dp数组i=0和j=0的地方以及dp数组的长度
dp的初始化方法:
dp = [1 for _ in range(n)] 或者dp =[1]*n
dp = [[0]*n for _ in range(m)]或者dp=[[0 for _ in range(n)] for _ in range(m)]
当i=0时,仅一间房,自然只抢那间房,当有两件会抢金额较大的那间,即:
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
- 原始问题的输出
最后一个值?最大的数字?保存的最大值?
对于本题直接返回两个状态数组的最大值。
代码实现
class Solution:
'''存在重复代码,分别计算抢第一家和不抢第一家的结果,最后求最大'''
def rob(self, nums: List[int]) -> int:
size = len(nums)
if not nums:
return 0
if size == 1:
return nums[0]
elif size in [2,3]:
return max(nums[0], nums[1])
else:
# 抢第一个
first, second = nums[0], max(nums[0], nums[1])
for i in range(2, size):
if i != size - 1:
first, second = second, max(first+nums[i], second)
# 不抢第一个,抢最后一个
first2, second2 = 0, nums[1]
for i in range(2, size):
first2, second2 = second2, max(first2+nums[i], second2)
print(second, second2)
return max(second, second2)
class Solution:
'''思路一样,复用前一题的代码'''
def rob(self, nums: List[int]) -> int:
size = len(nums)
if not nums:
return 0
if size == 1:
return nums[0]
def do(ccc):
dp = [0]*(len(ccc)+2)
for j in range(0,len(ccc)):
dp[j+2] = max(dp[j+1], dp[j]+ccc[j])
return max(dp)
return max(do(nums[:-1]), do(nums[1:]))
Leetcode5. 最长回文子串
问题:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
思路:
- 确定状态,用数组dp记录方便复用
状态:回文子串的起止位置?回文子串的起止位置及最长长度?
本题选择 二维的dp数组,以起止位置+是否为回文子串为状态。 - 确定当前位置是否选择对状态的影响——状态转移方程
- 字符串首尾两个字符必须相等,否则不是回文
- 当字符串手尾相等时:如果子串是回文,则整体就是回文。所以把s[i,j]的问题转变成s[i+1,j-1]的问题。P(i,j) = P(i+1,j-1)∩(Si==Sj)
带入套路代码:
for i in range(len(s)):
for j in range(len(s)):
if s[i] == s[j]:
if j-1 -(i+1)+1<2:
dp[i][j] = 1
else:
dp[i][j] = dp[i+1][j-1]
- 边界值和dp相关
dp数组整体的初始值和dp数组i=0和j=0的地方以及dp数组的长度
dp的初始化方法:
dp = [[0]*n for _ in range(m)]或者dp=[[0 for _ in range(n)] for _ in range(m)]
数组dp对角线位置代表仅一个字符的子串,所以是回文串,dp[i][i] = 1
- 原始问题的输出
dp数组中找到是回文子串的位置,可知起止位点,继而计算子串长度即可返回结果。
代码实现
class Solution:
def longestPalindrome(self, s: str) -> str:
def expand(left, right):
while left>=0 and right<=len(s)-1 and s[left] == s[right]:
left -= 1
right += 1
return left+1,right-1,right+1-left #因为不通过while才return的,所有要+1-1地还原
start, end, _len = 0,0,0
for i in range(len(s)):
start0, end0, len0 = expand(i, i)
start1, end1, len1 = expand(i, i+1)
if len0 >= len1:
_start = start0
_end = end0
else:
_start = start1
_end = end1
if max(len0, len1) > _len:
start = _start
end = _end
_len = max(len0, len1)
return s[start:(end+1)]
class Solution:
'''耗时更久,内存消耗更大'''
def longestPalindrome(self, s: str) -> str:
_len = len(s)
matrix = [[0]*_len for i in range(_len)]
ans = ''
for num in range(1, _len+1, 1):
for i in range(0, _len-num+1, 1):
j = num+i-1
if i == j:
matrix[i][j] = True
elif i + 1 == j:
matrix[i][j] = (s[i] == s[j])
else:
matrix[i][j] = matrix[i+1][j-1] and s[i] == s[j]
if matrix[i][j] and num > len(ans):
print(i,j,num)
ans = s[i:(j+1)]
return ans
Leetcode516. 最长回文子序列
问题:
给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000。
示例 1:
输入:
“bbbab”
输出:
4
思路:
- 确定状态,用数组dp记录方便复用
状态:回文子串的起止位置?回文子串的起止位置及最长长度?
本题选择 二维的dp数组,以回文子串的起止位置及最长长度为状态。 - 确定当前位置是否选择对状态的影响——状态转移方程
- 字符串首尾两个字符相等,则s[i+1,j-1]+2为最长回文子序列长度,否则只有一个出现在最长回文子序列中,即max(s[i,j-1],s[i+1,j])
- 边界值和dp相关
dp数组整体的初始值和dp数组i=0和j=0的地方以及dp数组的长度
dp的初始化方法:
dp = [[0]*n for _ in range(m)]或者dp=[[0 for _ in range(n)] for _ in range(m)]
数组dp对角线位置代表仅一个字符的子串,所以是回文串,dp[i][i] = 1
- 原始问题的输出
dp[0][-1]。
代码实现
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
maxL = -1
dp = [[0]*n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for i in range(n-1, -1, -1):
for j in range(i+1, n):
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i][j-1], dp[i+1][j])
return max(max(dp))
Leetcode 674.最长连续递增序列
问题:
给定一个未经排序的整数数组,找到最长且连续的的递增序列。
示例 1:
输入: [1,3,5,4,7]
输出: 3
解释: 最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为5和7在原数组里被4隔开。
思路:
- 确定状态,用数组dp记录方便复用
状态:截至当前数的最长连续递增子序列长度?
本题选择 二维的dp数组,以当前数的最长连续递增子序列长度为状态。 - 确定当前位置是否选择对状态的影响——状态转移方程
- 如果当前数比前一个小,则这个数的最长连续递增序列为1
- 如果当前数可以继续追加到连续递增序列,则dp[k] = dp[k-1]+1
- 边界值和dp相关
dp的初始化方法: 一维的全为1的数组
dp = [1 for _ in range(n)] 或者dp =[1]*n
- 原始问题的输出
max(dp)。
代码实现:
def findLengthOfLCIS(self, nums: List[int]) -> int:
if not nums:return 0 #判断边界条件
dp=[1]*len(nums) #初始化dp数组状态
#注意需要得到前一个数,所以从1开始遍历,否则会超出范围
for i in range(1,len(nums)):
if nums[i]>nums[i-1]:#根据题目所求得到状态转移方程
dp[i]=dp[i-1]+1
else:
dp[i]=1
return max(dp) #确定输出状态
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
if not nums:
return 0
s, e, ans = 0, 0, 1
for i in range(1, len(nums)):
if nums[i] > nums[i - 1]:
e = i
else:
s = e = i
ans = max(ans, e - s + 1)
return ans
Leetcode72. 编辑距离
题太多,这道放弃了。