Similar to the subset problem, but full of pitfalls

# 491. Non-decreasing Subsequences

LeetCode problem link (opens new window)

Given an integer array, your task is to find all the non-decreasing subsequences of the array. The length of these subsequences must be at least 2.

Example:

  • Input: [4, 6, 7, 7]
  • Output: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

Note:

  • The given array length will not exceed 15.
  • The range of integers in the array is [-100, 100].
  • The given array might contain duplicate numbers, and equal numbers should be regarded as a non-decreasing sequence.

# Thought Process

This problem of finding non-decreasing subsequences is somewhat like taking ordered subsets. Moreover, the problem also requires that there be no duplicate non-decreasing subsequences.

This is very similar to 0090.Subsets II (opens new window) which we discussed earlier.

But precisely because it is so similar, we need to pay attention to the differences, otherwise we might fall into traps!

In 0090.Subsets II (opens new window), we achieved de-duplication through sorting and a marker array.

However, in this problem, we cannot sort the original array to find non-decreasing subsequences because a sorted array would itself be non-decreasing.

So we cannot use the de-duplication logic from before!

The example given in this problem is a sorted array [4, 6, 7, 7], which might mislead people into thinking that the same approach should be used.

For a clear contrast, I'll use the array [4, 7, 6, 7] to illustrate, which can be abstracted into a tree structure as follows:

491. Non-decreasing Subsequences 1

# Backtracking in Three Steps

  • Parameters for the recursive function

The problem is about finding subsequences, and obviously, an element cannot be reused, so we need startIndex to adjust the starting position for the next layer of recursion.

The code is as follows:

vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex)
1
2
3
  • Termination condition

This problem is actually similar to the subset problem, which also needs to traverse the tree to find each node. Thus, similar to 0078.Subsets (opens new window), we can omit a termination condition since startIndex is incremented every time and won't recurse infinitely.

However, this problem has a different requirement for collecting results, as the problem requires non-decreasing subsequences of at least size 2. The code is as follows:

if (path.size() > 1) {
    result.push_back(path);
    // Note not to add 'return' here, as we need to get all nodes on the tree.
}
1
2
3
4
  • Logic for single-layer search

491. Non-decreasing Subsequences 1

In the figure, it's evident that the same elements at the same level under the same parent node should not be reused.

The single-layer search code is as follows:

unordered_set<int> uset; // Use a set to de-duplicate elements at this level
for (int i = startIndex; i < nums.size(); i++) {
    if ((!path.empty() && nums[i] < path.back())
            || uset.find(nums[i]) != uset.end()) {
            continue;
    }
    uset.insert(nums[i]); // Record that this element has been used at this level
    path.push_back(nums[i]);
    backtracking(nums, i + 1);
    path.pop_back();
}
1
2
3
4
5
6
7
8
9
10
11

Those accustomed to writing backtrack algorithms might feel uncomfortable not seeing a corresponding pop operation below uset.insert(nums[i]); in the recursive function.

It's important to note that unordered_set<int> uset; captures repetition at this level only. Each new level redefines (clears) uset, meaning uset only has a scope for that level!

Here is the complete C++ code:

// Version 1
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) {
            result.push_back(path);
            // Note not to add 'return', as we need to get all nodes on the tree
        }
        unordered_set<int> uset; // Use set to de-duplicate elements at this level
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back())
                    || uset.find(nums[i]) != uset.end()) {
                    continue;
            }
            uset.insert(nums[i]); // Record that this element has been used at this level
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  • Time Complexity: O(n * 2^n)
  • Space Complexity: O(n)

# Optimization

In the above code, I used unordered_set<int> to track whether elements at the same level were used.

We can significantly improve efficiency by using an array as a hash map.

Note that the problem stated the number range is [-100,100], so we can use an array for hashing.

When the program runs and unordered_set frequently calls insert, unordered_set needs to hash (map the key to a unique hash value through a hash function), which is relatively time-consuming. Also, redefining a set each time, while inserting causes the underlying symbol table to expand accordingly, making it inefficient.

Here's the optimized code:

