目录
写在前面
动态规划一般有两种思路:
1、第一种思路模板是一个一维的 dp 数组:
int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
dp[i] = 最值(dp[i], dp[j] + ...)
}
}
例如:「最长递增子序列」,在这个思路中 dp 数组的定义是:以i结尾(包括i在内)的数组最长的递增子序列的长度为 dp[i]
2、第二种思路模板是一个二维的 dp 数组:
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
运用相对更多一些,尤其是涉及两个字符串/数组的子序列。
常见题型
这里把leetcode常见的采用动态规划的各类最长序列、字符串、数组等问题罗列出来,做一个归纳。
- 输入为单个字符串或者数组
名称 | 动态方程含义 |
最长连续递增(上升)序列(数组) | 一维dp[i],表示截止到i这个数字(必须包括i)的最长连续上升序列的长度 |
最长上升子序列(数组) | 一维dp[i],表示截止到i这个数字(必须包括i)的最长上升子序列的长度 |
最长上升子序列个数(数组) | 两个一维dp[i],conunt[i],分别表示截止到i这个数字(必须包括i)的最长上升子序列的长度以及个数 |
最长回文子串(字符串) | 二维dp[j][i],表示j-i区间构成的子串是否为回文子串 |
最长回文子序列(字符串) | 二维dp[j][i],表示j-i区间构成的子串的回文子序列的长度 |
可以看到,也就是「最长连续递增序列」以及「最长递增子序列」采用了一维的dp,其余都是二维的dp。这两个问题也是最为简单。
- 输入为两个字符串和数组
名称 | 动态方程含义 |
最长重复子数组 | 二维dp[j][i],表示截止s[:j]与s[:i]的重复子数组的长度 |
最长公共子序列 | 二维dp[j][i],表示截止s[:j]到s[:i]的最长公共子序列的长度 |
例题分析
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续的的递增序列,并返回该序列的长度。
例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。
要点:
(1)因为每一个数字都可与自身构成连续递增,故动态数组dp初始化1。
(2)只要关注当前位置i与其前一个的位置i-1的值的大小:
- nums[i]>nums[i-1]:i至少可以与i-1形成一个连续递增序列,因为它们俩挨着,并且是递增的,长度上是dp[i-1]+1;
- nums[i]<=nums[i-1],这时候不能形成连续递增序列,后一个数要比前一个数小,呈下降的趋势,与自身形成连续递增
def findLengthOfLCIS(self, nums):
if not nums:
return 0
length = len(nums)
dp = [1 for i in range(length)] #创建一维的动态数组
for i in range(1,length):
if nums[i] > nums[i-1]:
dp[i] = dp[i-1]+1
return max(dp)
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。如:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
要点:
(1)这里定义dp[i]为截止到i这个数字,并包含i在内的最长上升子序列的长度。
(2)因为每一个数字都可与自身构成上升,故动态数组dp初始化1 。
(3) 如果nums[i] > nums[j],那么 nums[i] 一定能够融入 dp[j] 从而形成更大的序列,这个序列的长度是 dp[j] + 1,即为状态转移方程。
def lengthOfLIS(self, nums):
length = len(nums)
dp = [1 for i in range(length)]
for i in range(length):
for j in range(i-1,-1,-1): #也可以是for j in range(0,i)
if nums[i] > nums[j]:
dp[i] = max(dp[i],dp[j]+1)
return max(dp)
673. 最长递增子序列的个数
给定一个未排序的整数数组,找到最长递增子序列的个数。
例1:输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。例2:输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
本题在上一题的基础上继续求取最长子序列的个数。因此动态方程有两个,dp[i]和count[j],分别表示截止到i这个数字(必须包括i)的最长上升子序列的长度以及个数。
要点:
(1)每一个数字都可与自身构成连续递增,故动态数组dp初始化1 ,同时个数count也等于1。
(2)count状态转移方程:
- 当dp[j] + 1>dp[i],说明第一次找到以nums[i]为结尾的最长递增子序列,长度为dp[j] + 1,进而可以推出counter[i] = counter[j], 即以nums[i]结尾的最长递增子序列的组合数=以nums[j]结尾的最长递增子序列的组合数。这个可以这么理解:当nums[i] > nums[j],即就是在count[j]每一种组合结尾补上nums[i],对于组合数本身是没有增加的,唯独只是递增子序列的长度+1了;
- 当dp[j] + 1=dp[i],说明这个长度已经找到过一次了,counter[i] += counter[j],即现在的组合方式+counter[j]的组合方式。
def findNumberOfLIS(self, nums):
length = len(nums)
dp = [1 for i in range(length)]
count = [1 for i in range(length)]
for i in range(length):
for j in range(i-1,-1,-1):#也可以是for j in range(0,i)
if nums[i] > nums[j]:
if dp[j] + 1 > dp[i]:
dp[i] = dp[j]+1
count[i] = count[j]
elif dp[j] + 1 == dp[i]:
count[i] = count[i] + count[j]
maxlength = max(dp)
res = 0
for i in range(length):
if dp[i] == maxlength:
res = res + count[i]
return res
5. 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
例 1:输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
例 2:输入: "cbbd"
输出: "bb"
要点:
(1)解决这类问题的核心思想就是两个字“延伸”,具体来说:
- 如果一个字符串是回文串,那么在它左右分别加上一个相同的字符,那么它一定还是一个回文串
- 如果在一个不是回文字符串的字符串两端添加任何字符,或者在回文串左右分别加不同的字符,得到的一定不是回文串
(2)用 dp[i][j] 表示 s 中从 i 到 j(包括 i 和 j)是否可以形成回文,状态转移方程只是将上面的描述转化为代码即可:
if s[i] === s[j] and dp[i + 1][j - 1]:
dp[i][j] = true
def longestPalindrome(self, s):
length = len(s)
dp = [[False for i in range(length)] for j in range(length)]
logestLen = 0
logestSubStr = ''
for i in range(length):
for j in range(i,-1,-1): #写成for j in range(0,i+1) 也可
if i-j <= 1:
if s[i] == s[j]:
dp[j][i] = True
if i - j + 1 > logestLen:
logestSubStr = s[j:i+1]
logestLen = i - j + 1
else:
if s[i] == s[j] and dp[j+1][i-1] == True:
dp[j][i] = True
if i - j + 1 > logestLen:
logestSubStr = s[j:i+1]
logestLen = i - j + 1
return logestSubStr
516. 最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
例 1:输入:"bbbab"
输出:4
一个可能的最长回文子序列为 "bbbb"。例 2:输入:"cbbd"
输出:2
一个可能的最长回文子序列为 "bb"。
与上面最长回文子串不同,最长回文子序列不要求连续,比如对于输入bbbab,最长回文子序列为bbbb,长度为4。
要点:
(1)dp[j][i]表示s[j⋯i]中,最长的回文序列长度是多少。
- 当s[i] = s[j],那么就说明在原先的基础上又增加了回文子序列的长度,dp[j][i] = dp[j+1][i-1] + 2
- 当s[i] !=s[j],那么那么说明s[i]、s[j]至少有一个不在回文子序列中,这时的dp[j][i]只需取两者之间的最大值即可。即dp[j][i] = max(dp[j+1][i],dp[j][i-1])。
(2)第二个循环的遍历顺序必须为倒序。由于我们设定i>j,因此当j>i时,dp = 0。在计算dp[j][i]时就必须要确保左边dp[j][i-1]、下边dp[j+1][i]以及左下边dp[j+1][i-1]的数值是存在的。
以计算s[0:2]之间的最长回文子序列dp[0][2]为例,第二个循环如果采用正序的话,由于s[0]=c,不等于s[2]=b,dp[0][2] = max(dp[1][2],dp[0][1]),而此时dp[1][2]还没有被计算出来,故会出错的。而采用倒序的话,则首先会计算s[1:2]之间的最长回文子序列dp[1][2],之后在计算dp[0][2],即求出了s[0:2]之间的最长回文子序列。
ps:在最长回文子串中,第二个循环之所以也可以用正序,是因为当s[j]=s[i]时,如果dp[j+1][i-1]为True,那么dp[j][i]也为True,即就是说dp[j][i]只需要由左下角的dp[j+1][i-1]来确定,而正序条件下,左下角的dp[j+1][i-1]都可以被预先算出来
def longestPalindromeSubseq(self, s: str) -> int:
length = len(s)
dp =[[0] * length for _ in range(length)] #dp[j][i]表示 s的 j个字符到第i个字符组成的子串中,最长的回文序列长度是多少。
for i in range(length):
dp[i][i] = 1
for j in range(i-1,-1,-1):
if s[j] == s[i]: #两者相等
dp[j][i] = dp[j+1][i-1] + 2
else: #两者不等于
dp[j][i] = max(dp[j+1][i],dp[j][i-1])
return dp[0][length-1]
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"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)动态数组dp[i][j]表示的是截止到text1[:i]与text1[:j]的最长公共子序列。所谓子序列是不需要连续的。
(2)动态数组dp[i][j]更新如下:
- 如果text[i] == text[j]表明多了一共公共的字符,因此dp[i][j] = dp[i-1][j-1] + 1
- 如果text[i] != text[j],那么就在text1[:i-1]与text1[:j] 或者 text1[:i]与text1[:j-1]的公共子序列中取最大值,即dp[i][j] = max(dp[i-1][j],dp[i][j-1])
(3)在生成动态dp的过程中,我们多加了一行一列的原因在于,dp[i-1]很可能定位到矩阵的最后一行。例如求取'bm'和'mb'两个字符串的最长公共序列,正常答案为1,可以是'b'或者'm'。用i表示text1截止的字符位置,j表示textj截止的字符位置。如果没有多加一列多加一行,经过第一次外面j的大循环,我们得到的dp如下,即dp[1][0]为1,因为text1 = ‘bm’,text2 = 'm',最长公共子序列长度为1。开始第二次j的大循环时,此时text2 = ‘mb’,text1 = ‘b’,dp[0][1] = dp[-1][0] + 1,而dp[-1][0]就是dp[1][0],因此dp[0][1]就等于dp[1][0] + 1 = 2。但是实际上,两者的最长公共子序列为1。
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
if not text1 or not text2:
return 0
n1,n2 = len(text1),len(text2)
dp = [[0 for j in range(n2+1)] for i in range(n1+1)]
for j in range(1,n2+1):
for i in range(1,n1+1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j],dp[i][j-1])
return max(max(dp))
718. 最长重复子数组
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
输入:A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3, 2, 1] 。
本题与上一题十分类似,不同之处在于所求的公共的长度最长的子数组是连续的,而前一题的子序列可以是不连续的。
要点:
(1)动态数组dp[i][j]表示的是截止到A[:i]与B[:j]的最长公共子数组,初始化为0,动态转移方程如下:
- 若A[i] = B[j],说明多增加了一个连续,dp[i][j] = dp[i-1][j-1] + 1。
- 若A[i] != B[j],说明此时不连续了,长度为0,即为初始化的0
(2)最后return 不要写max(max(dp))。由于dp是一个二维数组,储存的是截止到A[:i]与B[:j]的最长公共子数组,因此想当然就会直接return dp的最大值。但是内层的max(dp)返回的是dp子列表首元素最大的那个列表,之后外层的max再找出这一行的最大值。以A=[1,2,3,2,1],B=[3,2,1,4,7]为例,求得的dp如下,正确答案为3,但是内部max(dp)后会首先返回第4行,因为第4行的第2个元素为1,最大。之后在max一次就会返回1,导致错误。
def findLength(self, A, B):
if not A or not B:
return 0
n1,n2 = len(A),len(B)
dp = [[0 for j in range(n2+1)] for i in range(n1+1)]
ans = 0
for j in range(1,n2+1):
for i in range(1,n1+1):
print(A[i-1],B[j-1])
if A[i-1] == B[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
ans = max(ans,dp[i][j])
return ans #不要写max(max(dp))