动态规划O(N^2)

dp[i]含义: 以i结尾的最长递增子序列长度

  1. public int lengthOfLIS(int[] nums) {
  2. int[] dp = new int[nums.length];
  3. for (int i = 0; i < nums.length; i++) {
  4. dp[i] = 1;
  5. for (int j = 0; j < i; j++) {
  6. if (nums[i] > nums[j]) {
  7. dp[i] = Math.max(dp[i], dp[j] + 1);
  8. }
  9. }
  10. }
  11. int res = 0;
  12. for (int i = 0; i < dp.length; i++) {
  13. res = Math.max(res, dp[i]);
  14. }
  15. return res;
  16. }

优化O(N * logN)

考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小

  • dp含义不变
  • ends数组用来加速每一步的dp而存在的
    • ends数组一开始全是无效区
    • ends[i]的含义: 代表所有长度为 i + 1的递增子序列的最小结尾是ends[i]

截图_20210311090330.png
截图_20210411090421.png

  • 接下来遍历到原数组1位置的数1了, 在ends里找≥1最左的位置,更新
    • 截图_20210511090503.png
    • 1所在的有效区连同左边有几个数就是它的dp值
    • 截图_20210511090525.png
  • 接下来遍历到原数组2位置的数6了,在ends里找不到≥6最左的位置,扩充有效区(扩充有效区代表长度为1的递增子序列可以被扩张成长度为2)
    • 截图_20210511090557.png
    • 6所在的有效区连同左边有几个数就是它的dp值
    • 截图_20210611090614.png
  • 截图_20210611090644.png
  • 截图_20210711090707.png
  • 截图_20210711090737.png
  • 截图_20210811090801.png
  • 截图_20210811090822.png

时间复杂度分析

来了一个num,你就去ends二分找到≥num最左的数,然后做操作,然后看它及其左边有几个数,就更新dp值
时间复杂度O(N * logN)

  1. //O(N * logN)
  2. public int lengthOfLIS2(int[] nums) {
  3. if (nums == null || nums.length == 0) {
  4. return 0;
  5. }
  6. int[] ends = new int[nums.length];
  7. ends[0] = nums[0];
  8. int right = 0; // 0..right是有效区
  9. int l = 0;
  10. int r = 0;
  11. int m = 0;
  12. int max = 1;
  13. for (int i = 1; i < nums.length; i++) {
  14. l = 0;
  15. r = right;
  16. while (l <= r) {
  17. m = (l + r) / 2;
  18. if (nums[i] > ends[m]) {
  19. l = m + 1;
  20. } else {
  21. r = m - 1;
  22. }
  23. }
  24. right = Math.max(right, l);
  25. ends[l] = nums[i];
  26. max = Math.max(max, l + 1);
  27. }
  28. return max;
  29. }