// Version 2
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        if (path.size() > 1) {
            result.push_back(path);
        }
        int used[201] = {0}; // We use an array to handle de-duplication, given the [-100, 100] range
        for (int i = startIndex; i < nums.size(); i++) {
            if ((!path.empty() && nums[i] < path.back())
                    || used[nums[i] + 100] == 1) {
                    continue;
            }
            used[nums[i] + 100] = 1; // Record that this element has been used at this level
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

This version performs much better on LeetCode than version one.

As mentioned in the Hash Table Summary (opens new window), arrays, sets, and maps can all serve as hash tables. If the value range is small, it's best to use arrays.

# Summary

While many solutions tag this as depth-first search, I lean towards saying it is tackled using backtracking. In this problem, I have fully utilized backtracking logic.

I believe you see echoes of the 0090.Subsets II (opens new window) solution throughout this problem, yet every step has a set of challenges.

For those getting too familiar with preset patterns or templates, this problem offers an excellent wake-up call, more importantly, it broadens your thought process!

# Other Language Versions

# Java

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums, 0);
        return result;
    }
    private void backTracking(int[] nums, int startIndex){
        if(path.size() >= 2)
                result.add(new ArrayList<>(path));
        HashSet<Integer> hs = new HashSet<>();
        for(int i = startIndex; i < nums.length; i++){
            if(!path.isEmpty() && path.get(path.size() -1 ) > nums[i] || hs.contains(nums[i]))
                continue;
            hs.add(nums[i]);
            path.add(nums[i]);
            backTracking(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
    private List<Integer> path = new ArrayList<>();
    private List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backtracking(nums,0);
        return res;
    }

    private void backtracking (int[] nums, int start) {
        if (path.size() > 1) {
            res.add(new ArrayList<>(path));
        }

        int[] used = new int[201];
        for (int i = start; i < nums.length; i++) {
            if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
                    (used[nums[i] + 100] == 1)) continue;
            used[nums[i] + 100] = 1;
            path.add(nums[i]);
            backtracking(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Method 2: Using map
class Solution {
    // Result set
    List<List<Integer>> res = new ArrayList<>();
    // Path set
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        getSubsequences(nums,0);
        return res;
    }
    private void getSubsequences( int[] nums, int start ) {
        if(path.size()>1 ){
            res.add( new ArrayList<>(path) );
            // Note here: don't add return, we need all nodes in the tree
        }
        HashMap<Integer,Integer> map = new HashMap<>();
        for(int i=start ;i < nums.length ;i++){
            if(!path.isEmpty() && nums[i]< path.getLast()){
                continue;
            }
            // Current number already used
            if ( map.getOrDefault( nums[i],0 ) >=1 ){
                continue;
            }
            map.put(nums[i],map.getOrDefault( nums[i],0 )+1);
            path.add( nums[i] );
            getSubsequences( nums,i+1 );
            path.removeLast();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# Python

Backtracking with set de-duplication

class Solution:
    def findSubsequences(self, nums):
        result = []
        path = []
        self.backtracking(nums, 0, path, result)
        return result
    
    def backtracking(self, nums, startIndex, path, result):
        if len(path) > 1:
            result.append(path[:])  # Use slices for a copy of the current path to append to result set
            # Note don't add return here, as we need to collect all nodes on the tree
        
        uset = set()  # Use a set to de-duplicate elements at this level
        for i in range(startIndex, len(nums)):
            if (path and nums[i] < path[-1]) or nums[i] in uset:
                continue
            
            uset.add(nums[i])  # Record this element as used at this level
            path.append(nums[i])
            self.backtracking(nums, i + 1, path, result)
            path.pop()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Backtracking with hash table de-duplication

class Solution:
    def findSubsequences(self, nums):
        result = []
        path = []
        self.backtracking(nums, 0, path, result)
        return result

    def backtracking(self, nums, startIndex, path, result):
        if len(path) > 1:
            result.append(path[:])  # Use slices for a copy of the current path to append to result set
        
        used = [0] * 201  # Use an array for de-duplication, based on the problem's number range [-100, 100]
        for i in range(startIndex, len(nums)):
            if (path and nums[i] < path[-1]) or used[nums[i] + 100] == 1:
                continue  # If the current element is smaller than the last one in the path, or already used, skip it
            
            used[nums[i] + 100] = 1  # Mark the current element as used
            path.append(nums[i])  # Append the current element into the current non-decreasing subsequence
            self.backtracking(nums, i + 1, path, result)
            path.pop()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Go

var (
    res [][]int
    path  []int
)
func findSubsequences(nums []int) [][]int {
    res, path = make([][]int, 0), make([]int, 0, len(nums))
    dfs(nums, 0)
    return res
}
func dfs(nums []int, start int) {
    if len(path) >= 2 {
        tmp := make([]int, len(path))
        copy(tmp, path)
        res = append(res, tmp)
    }
    used := make(map[int]bool, len(nums))   // Initialize a used map for level de-duplication
    for i := start; i < len(nums); i++ {
        if used[nums[i]] {   // De-duplication
            continue
        }
        if len(path) == 0 || nums[i] >= path[len(path)-1] {
            path = append(path, nums[i])
            used[nums[i]] = true
            dfs(nums, i+1)
            path = path[:len(path)-1]
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# JavaScript

var findSubsequences = function(nums) {
    let result = []
    let path = []
    function backtracing(startIndex) {
        if(path.length > 1) {
            result.push(path.slice())
        }
        let uset = []
        for(let i = startIndex; i < nums.length; i++) {
            if((path.length > 0 && nums[i] < path[path.length - 1]) || uset[nums[i] + 100]) {
                continue
            }
            uset[nums[i] + 100] = true
            path.push(nums[i])
            backtracing(i + 1)
            path.pop()
        }
    }
    backtracing(0)
    return result
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# TypeScript

function findSubsequences(nums: number[]): number[][] {
    const resArr: number[][] = [];
    backTracking(nums, 0, []);
    return resArr;
    function backTracking(nums: number[], startIndex: number, route: number[]): void {
        let length: number = nums.length;
        if (route.length >= 2) {
            resArr.push(route.slice());
        }
        const usedSet: Set<number> = new Set();
        for (let i = startIndex; i < length; i++) {
            if (
                nums[i] < route[route.length - 1] ||
                usedSet.has(nums[i])
            ) continue;
            usedSet.add(nums[i]);
            route.push(nums[i]);
            backTracking(nums, i + 1, route);
            route.pop();
        }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# Rust

Backtracking with hash

use std::collections::HashSet;
impl Solution {
    fn backtracking(result: &mut Vec<Vec<i32>>, path: &mut Vec<i32>, nums: &Vec<i32>, start_index: usize) {
        if path.len() > 1 { result.push(path.clone()); }
        let len = nums.len();
        let mut uset: HashSet<i32> = HashSet::new();
        for i in start_index..len {
            if (!path.is_empty() && nums[i] < *path.last().unwrap()) || uset.contains(&nums[i]) { continue; }
            uset.insert(nums[i]);
            path.push(nums[i]);
            Solution::backtracking(result, path, nums, i + 1);
            path.pop();
        }
    }

    pub fn find_subsequences(nums: Vec<i32>) -> Vec<Vec<i32>> {
        let mut result: Vec<Vec<i32>> = Vec::new();
        let mut path: Vec<i32> = Vec::new();
        Solution::backtracking(&mut result, &mut path, &nums, 0);
        result
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Backtracking with array

impl Solution {
    fn backtracking(result: &mut Vec<Vec<i32>>, path: &mut Vec<i32>, nums: &Vec<i32>, start_index: usize) {
        if path.len() > 1 { result.push(path.clone()); }
        let len = nums.len();
        let mut used = [0; 201];
        for i in start_index..len {
            if (!path.is_empty() && nums[i] < *path.last().unwrap()) || used[(nums[i] + 100) as usize] == 1 { continue; }
            used[(nums[i] + 100) as usize] = 1;
            path.push(nums[i]);
            Solution::backtracking(result, path, nums, i + 1);
            path.pop();
        }
    }

    pub fn find_subsequences(nums: Vec<i32>) -> Vec<Vec<i32>> {
        let mut result: Vec<Vec<i32>> = Vec::new();
        let mut path: Vec<i32> = Vec::new();
        Solution::backtracking(&mut result, &mut path, &nums, 0);
        result
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# C

int* path;
int pathTop;
int** ans;
int ansTop;
int* length;
// Copies the current path content into ans
void copy() {
    int* tempPath = (int*)malloc(sizeof(int) * pathTop);
    memcpy(tempPath, path, pathTop * sizeof(int));
    length[ansTop] = pathTop;
    ans[ansTop++] = tempPath;
}

// Searches if a value exists in uset
int find(int* uset, int usetSize, int key) {
    int i;
    for(i = 0; i < usetSize; i++) {
        if(uset[i] == key)
            return 1;
    }
    return 0;
}

void backTracking(int* nums, int numsSize, int startIndex) {
    // Add path to ans if more than one element is present
    if(pathTop > 1) {
        copy();
    }
    int* uset = (int*)malloc(sizeof(int) * numsSize);
    int usetTop = 0;
    int i;
    for(i = startIndex; i < numsSize; i++) {
        // Skip if current element is smaller than last one in path or already used in the same tree layer
        if((pathTop > 0 && nums[i] < path[pathTop - 1]) || find(uset, usetTop, nums[i]))
            continue;
        // Add current element to uset
        uset[usetTop++] = nums[i];
        // Add current element to path
        path[pathTop++] = nums[i];
        backTracking(nums, numsSize, i + 1);
        // Backtrack
        pathTop--;
    }
}

int** findSubsequences(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
    // Initialize auxiliary arrays
    path = (int*)malloc(sizeof(int) * numsSize);
    ans = (int**)malloc(sizeof(int*) * 33000);
    length = (int*)malloc(sizeof(int*) * 33000);
    pathTop = ansTop = 0;

    backTracking(nums, numsSize, 0);

    // Set the number of elements to return and each 1D array length
    *returnSize = ansTop;
    *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop);
    int i;
    for(i = 0; i < ansTop; i++) {
        (*returnColumnSizes)[i] = length[i];
    }
    return ans;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# Swift

func findSubsequences(_ nums: [Int]) -> [[Int]] {
    var result = [[Int]]()
    var path = [Int]()
    func backtracking(startIndex: Int) {
        // Collect results but do not return, as further concatenation is needed
        if path.count > 1 {
            result.append(path)
        }

        var uset = Set<Int>()
        let end = nums.count
        guard startIndex < end else { return } // Termination condition
        for i in startIndex ..< end {
            let num = nums[i]
            if uset.contains(num) { continue } // Skip duplicate elements
            if !path.isEmpty, num < path.last! { continue } // Ensure increment
            uset.insert(num) // Record using set
            path.append(num) // Process: collect elements
            backtracking(startIndex: i + 1) // Avoid duplicating visits of elements
            path.removeLast() // Backtrack
        }
    }
    backtracking(startIndex: 0)
    return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# Scala

object Solution {
  import scala.collection.mutable
  def findSubsequences(nums: Array[Int]): List[List[Int]] = {
    var result = mutable.ListBuffer[List[Int]]()
    var path = mutable.ListBuffer[Int]()

    def backtracking(startIndex: Int): Unit = {
      // If more than one element, add to result set
      if (path.size > 1) {
        result.append(path.toList)
      }

      var used = new Array[Boolean](201)
      // In the current loop, only unvisited elements can enter backtracking
      for (i <- startIndex until nums.size if !used(nums(i) + 100)) {
        // If the path has no elements or the current iteration element is greater than the last in the path, it can backtrack
        if (path.size == 0 || (!path.isEmpty && nums(i) >= path(path.size - 1))) {
          used(nums(i) + 100) = true
          path.append(nums(i))
          backtracking(i + 1)
          path.remove(path.size - 1)
        }
      }
    }

    backtracking(0)
    result.toList
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# C#

public class Solution {
    public IList<IList<int>> res = new List<IList<int>>();
    public IList<int> path = new List<int>();
    public IList<IList<int>> FindSubsequences(int[] nums) {
        BackTracking(nums, 0);
        return res;
    }
    public void BackTracking(int[] nums, int start){
        if(path.Count >= 2){
            res.Add(new List<int>(path));
        }
        HashSet<int> hs = new HashSet<int>();
        for(int i = start; i < nums.Length; i++){
            if(path.Count > 0 && path[path.Count - 1] > nums[i] || hs.Contains(nums[i])){
                continue;
            }
            hs.Add(nums[i]);
            path.Add(nums[i]);
            BackTracking(nums, i + 1);
            path.RemoveAt(path.Count - 1);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Last updated:: 9/4/2025, 3:19:38 PM
Copyright © 2025 keetcoder