解1:最长递增子序列长度:动态规划

题目来源:[NC]163. 最长上升子序列(一)

输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

👉定义 🍗【数组数字】递增子序列问题合集 - 图1 以下标为 🍗【数组数字】递增子序列问题合集 - 图2 结尾的最长上升子序列的长度,注意 🍗【数组数字】递增子序列问题合集 - 图3 必须被选取

从小到大计算 🍗【数组数字】递增子序列问题合集 - 图4 数组的值,在计算 🍗【数组数字】递增子序列问题合集 - 图5 之前,我们已经计算出 🍗【数组数字】递增子序列问题合集 - 图6 的值

状态转移方程:

🍗【数组数字】递增子序列问题合集 - 图7

最后,整个数组中的最长上升子序列即所有 🍗【数组数字】递增子序列问题合集 - 图8 中的最大值。

🍗【数组数字】递增子序列问题合集 - 图9

复杂度分析

时间复杂度:🍗【数组数字】递增子序列问题合集 - 图10,其中 🍗【数组数字】递增子序列问题合集 - 图11 为数组 🍗【数组数字】递增子序列问题合集 - 图12 的长度。

动态规划的状态数为 🍗【数组数字】递增子序列问题合集 - 图13,计算状态 🍗【数组数字】递增子序列问题合集 - 图14 时,需要 🍗【数组数字】递增子序列问题合集 - 图15 的时间遍历 🍗【数组数字】递增子序列问题合集 - 图16 的所有状态

空间复杂度:🍗【数组数字】递增子序列问题合集 - 图17,需要额外使用长度为 🍗【数组数字】递增子序列问题合集 - 图18🍗【数组数字】递增子序列问题合集 - 图19数组。

我的代码

  1. public class Solution {
  2. //状态转移方程:nums[i]>nums[j]&&dp[i]=max{dp[i],dp[j]+1}
  3. public static int lengthofLIS(int[] nums){
  4. int n=nums.length;
  5. int[]dp=new int[n];
  6. int res=0;
  7. for (int i = 0; i < n; i++) {
  8. dp[i]=1;
  9. for (int j = 0; j <i; j++) {
  10. if(nums[i]>nums[j])
  11. dp[i]=Math.max(dp[i],dp[j]+1);
  12. }
  13. res=Math.max(res,dp[i]);
  14. }
  15. return res;
  16. }
  17. }

解1:最长递增子序列长度:贪心+二分

题目来源:[NC]164. 最长上升子序列(二)
贪心:)要使上升子序列尽可能的长,则我们希望每次在上升子序列最后加上的那个数尽可能的小。基于上面的贪心思路,我们维护一个数组 🍗【数组数字】递增子序列问题合集 - 图20

👉定义 🍗【数组数字】递增子序列问题合集 - 图21 存放长度为 🍗【数组数字】递增子序列问题合集 - 图22 的最长上升子序列的末尾元素的最小值,🍗【数组数字】递增子序列问题合集 - 图23 记录目前最长上升子序列的长度

同时我们可以注意到 🍗【数组数字】递增子序列问题合集 - 图24 是关于 🍗【数组数字】递增子序列问题合集 - 图25 单调递增的,因此可以通过二分查找来快速插入

