前言
与其知道动态规划是什么,不如知道什么什么时候用动态规划。当一个问题可以被拆解成相同性质但规划更小的递归之问题时,则可动态规划。动态规划时,可能不是简单的取前一个子问题的值,也有可能结合贪心来取max()/min()。除此之外,当我们碰到有序,就可以联想到双指针/二分法,利用好有序条件,将时间复杂度降下来。
一、最长递增子序列
二、抓特点刨析问题
1、贪心+二分
// 最长递增子序列。
public class LengthOfLIS {
/*
target:找 最长严格递增子序列的 长度。子序列就意味着不能改变相对顺序。
子序列如何找?
这个子序列不要求连续,则不能简单的确定起点和终点找到子序列。
将子序列存入list.
每个元素具有 值属性 & 位置属性,后面的值要大于前面的值则可加入list
有一个关键问题:已经有一个递增序列了,后面出现一个更小的递增序列怎么办?
既然是从前往后遍历,那么在位置上,后一个元素都是满足位置条件的,那么这些小的递增序列就可以替代前面的大递增序列元素。
毕竟不需要得到这个序列,只需要求长度。
*/
public int lengthOfLIS(int[] nums) {
List<Integer> arr = new ArrayList<>();
for (int num : nums) {
if (arr.size() == 0 || arr.get(arr.size() - 1) < num) arr.add(num);
else {
int idx = binarySearch(arr, num);
arr.set(idx, num);
}
}
return arr.size();
}
private int binarySearch(List<Integer> arr, int target) {
int low = 0, high = arr.size() - 1;
while (low < high) {
// 取刚好等于或大于自己的那个位置。
int mid = low + (high - low >>> 1);
int midVal = arr.get(mid);
if (midVal < target) low = mid + 1;
else high = mid;
}
return low;
}
}
2、动态规划+贪心
// 一题多解,打开视野。
class LengthOfLIS2 {
/*
target:找 最长严格递增子序列的 长度。子序列就意味着不能改变相对顺序。
递增,要么是从前往后找大的;要么是从后往前找小的。
问题拆解:从nums[0:n]的子序列最大递增长度,和nums[0:i]有关,0<=i<=n-1;
拆解之后的问题和原问题有相同性质,却规模变得更小一些,所以可动态规划,很明显这里动起来需要取max,所以为DP + greed
f[n] = max(f(x))当且仅当nums[n - 1] > nums[x - 1]
*/
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] f = new int[n + 1];
int max = 1;
for (int i = 1; i <= n; i++) {
f[i] = 1;
for (int j = 1; j < i; j++) {
if (nums[i - 1] > nums[j - 1]) {
f[i] = Math.max(f[j] + 1, f[i]);
}
}
max = Math.max(max, f[i]);
}
return max;
}
}
总结
1)一题多解,打开视野。
2)贪心+DP,考察逻辑思维 & 数据结构基础的好题型。
3)抓问题特点/问题核心,刨析问题,联想/组合解决方案。
参考文献
[1] LeetCode 最长递增子序列