回溯——491. 递增子序列

简介: 本专栏按照数组—链表—哈希—字符串—栈与队列—二叉树—回溯—贪心—动态规划—单调栈的顺序刷题,采用代码随想录所给的刷题顺序,一个正确的刷题顺序对算法学习是非常重要的,希望对大家有帮助

1 题目描述

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

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

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/increasing-subsequences

2 题目示例

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

3 题目提示

1 <= nums.length <= 15
-100 <= nums[i] <= 100

4 思路

可以用递归的方法实现二进制枚举,枚举出所有的子序列,然后判断是否合法。,我们可以得到这样的代码:

List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> temp = new ArrayList<Integer>();
public void dfs(int cur, int[] nums) {
    if (cur == nums.length) {
        // 判断是否合法,如果合法判断是否重复,将满足条件的加入答案
        if (isValid() && notVisited()) {
            ans.add(new ArrayList<Integer>(temp));
        }
        return;
    }
    // 如果选择当前元素
    temp.add(nums[cur]);
    dfs(cur + 1, nums);
    temp.remove(temp.size() - 1);
    // 如果不选择当前元素
    dfs(cur + 1, nums);
}

这是一个递归枚举子序列的通用模板,即用一个临时数组temp来保存当前选出的子序列,使用cur来表示当前位置的下标,在dfs(cur,nums)开始之前,[0, cur ―1]这个区间内的所有元素都已经被考虑过,而[cur, n]这个区间内的元素还未被考虑。在执行dfs(cur,nums)时,我们考虑cur这个位置选或者不选,如果选择当前元素,那么把当前元素加入到temp中,然后递归下一个位置,在递归结束后,应当把temp 的最后一个元素删除进行回溯;如果不选当前的元素,直接递归下一个位置。当然,如果我们简单地这样枚举,对于每一个子序列,我们还需要做一次O(n)的合法性检查和哈希判重复,在执行整个程序的过程中,我们还需要使用一个空间代价O(2")的哈希表来维护已经出现的子序列的哈希值。我们可以对选择和不选择做一些简单的限定,就可以让枚举出来的都是合法的并且不重复:

  • 使序列合法的办法非常简单,即给「选择」做一个限定条件,只有当前的元素大于等于上一个选择的元素的时候才能选择这个元素,这样枚举出来的所有元素都是合法的
  • 那如何保证没有重复呢?我们需要给「不选择」做一个限定条件,只有当当前的元素不等于上一个选择的元素的时候,才考虑不选择当前元素,直接递归后面的元素。因为如果有两个相同的元素,我们会考虑这样四种情况:

    1. 前者被选择,后者被选择
    2. 前者被选择,后者不被选择
    3. 前者不被选择,后者被选择
    4. 前者不被选择,后者不被选择

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

复杂度分析

  • 时间复杂度:o(2^n^*n)。仍然需要对子序列做二进制枚举,枚举出的序列虽然省去了判断合法性和哈希的过程,但是仍然需要O(n)的时间添加到答案中。
  • 空间复杂度:O(n)。这里临时数组的空间代价是O(n),递归使用的栈空间的空间代价也是O(n)。

5 我的答案

class Solution {
    List<Integer> temp = new ArrayList<Integer>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    public List<List<Integer>> findSubsequences(int[] nums) {
        dfs(0, Integer.MIN_VALUE, nums);
        return ans;
    }

    public void dfs(int cur, int last, int[] nums) {
        if (cur == nums.length) {
            if (temp.size() >= 2) {
                ans.add(new ArrayList<Integer>(temp));
            }
            return;
        }
        if (nums[cur] >= last) {
            temp.add(nums[cur]);
            dfs(cur + 1, nums[cur], nums);
            temp.remove(temp.size() - 1);
        }
        if (nums[cur] != last) {
            dfs(cur + 1, last, nums);
        }
    }
}
相关文章
|
4月前
|
Kubernetes 算法 测试技术
【贪心】【回溯】【字符串】2014. 重复 K 次的最长子序列
【贪心】【回溯】【字符串】2014. 重复 K 次的最长子序列
|
4月前
|
测试技术 Perl
【动态规划】【记忆化搜索】【回文】1312让字符串成为回文串的最少插入次数
【动态规划】【记忆化搜索】【回文】1312让字符串成为回文串的最少插入次数
|
4月前
|
算法 测试技术 C#
【字符串】【贪心】【 树状数组】2193. 得到回文串的最少操作次数
【字符串】【贪心】【 树状数组】2193. 得到回文串的最少操作次数
|
算法 JavaScript Go
【动态规划】最长递增子序列
【动态规划】最长递增子序列
|
4月前
|
算法
【面试算法——动态规划 19】最长回文子序列&& (hard)让字符串成为回文串的最少插入次数
【面试算法——动态规划 19】最长回文子序列&& (hard)让字符串成为回文串的最少插入次数
|
9月前
|
算法 程序员 C#
C++二分查找算法的应用:最长递增子序列
C++二分查找算法的应用:最长递增子序列
|
4月前
|
算法
回溯-求出数组的所有子序列【学习算法】
回溯-求出数组的所有子序列【学习算法】
45 0
|
4月前
|
算法 程序员
【算法训练-动态规划 二】【线性DP问题】连续子数组的最大和、乘积最大子数组、最长递增子序列
【算法训练-动态规划 二】【线性DP问题】连续子数组的最大和、乘积最大子数组、最长递增子序列
88 0
|
11月前
|
算法
【学会动态规划】 最长递增子序列(26)
【学会动态规划】 最长递增子序列(26)
33 0
|
11月前
|
算法
【学会动态规划】最长递增子序列的个数(28)
【学会动态规划】最长递增子序列的个数(28)
36 0