🚩最后整个算法流程为:从前往后遍历数组 🍗【数组数字】递增子序列问题合集 - 图26,在遍历到 🍗【数组数字】递增子序列问题合集 - 图27 时:

  1. - **如果 **![](https://cdn.nlark.com/yuque/__latex/bae144974d1e58789940fce952383a1b.svg#card=math&code=%5Ctext%7Bnums%5Bi%5D%3Ed%5Blen%5D%7D&id=koKfW) ,则直接加入到数组末尾 ![](https://cdn.nlark.com/yuque/__latex/f9bf48b77802229997aa08c9f81e5f04.svg#card=math&code=%5Ctext%7Bd%5Blen%5D%3Dnums%5Bi%5D%7D&id=pIZzS),并更新 ![](https://cdn.nlark.com/yuque/__latex/a05de6e9ce52d289050a7ee2a01d2f03.svg#card=math&code=%5Ctext%7Blen%3Dlen%2B1%7D&id=EExk0);
  2. - **否则** ![](https://cdn.nlark.com/yuque/__latex/56c1b0cb7a48ccf9520b0adb3c8cb2e8.svg#card=math&code=d&id=jybAT) 数组中二分查找,找到第一个比 ![](https://cdn.nlark.com/yuque/__latex/941f962048e38c7dbad3168b7e788144.svg#card=math&code=%5Ctext%7Bnums%5Bi%5D%7D&id=zfKWL) 小的数 ![](https://cdn.nlark.com/yuque/__latex/9a132990ed493d425360a06ed811d4f2.svg#card=math&code=%5Ctext%7Bd%5Bk%5D%7D&id=ly69G) ,并更新 ![](https://cdn.nlark.com/yuque/__latex/e23a2144806342c8a633f93f199d1921.svg#card=math&code=%5Ctext%7Bd%5Bk%2B1%5D%3Dnums%5Bi%5D%7D&id=llKgC)。

🍗重点:以输入序列 [0, 8, 4, 12, 2] 为例: 第一步插入 0, d = [0]; 第二步插入 8, d = [0, 8]; 第三步插入 4, d = [0, 4]; 第四步插入 12,d = [0, 4, 12]; 第五步插入 2 , d = [0, 2, 12]。

解释:为什么可以直接找到第一个比 🍗【数组数字】递增子序列问题合集 - 图28 小的数 🍗【数组数字】递增子序列问题合集 - 图29 ,并更新 🍗【数组数字】递增子序列问题合集 - 图30 因为从前往后遍历数组 🍗【数组数字】递增子序列问题合集 - 图31🍗【数组数字】递增子序列问题合集 - 图32一定比 🍗【数组数字】递增子序列问题合集 - 图33 中数字后出现,只要比它大就可以构成递增序列

复杂度分析

时间复杂度:🍗【数组数字】递增子序列问题合集 - 图34,其中 🍗【数组数字】递增子序列问题合集 - 图35 为数组 🍗【数组数字】递增子序列问题合集 - 图36 的长度。

  - 遍历数组个数为 ![](https://cdn.nlark.com/yuque/__latex/58e9ef90f745f513253970379e805547.svg#card=math&code=%20n&height=12&id=BLC2H),每次二分查找需要 ![](https://cdn.nlark.com/yuque/__latex/65c54415e21b8b592c65dc4cd43cbd33.svg#card=math&code=O%28log_2n%29&height=20&id=uuJj8) 的时间

空间复杂度:🍗【数组数字】递增子序列问题合集 - 图37,需要额外使用长度为 🍗【数组数字】递增子序列问题合集 - 图38🍗【数组数字】递增子序列问题合集 - 图39 数组。

我的代码

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = 1, n = nums.length;
        if (n == 0) return 0;
        int[] d = new int[n + 1];
        d[len] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > d[len]) {
                d[++len] = nums[i];
            } else {
                // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
                int l = 1, r = len, pos = 0; 
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (d[mid] < nums[i]) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                d[pos + 1] = nums[i];
            }
        }
        return len;
    }
}

解2:最长递增子序列的具体序列:贪心+二分

题目来源:[NC]91. 最长上升子序列(三)

给定数组 🍗【数组数字】递增子序列问题合集 - 图40 ,设长度为 🍗【数组数字】递增子序列问题合集 - 图41 ,输出 🍗【数组数字】递增子序列问题合集 - 图42 的最长上升子序列。

(若有多个答案,输出其中 按数值(注:区别于按单个字符的ASCII码值)进行比较的字典序最小 的那个)

要求:空间复杂度 🍗【数组数字】递增子序列问题合集 - 图43,时间复杂度 🍗【数组数字】递增子序列问题合集 - 图44

示例1:

输入:[2,1,5,3,6,4,8,9,7] 返回值:[1,3,4,8,9]

示例2:

输入:[1,2,8,6,4] 返回值:[1,2,4] 说明: 其最长递增子序列有3个,(1,2,8)、(1,2,6)、(1,2,4)其中第三个 按数值进行比较的字典序 最小,故答案为(1,2,4)

我们知道求解最长递增子序列问题有两种:

  1. 动态规划
  2. 贪心+二分

