Facebook Pixel

491. Non-decreasing Subsequences

MediumBit ManipulationArrayHash TableBacktracking
Leetcode Link

Problem Description

You are given an integer array nums. Your task is to find all possible subsequences that satisfy two conditions:

  1. The subsequence must be non-decreasing (each element is greater than or equal to the previous one)
  2. The subsequence must contain at least two elements

A subsequence is formed by deleting some (possibly zero) elements from the original array without changing the relative order of the remaining elements. For example, [1,3] is a subsequence of [1,2,3].

The key challenge is that you need to return all different possible non-decreasing subsequences, meaning no duplicates should appear in your result. The subsequences can be returned in any order.

For example:

  • If nums = [4,6,7,7], the valid non-decreasing subsequences with at least 2 elements would include: [4,6], [4,6,7], [4,6,7,7], [4,7], [4,7,7], [6,7], [6,7,7], and [7,7].

The solution uses a depth-first search (DFS) approach with backtracking. The algorithm explores each position in the array and makes two choices at each step:

  1. Include the current element if it's greater than or equal to the last selected element
  2. Skip the current element (but only if it's different from the last value to avoid duplicates)

The variable last tracks the value of the previously selected element (initialized to -1000 to handle all valid integer inputs), and t maintains the current subsequence being built. When the end of the array is reached and the subsequence has at least 2 elements, it's added to the answer list.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: The problem involves finding subsequences in an array, not traversing nodes and edges in a graph structure.

Need to solve for kth smallest/largest?

  • No: We need to find all valid subsequences, not the kth smallest or largest element.

Involves Linked Lists?

  • No: The problem works with an integer array, not linked list nodes.

Does the problem have small constraints?

  • Yes: Finding all possible subsequences is inherently exponential (2^n possibilities), which typically means the problem has small constraints to be computationally feasible. Each element can either be included or excluded, leading to exploring all combinations.

Brute force / Backtracking?

  • Yes: We need to explore all possible subsequences systematically. Backtracking is perfect here because:
    • We build subsequences incrementally
    • We need to explore multiple paths (include or skip each element)
    • We can prune invalid paths (when elements would break the non-decreasing property)
    • We need to track and backtrack our choices to explore all possibilities

Conclusion: The flowchart correctly leads us to use a Backtracking approach. This aligns perfectly with the solution, which uses DFS with backtracking to explore all valid non-decreasing subsequences by making decisions at each index (include if valid, or skip) and backtracking to explore alternative paths.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to find all possible subsequences with certain properties, we're essentially exploring a decision tree. At each position in the array, we face a choice: include this element or skip it. This naturally leads to a backtracking approach.

Think of building subsequences like making a series of choices. Starting from the first element, we can either:

  1. Take it (if it maintains the non-decreasing property)
  2. Leave it

After making a choice, we move to the next element and repeat. This creates a tree of decisions where each path from root to leaf represents a different subsequence.

The key insight for avoiding duplicates is subtle but important. Consider [4, 6, 7, 7]. If we're at index 2 (first 7) and our current subsequence is [4, 6], we can:

  • Add 7 to get [4, 6, 7] and continue exploring
  • Skip this 7 and move to the next position

But when we reach index 3 (second 7), if we already skipped a 7 at index 2, we shouldn't explore skipping again - it would generate the same subsequences we already found. This is why the code checks if nums[u] != last before recursing without adding the current element.

The last parameter serves dual purpose:

  1. It ensures we maintain the non-decreasing property (only add elements >= last)
  2. It helps avoid duplicate subsequences by preventing redundant "skip" operations for equal consecutive values

By systematically exploring all valid paths in this decision tree and collecting subsequences with at least 2 elements, we guarantee finding all unique non-decreasing subsequences.

Solution Approach

The solution implements a depth-first search (DFS) with backtracking to explore all possible subsequences. Here's how the implementation works:

Core DFS Function Structure:

The dfs(u, last, t) function takes three parameters:

  • u: Current index in the array we're considering
  • last: The value of the last element added to our subsequence (or -1000 initially)
  • t: The current subsequence being built

Base Case: When u == len(nums), we've considered all elements. If our subsequence t has at least 2 elements, we add a copy to our answer list.

Recursive Cases:

At each position, we make up to two recursive calls:

  1. Include current element (if valid):

    if nums[u] >= last:
        t.append(nums[u])
        dfs(u + 1, nums[u], t)
        t.pop()  # backtrack

    We can only include nums[u] if it maintains the non-decreasing property (nums[u] >= last). After exploring this path, we backtrack by removing the element.

  2. Skip current element (with duplicate avoidance):

    if nums[u] != last:
        dfs(u + 1, last, t)

    We skip the current element, but only if it's different from last. This crucial check prevents generating duplicate subsequences when we have repeated values.

Why the duplicate check works:

Consider when nums[u] == last. This means we just added a value equal to nums[u] in the previous step. If we skip nums[u] now, we'd be exploring subsequences that don't include this value - but we already explored those paths when we decided not to include the previous occurrence. So we avoid this redundant exploration.

Data Structures:

  • ans: List to store all valid subsequences
  • t: Temporary list used during DFS to build subsequences (modified and restored via backtracking)

Initial Call: dfs(0, -1000, []) starts at index 0, with -1000 as the initial last value (chosen to be smaller than any valid array element), and an empty subsequence.

The algorithm has time complexity of O(2^n * n) in the worst case (where we might generate up to 2^n subsequences, each taking O(n) to copy), and space complexity of O(n) for the recursion stack and temporary subsequence storage.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with nums = [4, 6, 7, 7].

We start with dfs(0, -1000, []) - at index 0, last value is -1000, empty subsequence.

At index 0 (value = 4):

  • Since 4 >= -1000, we can include 4: Add 4 to subsequence → [4], recurse with dfs(1, 4, [4])

    At index 1 (value = 6), subsequence = [4]:

    • Since 6 >= 4, include 6: [4,6]dfs(2, 6, [4,6])

      At index 2 (value = 7), subsequence = [4,6]:

      • Since 7 >= 6, include 7: [4,6,7]dfs(3, 7, [4,6,7])

        At index 3 (value = 7), subsequence = [4,6,7]:

        • Since 7 >= 7, include 7: [4,6,7,7]dfs(4, 7, [4,6,7,7])
          • Reached end, len([4,6,7,7]) >= 2, add [4,6,7,7] to answer ✓
        • Since 7 == 7 (nums[3] == last), DON'T skip (would be duplicate)
      • Since 7 != 6, skip 7: keep [4,6]dfs(3, 6, [4,6])

        At index 3 (value = 7), subsequence = [4,6]:

        • Since 7 >= 6, include 7: [4,6,7]dfs(4, 7, [4,6,7])
          • Reached end, add [4,6,7] to answer ✓
        • Since 7 != 6, skip 7: [4,6]dfs(4, 6, [4,6])
          • Reached end, add [4,6] to answer ✓
    • Since 6 != 4, skip 6: keep [4]dfs(2, 4, [4])

      At index 2 (value = 7), subsequence = [4]:

      • Since 7 >= 4, include 7: [4,7]dfs(3, 7, [4,7])

        At index 3 (value = 7), subsequence = [4,7]:

        • Since 7 >= 7, include 7: [4,7,7]dfs(4, 7, [4,7,7])
          • Reached end, add [4,7,7] to answer ✓
        • Since 7 == 7, DON'T skip
      • Since 7 != 4, skip 7: [4]dfs(3, 4, [4])

        At index 3 (value = 7), subsequence = [4]:

        • Since 7 >= 4, include 7: [4,7]dfs(4, 7, [4,7])
          • Reached end, add [4,7] to answer ✓
  • Since 4 != -1000, skip 4: empty subsequence → dfs(1, -1000, [])

    This branch explores subsequences not starting with 4, eventually finding [6,7], [6,7,7], and [7,7].

The key insight: when we have last = 7 and encounter another 7, we don't explore skipping it because we already explored "subsequences without this 7" when we skipped the previous 7.

Final answer: [[4,6,7,7], [4,6,7], [4,6], [4,7,7], [4,7], [6,7,7], [6,7], [7,7]]

Solution Implementation

1class Solution:
2    def findSubsequences(self, nums: List[int]) -> List[List[int]]:
3        """
4        Find all non-decreasing subsequences with at least 2 elements.
5        Uses DFS with backtracking to explore all possibilities.
6        """
7      
8        def dfs(index: int, last_value: int, current_path: List[int]) -> None:
9            """
10            Recursive DFS to build subsequences.
11          
12            Args:
13                index: Current position in nums array
14                last_value: The last value added to current subsequence
15                current_path: Current subsequence being built
16            """
17            # Base case: reached end of array
18            if index == len(nums):
19                # Add valid subsequence (length > 1) to result
20                if len(current_path) > 1:
21                    result.append(current_path[:])  # Make a copy
22                return
23          
24            # Choice 1: Include current element if it maintains non-decreasing order
25            if nums[index] >= last_value:
26                current_path.append(nums[index])
27                dfs(index + 1, nums[index], current_path)
28                current_path.pop()  # Backtrack
29          
30            # Choice 2: Skip current element (avoid duplicates at same recursion level)
31            # Only skip if current element is different from last_value
32            # This prevents duplicate subsequences
33            if nums[index] != last_value:
34                dfs(index + 1, last_value, current_path)
35      
36        # Initialize result list
37        result = []
38      
39        # Start DFS with initial values
40        # -1000 as initial last_value since -100 <= nums[i] <= 100
41        dfs(0, -1000, [])
42      
43        return result
44
1class Solution {
2    private int[] inputArray;
3    private List<List<Integer>> result;
4
5    /**
6     * Finds all non-decreasing subsequences with at least 2 elements
7     * @param nums The input array
8     * @return List of all valid non-decreasing subsequences
9     */
10    public List<List<Integer>> findSubsequences(int[] nums) {
11        this.inputArray = nums;
12        this.result = new ArrayList<>();
13      
14        // Start DFS from index 0 with minimum possible value as last element
15        // -1000 is used as initial value since it's smaller than the constraint range
16        dfs(0, -1000, new ArrayList<>());
17      
18        return result;
19    }
20
21    /**
22     * Depth-first search to generate subsequences
23     * @param currentIndex Current position in the input array
24     * @param lastValue Last value added to the current subsequence
25     * @param currentSubsequence The subsequence being built
26     */
27    private void dfs(int currentIndex, int lastValue, List<Integer> currentSubsequence) {
28        // Base case: reached end of array
29        if (currentIndex == inputArray.length) {
30            // Only add subsequences with at least 2 elements
31            if (currentSubsequence.size() > 1) {
32                result.add(new ArrayList<>(currentSubsequence));
33            }
34            return;
35        }
36      
37        // Option 1: Include current element if it maintains non-decreasing order
38        if (inputArray[currentIndex] >= lastValue) {
39            // Add current element to subsequence
40            currentSubsequence.add(inputArray[currentIndex]);
41          
42            // Recursively explore with this element included
43            dfs(currentIndex + 1, inputArray[currentIndex], currentSubsequence);
44          
45            // Backtrack: remove the element for other explorations
46            currentSubsequence.remove(currentSubsequence.size() - 1);
47        }
48      
49        // Option 2: Skip current element to avoid duplicates
50        // Only skip if current element is different from last selected value
51        // This prevents generating duplicate subsequences
52        if (inputArray[currentIndex] != lastValue) {
53            dfs(currentIndex + 1, lastValue, currentSubsequence);
54        }
55    }
56}
57
1class Solution {
2public:
3    vector<vector<int>> findSubsequences(vector<int>& nums) {
4        vector<vector<int>> result;
5        vector<int> currentSequence;
6      
7        // Start DFS with initial index 0 and minimum possible value as last element
8        dfs(0, INT_MIN, currentSequence, nums, result);
9      
10        return result;
11    }
12
13private:
14    void dfs(int currentIndex, int lastValue, vector<int>& currentSequence, 
15             vector<int>& nums, vector<vector<int>>& result) {
16        // Base case: reached end of array
17        if (currentIndex == nums.size()) {
18            // Only add sequences with at least 2 elements
19            if (currentSequence.size() >= 2) {
20                result.push_back(currentSequence);
21            }
22            return;
23        }
24      
25        // Option 1: Include current element if it maintains non-decreasing order
26        if (nums[currentIndex] >= lastValue) {
27            currentSequence.push_back(nums[currentIndex]);
28            dfs(currentIndex + 1, nums[currentIndex], currentSequence, nums, result);
29            currentSequence.pop_back(); // Backtrack
30        }
31      
32        // Option 2: Skip current element (avoid duplicates at same recursion level)
33        // Only skip if current element is different from last selected value
34        // This prevents generating duplicate subsequences
35        if (nums[currentIndex] != lastValue) {
36            dfs(currentIndex + 1, lastValue, currentSequence, nums, result);
37        }
38    }
39};
40
1function findSubsequences(nums: number[]): number[][] {
2    const result: number[][] = [];
3    const currentSequence: number[] = [];
4  
5    // Start DFS with initial index 0 and minimum possible value as last element
6    dfs(0, Number.MIN_SAFE_INTEGER, currentSequence, nums, result);
7  
8    return result;
9}
10
11function dfs(
12    currentIndex: number, 
13    lastValue: number, 
14    currentSequence: number[], 
15    nums: number[], 
16    result: number[][]
17): void {
18    // Base case: reached end of array
19    if (currentIndex === nums.length) {
20        // Only add sequences with at least 2 elements
21        if (currentSequence.length >= 2) {
22            result.push([...currentSequence]);
23        }
24        return;
25    }
26  
27    // Option 1: Include current element if it maintains non-decreasing order
28    if (nums[currentIndex] >= lastValue) {
29        currentSequence.push(nums[currentIndex]);
30        dfs(currentIndex + 1, nums[currentIndex], currentSequence, nums, result);
31        currentSequence.pop(); // Backtrack
32    }
33  
34    // Option 2: Skip current element (avoid duplicates at same recursion level)
35    // Only skip if current element is different from last selected value
36    // This prevents generating duplicate subsequences
37    if (nums[currentIndex] !== lastValue) {
38        dfs(currentIndex + 1, lastValue, currentSequence, nums, result);
39    }
40}
41

Time and Space Complexity

Time Complexity: O(2^n * n)

The algorithm explores all possible subsequences using backtracking. At each position u, we have at most 2 choices:

  1. Include nums[u] if it's greater than or equal to the last element
  2. Skip nums[u] if it's different from the last element

This creates a decision tree with depth n (where n is the length of nums), and at each node we make up to 2 recursive calls. This gives us O(2^n) recursive calls in the worst case.

Additionally, when we find a valid subsequence (length > 1), we copy it to the answer list using t[:], which takes O(n) time in the worst case.

Therefore, the overall time complexity is O(2^n * n).

Space Complexity: O(n)

The space complexity consists of:

  1. Recursion call stack: The maximum depth of recursion is n, requiring O(n) space
  2. Temporary list t: This stores the current subsequence being built, which can have at most n elements, requiring O(n) space
  3. Output space: The ans list stores all valid subsequences (not counted in auxiliary space complexity as it's part of the output)

The auxiliary space complexity is O(n) for the recursion stack and temporary storage.

If we include the output space, there can be up to 2^n subsequences, each of maximum length n, giving a total space complexity of O(2^n * n).

Common Pitfalls

Pitfall 1: Incorrect Duplicate Handling

The Problem: A common mistake is trying to use a set to remove duplicates after generating all subsequences, or checking if a subsequence already exists in the result before adding it:

# INCORRECT approach
def findSubsequences(self, nums):
    result = []
  
    def dfs(index, path):
        if index == len(nums):
            if len(path) > 1 and path not in result:  # Inefficient!
                result.append(path[:])
            return
      
        # Include current element
        if not path or nums[index] >= path[-1]:
            path.append(nums[index])
            dfs(index + 1, path)
            path.pop()
      
        # Always skip (no duplicate check)
        dfs(index + 1, path)
  
    dfs(0, [])
    return result

This generates many duplicate subsequences and checking membership with path not in result is O(n*m) for each check, making the solution extremely inefficient.

The Solution: The key insight is preventing duplicates during generation rather than filtering them afterward. The condition if nums[index] != last_value when skipping ensures we don't explore redundant branches:

# CORRECT approach
if nums[index] != last_value:
    dfs(index + 1, last_value, current_path)

Pitfall 2: Using a Set at Each Recursion Level

The Problem: Another approach people try is using a set at each recursion level to track which values have been used:

# INCORRECT - creates too many set objects
def dfs(index, path):
    if index == len(nums):
        if len(path) > 1:
            result.append(path[:])
        return
  
    used = set()  # New set for each recursion level
    for i in range(index, len(nums)):
        if nums[i] in used:
            continue
        if not path or nums[i] >= path[-1]:
            used.add(nums[i])
            path.append(nums[i])
            dfs(i + 1, path)
            path.pop()

While this can work, it's unnecessarily complex and creates many set objects. The elegant solution using last_value comparison achieves the same deduplication with simpler logic.

Pitfall 3: Misunderstanding the Deduplication Logic

The Problem: Some might think the condition should be reversed:

# INCORRECT logic
if nums[index] == last_value:  # Wrong condition!
    dfs(index + 1, last_value, current_path)

This would skip elements when they equal the last value, which is exactly when we DON'T want to skip (because we'd be creating the same subsequence pattern we already explored).

The Solution: Remember the logic: we skip the current element only when nums[index] != last_value. When they're equal, we've just included this value in a previous branch, so skipping it now would recreate subsequences we've already generated when we didn't include the previous occurrence.

Pitfall 4: Incorrect Initial Value for last_value

The Problem: Using an insufficient initial value:

# INCORRECT if array contains -100
dfs(0, -100, [])  # Won't work for nums = [-100, -99]

Since the constraint states -100 <= nums[i] <= 100, using -100 as the initial value would incorrectly prevent -100 from being the first element in a subsequence.

The Solution: Use a value guaranteed to be smaller than any array element:

dfs(0, -1000, [])  # Or float('-inf')
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More