【算法突击】动态规划系列 (一)| 程序员面试 | 最大子数组和 | 最长递增子序列 | 最长公共子序列
文章目录
- 【算法突击】动态规划系列 (一)| 程序员面试 | 最大子数组和 | 最长递增子序列 | 最长公共子序列
- 1. 最大子数组和
- 1.1 题目描述
- 1.2 解题思路
- 1.3 代码实现
- 2. 最长递增子序列
- 2.1 题目描述
- 1.2 解题思路
- 1.3 代码实现
- 3. 最长公共子序列
- 3.1 题目描述
- 3.2 解题思路
- 3.3 代码实现
1. 最大子数组和
1.1 题目描述
给你一个整数数组 nums ,请你找出一个具有最大元素和 的连续子数组(子数组最少包含一个元素),返回其最大和(子数组 是数组中的一个连续部分)。
1.2 解题思路
(1)穷举法
以数组nums[-2,1,-3,4,-1,2,1,-5,4] 为例,该数组长度为9,设定子数组[i,j](其中j≥i,i≥0, i<9)为最大和的子数组。直接两个嵌套的for循环即可得出答案,时间复杂度是O(n^2)。复杂度太高,是否还有优化空间?
(2)动态规划
以数组nums [-2,1,-3,4,-1,2,1,-5,4] 为例,设定数组f[i]为第i位元素结尾时,子数组中最大的元素和(i≥0, i<9),我们接下来寻找一下这个例子中蕴藏的规律。
- 当指针i=0时
显然f[0] = nums[0],当数组中只有一个元素时,其连续子数组之和就是这个元素;
- 当指针i=1时
当前数组nums中只有[-2,1]两个元素,显然-2 + 1 < 1,所以f[1] = nums[1];
- 当指针i=2时
nums={-2, 1, -3},由于已经知道了f[1],所以只需比较nums[2] + f[1] 和 当前 nums[2]的大小即可决定在f[2]的大小,故f[2]=nums[2] + f[1] ;
- 当指针i=3时
f[3] = nums[3]
- 以此类推,当指针i=8时
此时已经计算出数组中全部元素的最大连续和了,发现f[6]是最大的。
基于上述的推导过程不难发现,f[i]只和f[i-1]和nums[i]的值有关系,且f[0]有初始值,我们将上面分析出的规律总结成数学表达式的形式,如下:
f [ i ] = { n u m s [ 0 ] , 当i=0时 m a x { n u m s [ i ] , n u m s [ i ] + f [ i − 1 ] } , 当i>0时 f[i] = \begin{cases} nums[0], & \text{当i=0时} \\ max\{nums[i], nums[i] + f[i-1]\}, & \text{当i>0时} \\ \end{cases} f[i]={nums[0],max{nums[i],nums[i]+f[i−1]},当i=0时当i>0时
这个就是动态规划中最重要的状态转移方程,有了这个东西我们可以根据已有的简单结论推导出复杂场景下的结果。另外可以看到这里只需要一次遍历即可解决问题,时间复杂度降到了O(n)。
1.3 代码实现
public int maxSubArray(int[] nums) {
int[] f = new int[nums.length]; // 以第i个元素结尾的 最大连续子数组之和
f[0] = nums[0];
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
f[i] = Math.max(nums[i], nums[i] + f[i - 1]);
max = Math.max(f[i], max);
}
return max;
}
2. 最长递增子序列
2.1 题目描述
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
1.2 解题思路
对于这个题,建模的时候我们将f[i]定义成“第i位元素结尾时,当前数组中最大递增序列长度”,其中i为指针。
以输入nums = [10,9,2,5,3,7,101,18]为例,推导一下整个f数组的构建过程:
- 当指针i=0时
当前数组中只有一个元素,nums[0],即当前递增子序列长度恒定为1;
- 当指针i=1时
当前nums[1] < nums[0],f[1] = 1;
- 当指针i=2时
同上,f[2]=1
- 当指针i=3时
nums[3] > nums[2], f[3] = f[2] + 1;
-
当指针i=4时
这个时候会发现一个问题,因为nums[4] < nums[3],但是由于题目规则允许跳跃元素,且nums[4] > nums[2],此时的f[4] = f[2] + 1 = 2。由于这个特殊的规则,我们必须再加入一个回溯指针j,在nums[i] < nums[i] - 1时,往i - 1之前进行回溯,取最大的长度f[j] + 1,才能得到当前第i个元素结尾时最长的递增序列长度。在i=4时,j=2即可得到f[4]的正确值。
-
以此类推,当指针i=7时
此时的回溯指针j=5时,可以使得f[7]最大,f[7] = f[5] + 1。
基于上述的推导过程不难发现,f[i]和f[i-1]之间的关系并不固定,我们必须引入i的回溯指针j才能求出正确结果,把上述规律总结成数学表达式为:
f [ i ] = { 1 , 当i=0时 m a x { f [ j ] + 1 } , 当i>0时,j<i f[i] = \begin{cases} 1 , & \text{当i=0时} \\ max\{f[j] + 1\}, & \text{当i>0时,j<i} \\ \end{cases} f[i]={1,max{f[j]+1},当i=0时当i>0时,j<i
1.3 代码实现
public int lengthOfLIS(int[] nums) {
int[] f = new int[nums.length];
int max = 1;
// 初始化
for(int i=0;i<nums.length;i++) {
f[i] = 1;
}
// 状态转移方程
for(int i=1;i<nums.length;i++) {
for(int j=i-1;j<i;j++) {
if(nums[i] > nums[j] && f[i] < f[j] + 1){
f[i] = f[j] + 1;
}
}
max = Math.max(max, f[i]);
}
return max;
}
3. 最长公共子序列
3.1 题目描述
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:
它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0
3.2 解题思路
为了简单起见,我们将字符串text1称为A串,text2称为B串。
这个题,我们需要在两个不同的字符串中来回比较,所以我们的f[i]不能再是一维的,设f[i][j]为A串中以第i个元素结尾,B串中以第j个元素结尾时的最长公共子序列长度。以text1 = “abcde”, text2 = "ace"为例,推导f数组的构建过程:
- 当指针i=0, j=0时
因为A[i] = B[j],所以f[i][j] = 1
更进一步,可以推导出所有f[0][j]和f[i][0]的值
- 当指针i=1, j=1时
A[1] ≠ B[2],所以f[1][2] = f[1][0]
- 当指针i=1, j=2时
A[1] ≠ B[2],所以f[1][2] = f[1][1]
- 当指针i=2, j=1时
A[2] = B[1], f[2][1] = f[1][0]+ 1
- 当指针i=3, j=1时
A[3] ≠ B[1], f[3][1] = f[2][1]+ 1,要注意的是,这里有两种选择{abcd, a},{abc, ac},选其中最大的即可
- 当指针i=4, j=2时
A[4] = B[2], f[4][2] = f[3][1]
基于上述的推导过程不难发现,初始化过程之外,匹配的时候需要分两种情况讨论,当A[i] = B[j]时,f[i][j]=f[i-1][j-1] + 1;当A[i] ≠ B[j]时,f[i][j]有两种取值情况,f[i-1][j]与f[i][j-1],取两者中的最大值即可。其数学表达式为:
f [ i ] [ j ] = { 1 , 当i=0且j=0时 m a x { f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] } , 当A[i] != B[j]时 f [ i − 1 ] [ j − 1 ] + 1 , 当A[i]=B[i]时 f[i][j] = \begin{cases} 1 , & \text{当i=0且j=0时} \\ max\{f[i-1][j], f[i][j-1]\}, & \text{当A[i] != B[j]时} \\f[i-1][j-1] + 1 , & \text{当A[i]=B[i]时} \\ \end{cases} f[i][j]=⎩ ⎨ ⎧1,max{f[i−1][j],f[i][j−1]},f[i−1][j−1]+1,当i=0且j=0时当A[i] != B[j]时当A[i]=B[i]时
3.3 代码实现
public int longestCommonSubsequence(String text1, String text2) {
char[] A = text1.toCharArray();
char[] B = text2.toCharArray();
int[][] f = new int[A.length][B.length];
// 初始化
f[0][0] = A[0] == B[0] ? 1 : 0;
for (int i = 1; i < A.length; i++) {
f[i][0] = A[i] == B[0] ? 1 : f[i - 1][0];
}
for (int j = 1; j < B.length; j++) {
f[0][j] = A[0] == B[j] ? 1 : f[0][j - 1];
}
// 状态转移方程
for (int i = 1; i < A.length; i++) {
for (int j = 1; j < B.length; j++) {
if (A[i] != B[j]) {
f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
} else {
f[i][j] = f[i - 1][j - 1] + 1;
}
}
}
return f[A.length - 1][B.length - 1];
}