由于本题空间和时间复杂度的要求,故而使用贪心+二分,解题分为两步走:

  1. 第一步——求最长递增子序列长度

    求解出来的 🍗【数组数字】递增子序列问题合集 - 图45 其实就是以当前 🍗【数组数字】递增子序列问题合集 - 图46 结尾的递增子序列的最大长度

  2. 第二步——求字典序靠前的子序列

    假设原始数组是 🍗【数组数字】递增子序列问题合集 - 图47,得到的 🍗【数组数字】递增子序列问题合集 - 图48🍗【数组数字】递增子序列问题合集 - 图49,最终输出结果为 🍗【数组数字】递增子序列问题合集 - 图50(字典序最小的最长递增子序列),🍗【数组数字】递增子序列问题合集 - 图51 的最后一个元素在 🍗【数组数字】递增子序列问题合集 - 图52中位置无庸置疑是 🍗【数组数字】递增子序列问题合集 - 图53 对应的下标,那么到底是 🍗【数组数字】递增子序列问题合集 - 图54 还是 🍗【数组数字】递增子序列问题合集 - 图55 呢?如果是 🍗【数组数字】递增子序列问题合集 - 图56 ,那么 🍗【数组数字】递增子序列问题合集 - 图57 ,则 🍗【数组数字】递增子序列问题合集 - 图58,与已知条件相悖。因此我们应该取 🍗【数组数字】递增子序列问题合集 - 图59 放在 🍗【数组数字】递增子序列问题合集 - 图60 的最后一个位置。也就是说相同的 🍗【数组数字】递增子序列问题合集 - 图61 靠后的才是字典序小的 。

👉定义 🍗【数组数字】递增子序列问题合集 - 图62 存放长度为 🍗【数组数字】递增子序列问题合集 - 图63 的最长上升子序列的末尾元素的最小值,🍗【数组数字】递增子序列问题合集 - 图64 记录目前最长上升子序列的长度
👉定义 🍗【数组数字】递增子序列问题合集 - 图65 存放以下标 🍗【数组数字】递增子序列问题合集 - 图66 为结尾的递增子序列的最大长度

复杂度分析

