动态规划 之 子序列系列 算法专题
一. 最长递增子序列
最长递增子序列
- 状态表示
dp[i] 表示以i位置为结尾, 最长的递增子序列 - 状态转移方程
1.如果以i位置为开头 那么长度为1
2.如果以i位置结尾, 由于不知道从前面哪一个位置取子序列, 所以要定义一个j(0 <= j <= i - 1), 找到以j位置结尾的所有子序列中, 最长的, 即dp[j]
- dp[i] = dp[j] + 1
- 初始化
可以先全部初始化为1 - 填表顺序
从左往右 - 返回值
返回dp表中的最大值
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
int ret = 1;
for(int i = 1; i < n; i++){
int max = 1;
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
}
ret = Math.max(dp[i], ret);
}
return ret;
}
}
二. 摆动序列
摆动序列
- 状态表示
f[i] 表示以i位置为结尾, 最后呈"上升"趋势最长的摆动序列
g[i] 表示以i位置为结尾, 最后呈"下降"趋势最长的摆动序列 - 状态转移方程
1.如果以i位置为开头 那么长度为1
2.如果以i位置结尾, 由于不知道从前面哪一个位置取子序列, 所以要定义一个j(0 <= j <= i - 1), 找到以j位置结尾的所有子序列中, 最长的, 即dp[j]
if(nums[j] < nums[i]){
f[i] = Math.max(g[j] + 1, f[i]);
}else if(nums[j] > nums[i]){
g[i] = Math.max(f[j] + 1, g[i]);
} - 初始化
可以先全部初始化为1 - 填表顺序
从左往右 - 返回值
返回dp表中的最大值
class Solution {
public int wiggleMaxLength(int[] nums) {
int n = nums.length;
int[] f = new int[n];
int[] g = new int[n];
Arrays.fill(f, 1);
Arrays.fill(g, 1);
int ret = 1;
for(int i = 1; i < n; i++){
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
f[i] = Math.max(g[j] + 1, f[i]);
}else if(nums[j] > nums[i]){
g[i] = Math.max(f[j] + 1, g[i]);
}
ret = Math.max(ret, Math.max(f[i], g[i]));
}
}
return ret;
}
}
三. 最长数对链
最长数对链
前置处理: 为了保证使用动态规划, 填表顺序是从左往右, 所以要先对数组进行排序, 只需要根据第一个数进行排序即可, 保证选择i位置时, 不会选到i后面的位置
- 状态表示
dp[i] 表示以i位置为结尾, 最长数对链 - 状态转移方程
1.如果以i位置为开头 那么长度为1
2.如果以i位置结尾, 由于不知道从前面哪一个位置取子序列, 所以要定义一个j(0 <= j <= i - 1), 找到以j位置结尾的所有子序列中, 最长的, 即dp[j]
- dp[i] = dp[j] + 1
- 初始化
可以先全部初始化为1 - 填表顺序
从左往右 - 返回值
返回dp表中的最大值
class Solution {
public int findLongestChain(int[][] pairs) {
Arrays.sort(pairs, (a, b) -> a[0] - b[0]);
int n = pairs.length;
int[] dp = new int[n];
Arrays.fill(dp, 1);
int ret = 1;
for(int i = 1; i < n; i++){
for(int j = 0; j < i; j++){
if(pairs[j][1] < pairs[i][0]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
ret = Math.max(dp[i], ret);
}
}
return ret;
}
}
四. 最长定差子序列
最长定差子序列
- 状态表示
dp[i] 表示以i位置为结尾, 最长定差子序列 - 状态转移方程
1.如果以i位置为开头 那么长度为1
2.如果以i位置结尾, 由于不知道从前面哪一个位置取子序列, 所以要定义一个j(0 <= j <= i - 1), 找到以j位置结尾的所有子序列中, 最长的, 即dp[j]
只需要找到在前面中, 大小为arr[i] - difference 即可, 并且由于找的值是固定的, 那么我们只需要找到前面的其中一个即可
所以我们可以将dp表放在hash表中
- 初始化
可以先全部初始化为1(在写代码时有技巧) - 填表顺序
从左往右 - 返回值
返回dp表中的最大值
class Solution {
public int longestSubsequence(int[] arr, int difference) {
//创建一个hash表, 在hash表中做dp
Map<Integer, Integer> hash = new HashMap<>();//arr[i], dp[i]
int ret = 1;
for(int a : arr){
hash.put(a, hash.getOrDefault(a - difference, 0) + 1);
ret = Math.max(ret, hash.get(a));
}
return ret;
//下面的方法超时
// int n = arr.length;
// int[] dp = new int[n];
// Arrays.fill(dp, 1);
// int ret = 1;
// for(int i = 1; i < n; i++){
// for(int j = 0; j < i; j++){
// if(arr[i] - arr[j] == difference){
// dp[i] = Math.max(dp[i], dp[j] + 1);
// }
// ret = Math.max(ret, dp[i]);
// }
// }
// return ret;
}
}
五. 最长的斐波那契子序列的长度
最长的斐波那契子序列的长度
-
状态表示
dp[i] 表示以i位置为结尾, 最长斐波那契子序列的长度, 这样我们并不知道前面两个数是谁, 所以我们可以固定两个位置, 以i和j位置为结尾, 这样就可以找到k位置 -
状态转移方程
如果找到k位置, 并且这个位置在i, j的前面, 那么dp[i][j] = dp[k][i] + 1
因为要频繁查找k的位置, 可以将arr中下标和值的映射关系放在hash表中
- 初始化
可以先全部初始化为2 - 填表顺序
从上往下 - 返回值
返回dp表中的最大值, 但是如果找不到斐波那契序列, 那么应该返回0
class Solution {
public int lenLongestFibSubseq(int[] arr) {
int n = arr.length;
Map<Integer, Integer> hash = new HashMap<>();
for(int i = 0; i < n; i++){
hash.put(arr[i], i);
}
int ret = 2;
int[][] dp = new int[n][n];
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
dp[i][j] = 2;
}
}
for(int i = 2; i < n; i++){
for(int j = 1; j < i; j++){
int a = arr[i] - arr[j];
if(a < arr[j] && hash.containsKey(a))
dp[j][i] = dp[hash.get(a)][j] + 1;
ret = Math.max(dp[j][i], ret);
}
}
return ret == 2 ? 0 : ret;
}
}