时间复杂度:🍗【数组数字】递增子序列问题合集 - 图67,其中 🍗【数组数字】递增子序列问题合集 - 图68 为数组 🍗【数组数字】递增子序列问题合集 - 图69 的长度。

  - 遍历数组个数为 ![](https://cdn.nlark.com/yuque/__latex/cb1b63cf5927b4cb6be5f66d95f69dfa.svg#card=math&code=%5Csmall%20n&height=12&id=PN8N3),每次二分查找需要 ![](https://cdn.nlark.com/yuque/__latex/fb0ac29fef83dda4fe02f82f8fa1c256.svg#card=math&code=%5Csmall%20O%28log_2n%29&height=20&id=mmpHp) 的时间

空间复杂度:🍗【数组数字】递增子序列问题合集 - 图70

  - 需要额外使用长度为 ![](https://cdn.nlark.com/yuque/__latex/cb1b63cf5927b4cb6be5f66d95f69dfa.svg#card=math&code=%5Csmall%20n&height=12&id=yaA50) 的 ![](https://cdn.nlark.com/yuque/__latex/8fe6a9644d8d85a2bd797cb98d047a15.svg#card=math&code=%5Csmall%20d%0A&height=18&id=X8huM) 数组,以及长度为 ![](https://cdn.nlark.com/yuque/__latex/f5a8e923f8cd24b56b3bab32358cc58a.svg#card=math&code=len%0A&id=xUNMF)** **的  ![](https://cdn.nlark.com/yuque/__latex/4b3a8d50ebaeb3b7e1c5c0cba720fa1c.svg#card=math&code=%5Csmall%20dp%0A&height=18&id=bKl8m) 数组。

我的代码


public class Solution {

    //状态转移方程:nums[i]>nums[j]&&dp[i]=max{dp[i],dp[j]+1}    
    public int[] LIS (int[] nums) {
        //1. 初始化
        int len = 1, n = nums.length;
        if (n == 0) return null;
        int[] d = new int[n + 1];
        int[]dp=new int[nums.length];
        Arrays.fill(dp,1);
        d[len] = nums[0];
        //2. 依次遍历查找元素
        for (int i = 1; i < n; ++i) {
            //3. 当前元素能够续接上
            if (nums[i] > d[len]) {
                d[++len] = nums[i];
                dp[i]=len;
            } else {
                // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
                int l = 1, r = len, pos = 0; 
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (d[mid] < nums[i]) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                d[pos + 1] = nums[i];
                dp[i]=pos+1;
            }
        }
        //new出新的答案数组
        int[]ans=new int[len];
        for(int i=dp.length-1;i>=0;i--){
            if(len==dp[i]){
                ans[--len]=nums[i];
            }
        }
        return ans;
    }
}

解3:最长递增子序列的序列个数:动态规划

给定一个未排序的整数数组,找到最长递增子序列的个数。

示例 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。

👉定义 🍗【数组数字】递增子序列问题合集 - 图71 以下标为 🍗【数组数字】递增子序列问题合集 - 图72 结尾的最长上升子序列的长度注意 🍗【数组数字】递增子序列问题合集 - 图73 必须被选取
👉定义 🍗【数组数字】递增子序列问题合集 - 图74 以下标为 🍗【数组数字】递增子序列问题合集 - 图75 结尾的最长上升子序列的个数注意 🍗【数组数字】递增子序列问题合集 - 图76 必须被选取

🍗【数组数字】递增子序列问题合集 - 图77 的最长上升子序列的长度为 🍗【数组数字】递增子序列问题合集 - 图78,那么答案为所有满足 🍗【数组数字】递增子序列问题合集 - 图79🍗【数组数字】递增子序列问题合集 - 图80 所对应的🍗【数组数字】递增子序列问题合集 - 图81 之和。

🔖我们从小到大计算 🍗【数组数字】递增子序列问题合集 - 图82 数组的值,在计算 🍗【数组数字】递增子序列问题合集 - 图83 之前,我们已经计算出 🍗【数组数字】递增子序列问题合集 - 图84 的值
状态转移方程:
🍗【数组数字】递增子序列问题合集 - 图85

🍗【数组数字】递增子序列问题合集 - 图86 时考虑往 🍗【数组数字】递增子序列问题合集 - 图87 中最长的上升子序列后面再加一个 🍗【数组数字】递增子序列问题合集 - 图88

对于 🍗【数组数字】递增子序列问题合集 - 图89,其等于所有满足 🍗【数组数字】递增子序列问题合集 - 图90🍗【数组数字】递增子序列问题合集 - 图91 之和。

复杂度分析

时间复杂度:🍗【数组数字】递增子序列问题合集 - 图92,其中 🍗【数组数字】递增子序列问题合集 - 图93 为数组 🍗【数组数字】递增子序列问题合集 - 图94 的长度 。

空间复杂度:🍗【数组数字】递增子序列问题合集 - 图95

我的代码

public int findNumberOfLIS(int[] nums) {
    //1. 初始化
    int n = nums.length, maxLen = 0, ans = 0;
    int[] dp  = new int[n];
    int[] cnt = new int[n];
    Arrays.fill(dp,1);
    Arrays.fill(cnt,1);
    //2. 状态转移
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < i; ++j) {
            //若 nums[i] > nums[j],查看是否能刷新最长长度 
            if (nums[i] > nums[j]) {
                //若能刷新,首次路径
                if (dp[j] + 1 > dp[i]) {
                    dp[i] = dp[j] + 1;
                    cnt[i] = cnt[j]; // 重置计数
                } else if(dp[j] + 1 == dp[i]){
                    cnt[i] += cnt[j]; // 多条路径,路径数相加
                }
            }
        }
        //3. 中途保存答案
        if (dp[i] > maxLen) {
            maxLen = dp[i];
            ans = cnt[i]; // 重置计数
        } else if (dp[i] == maxLen) {
            ans += cnt[i];
        }
    }
    return ans;
}

解4:所有具体的递增子序列:递归+剪枝

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7] 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

输入:nums = [4,4,3,2,1] 输出:[[4,4]]

我们可以采取最朴素的思路,即枚举出所有的子序列,然后判断当前的子序列是否是非严格递增的。
那么我们可以用什么办法来枚举所有的子序列呢?
对于原序列的每一个数来说,都有两种可能的状态,即被选中或者不被选中。如果我们用 🍗【数组数字】递增子序列问题合集 - 图96 代表被选中,🍗【数组数字】递增子序列问题合集 - 图97 代表不被选中,假设一个序列长度为 🍗【数组数字】递增子序列问题合集 - 图98,那么选出的子序列就对应着下面的八种状态:

[0, 0, 0] [0, 0, 1] [0, 1, 0] [0, 1, 1] [1, 0, 0] [1, 0, 1] [1, 1, 0] [1, 1, 1]

那么我们可以用什么办法来判断所有的重复子序列呢?
我们可以使用 🍗【数组数字】递增子序列问题合集 - 图99 表,🍗【数组数字】递增子序列问题合集 - 图100 编码使用 Rabin-Karp 编码

算法说明:
我们使用递归枚举子序列

  1. 临时数组 🍗【数组数字】递增子序列问题合集 - 图101用来保存当前选出的子序列
  2. 🍗【数组数字】递增子序列问题合集 - 图102 :用来表示当前位置的下标,在执行 🍗【数组数字】递增子序列问题合集 - 图103 时,我们需要考虑 🍗【数组数字】递增子序列问题合集 - 图104 这个位置选或者不选 。

    🍗【数组数字】递增子序列问题合集 - 图105 执行开始前,说明 🍗【数组数字】递增子序列问题合集 - 图106 这个区间内的所有元素都已经被考虑过,而 🍗【数组数字】递增子序列问题合集 - 图107 这个区间内的元素还未被考虑。

  • 如果选择当前元素,那么把当前元素加入到🍗【数组数字】递增子序列问题合集 - 图108 中,然后递归下一个位置,在递归结束后,应当把🍗【数组数字】递增子序列问题合集 - 图109 的最后一个元素删除进行回溯
  • 如果不选当前的元素,直接递归下一个位置。

当然,如果我们简单地这样枚举,对于每一个子序列,我们还需要做一次 🍗【数组数字】递增子序列问题合集 - 图110 的合法性检查和哈希判重复,在执行整个程序的过程中,我们还需要使用一个空间代价 🍗【数组数字】递增子序列问题合集 - 图111 的哈希表来维护已经出现的子序列的哈希值。我们可以对选择和不选择做一些简单的限定,就可以让枚举出来的都是合法的并且不重复:

  • 使序列合法的办法非常简单,即给「选择」做一个限定条件

    只有当当前的元素大于等于上一个选择的元素的时候才能选择这个元素,即 🍗【数组数字】递增子序列问题合集 - 图112,这样枚举出来的所有元素都是合法的递增序列。

  • 使序列不重复的办法非常简单,我们需要给「不选择」做一个限定条件

    只有当当前的元素不等于上一个选择的元素的时候,才考虑不选择当前元素,直接递归后面的元素。因为如果有两个相同的元素,我们会考虑这样四种情况:

    1. 前者被选择,后者被选择
    2. 前者被选择,后者不被选择 (舍弃)
    3. 前者不被选择,后者被选择
    4. 前者不被选择,后者不被选择

    其中第二种情况和第三种情况其实是等价的,我们这样限制之后,舍弃了第二种,保留了第三种,于是达到了去重的目的。

复杂度分析

时间复杂度:🍗【数组数字】递增子序列问题合集 - 图113,其中 🍗【数组数字】递增子序列问题合集 - 图114 为数组 🍗【数组数字】递增子序列问题合集 - 图115 的长度 。

  - 仍然需要对子序列做二进制枚举,枚举出的序列虽然省去了判断合法性和哈希的过程,但是仍然需要 ![](https://cdn.nlark.com/yuque/__latex/7ba55e7c64a9405a0b39a1107e90ca94.svg#card=math&code=O%28n%29&id=Da0UN) 的时间添加到答案中。

空间复杂度:🍗【数组数字】递增子序列问题合集 - 图116

  - 临时数组使用的空间代价是 ![](https://cdn.nlark.com/yuque/__latex/7ba55e7c64a9405a0b39a1107e90ca94.svg#card=math&code=O%28n%29&height=20&id=DaVRk),递归使用的栈空间的空间代价也是 ![](https://cdn.nlark.com/yuque/__latex/7ba55e7c64a9405a0b39a1107e90ca94.svg#card=math&code=O%28n%29&height=20&id=qEpMK)。

我的代码

class Solution {
    //1. 临时数组
    List<Integer> temp = new ArrayList<Integer>();
    //2. 结果集
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    //3. 从0开始执行递归
    public List<List<Integer>> findSubsequences(int[] nums) {
        dfs(0, Integer.MIN_VALUE, nums);
        return ans;
    }

    public void dfs(int cur, int pre, int[] nums) {
        //如果到达序列末尾,并且长度大于2合法,加入答案
        if (cur == nums.length) {
            if (temp.size() >= 2) {
                ans.add(new ArrayList<Integer>(temp));
            }
            return;
        }
        //未到达末尾,当前元素大于等于上一个元素,可以加入属于合法元素
        if (nums[cur] >= pre) {
            temp.add(nums[cur]);            //【选择】临时数组加入当前元素
            dfs(cur + 1, nums[cur], nums);    //递归下一个元素,pre变成当前元素
            temp.remove(temp.size() - 1);   //【不选择】移除当前元素
        }
        if (nums[cur] != pre) {                //如果当前元素和上一个元素不重复
            dfs(cur + 1, pre, nums);        //直接递归下一个元素
        }
    }
}