Facebook Pixel

3437. Permutations III 🔒

Problem Description

Given an integer n, you need to find all alternating permutations of the first n positive integers (1, 2, 3, ..., n).

An alternating permutation is a permutation where no two adjacent elements have the same parity. In other words, odd and even numbers must alternate throughout the entire sequence - you cannot have two odd numbers next to each other, and you cannot have two even numbers next to each other.

For example, if n = 4, then [1, 2, 3, 4] is an alternating permutation because:

  • 1 (odd) is followed by 2 (even)
  • 2 (even) is followed by 3 (odd)
  • 3 (odd) is followed by 4 (even)

However, [1, 3, 2, 4] would NOT be an alternating permutation because 1 (odd) is followed by 3 (odd), violating the alternating rule.

The task is to return all valid alternating permutations sorted in lexicographical order (dictionary order, where smaller numbers come first when comparing position by position from left to right).

The solution uses backtracking to build valid permutations. It maintains a visited array to track which numbers have been used, and at each position, it only places a number if:

  1. The number hasn't been used yet
  2. The number has different parity from the previous number in the permutation (or it's the first position)

The recursive function explores all valid possibilities and collects the complete alternating permutations.

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 generating permutations of integers, not traversing nodes and edges in a graph structure.

Need to solve for kth smallest/largest?

  • No: We need to find ALL valid alternating permutations, not just the kth smallest or largest one.

Involves Linked Lists?

  • No: The problem works with permutations of integers stored in arrays/lists, not linked list data structures.

Does the problem have small constraints?

  • Yes: Since we're generating all permutations of n integers, the constraints must be small. Permutations grow factorially (n!), which becomes computationally infeasible for large n. The problem asks for all valid permutations, which is only practical with small values of n.

Brute force / Backtracking?

  • Yes: We need to explore all possible arrangements of numbers while ensuring the alternating parity constraint. Backtracking is perfect here because:
    • We build the permutation one position at a time
    • At each step, we try different valid numbers
    • We backtrack when we can't place a valid number or complete a permutation
    • We need to explore ALL valid possibilities, not just find one solution

Conclusion: The flowchart correctly leads us to use a Backtracking approach. This aligns perfectly with the solution, which uses a recursive DFS function to build permutations incrementally, maintaining a visited array to track used numbers, and backtracking when necessary to explore all valid alternating permutations.

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

Intuition

When we need to generate all valid permutations with specific constraints, backtracking becomes our go-to strategy. Think of building the permutation as making a series of choices - at each position, we decide which number to place next.

The key insight here is that we need to enforce the alternating parity constraint dynamically as we build each permutation. We can't simply generate all permutations first and then filter them - that would be inefficient. Instead, we want to prune invalid branches early in our search tree.

Consider how we'd build an alternating permutation manually for n = 4:

  • Start with position 0: We can place any number (1, 2, 3, or 4)
  • Say we choose 1 (odd). For position 1, we can only place even numbers (2 or 4)
  • If we choose 2, then position 2 must be odd (1 or 3, but 1 is already used, so only 3)
  • Finally, position 3 must be even (only 4 remains)

This naturally suggests a recursive approach where:

  1. We maintain a visited array to track which numbers we've already used
  2. At each recursive call, we try placing each unused number that satisfies the parity constraint
  3. The parity constraint is simple: current_number % 2 != previous_number % 2
  4. We recursively build the rest of the permutation after placing each valid number
  5. When we can't place any more numbers (either all positions filled or no valid options), we backtrack

The beauty of backtracking here is that it automatically handles the exploration of all possibilities while respecting our constraints. By checking the parity condition before making a recursive call, we avoid exploring branches that would lead to invalid permutations, making our solution efficient despite the exponential nature of permutations.

The lexicographical ordering comes naturally from trying numbers in ascending order (1 to n) at each position. This ensures we generate permutations in the desired sorted order without needing a separate sorting step.

Learn more about Backtracking patterns.

Solution Approach

The solution implements a depth-first search (DFS) with backtracking to generate all valid alternating permutations. Let's break down the implementation step by step:

Core Function Structure: The main function permute(n) sets up the necessary data structures and calls a helper function dfs(i) which recursively builds the permutations.

Data Structures Used:

  1. ans = []: Stores all valid alternating permutations found
  2. t = []: Temporary list that builds the current permutation being explored
  3. vis = [False] * (n + 1): Boolean array to track which numbers (1 to n) have been used in the current permutation. We use size n+1 to directly index by number (ignoring index 0)

The DFS Function: The dfs(i) function represents filling the i-th position (0-indexed) of the permutation:

  1. Base Case: When i >= n, all positions have been filled. We've found a complete valid permutation, so we add a copy of t to our answer: ans.append(t[:])

  2. Recursive Case: For position i, we try each number from 1 to n:

    for j in range(1, n + 1):
  3. Constraint Checking: Before placing number j at position i, we verify:

    • not vis[j]: The number hasn't been used yet
    • i == 0 or t[-1] % 2 != j % 2: Either this is the first position, OR the parity of j differs from the last number in our current permutation

    The parity check t[-1] % 2 != j % 2 ensures alternating odd/even pattern - if the last number is odd (remainder 1), the next must be even (remainder 0), and vice versa.

  4. Backtracking Pattern: When a valid number j is found:

    t.append(j)        # Place the number
    vis[j] = True      # Mark as used
    dfs(i + 1)         # Recursively fill next position
    vis[j] = False     # Backtrack: mark as unused
    t.pop()            # Backtrack: remove the number

Why This Works:

  • By trying numbers in order (1 to n), we naturally generate permutations in lexicographical order
  • The parity constraint is enforced at each step, preventing invalid branches from being explored
  • The backtracking ensures we explore all possible valid combinations systematically
  • The visited array prevents number reuse within a single permutation

Time Complexity: O(n! × n) in the worst case, as we might generate up to n! permutations and each takes O(n) to build and copy.

Space Complexity: O(n) for the recursion stack and temporary arrays, not counting the output storage.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the solution with n = 3 to see how the backtracking algorithm builds all alternating permutations.

Initial Setup:

  • Numbers available: 1, 2, 3
  • ans = [] (stores final results)
  • t = [] (current permutation being built)
  • vis = [False, False, False, False] (index 0 unused, indices 1-3 track if numbers 1-3 are used)

Building the Permutations:

Branch 1: Start with 1 (odd)

  • Position 0: Place 1 → t = [1], vis[1] = True
  • Position 1: Need even number (2 is the only even available)
    • Place 2 → t = [1, 2], vis[2] = True
    • Position 2: Need odd number (only 3 is available and odd)
      • Place 3 → t = [1, 2, 3], vis[3] = True
      • All positions filled! Add [1, 2, 3] to ans
      • Backtrack: Remove 3, vis[3] = False, t = [1, 2]
    • No more options for position 2
    • Backtrack: Remove 2, vis[2] = False, t = [1]
  • No more even numbers for position 1
  • Backtrack: Remove 1, vis[1] = False, t = []

Branch 2: Start with 2 (even)

  • Position 0: Place 2 → t = [2], vis[2] = True
  • Position 1: Need odd number (1 or 3 available)
    • Try 1 → t = [2, 1], vis[1] = True
      • Position 2: Need even number (only 2, but already used!)
      • No valid options, backtrack: Remove 1, vis[1] = False, t = [2]
    • Try 3 → t = [2, 3], vis[3] = True
      • Position 2: Need even number (only 2, but already used!)
      • No valid options, backtrack: Remove 3, vis[3] = False, t = [2]
  • Backtrack: Remove 2, vis[2] = False, t = []

Branch 3: Start with 3 (odd)

  • Position 0: Place 3 → t = [3], vis[3] = True
  • Position 1: Need even number (2 is the only even available)
    • Place 2 → t = [3, 2], vis[2] = True
    • Position 2: Need odd number (only 1 is available and odd)
      • Place 1 → t = [3, 2, 1], vis[1] = True
      • All positions filled! Add [3, 2, 1] to ans
      • Backtrack: Remove 1, vis[1] = False, t = [3, 2]
    • No more options for position 2
    • Backtrack: Remove 2, vis[2] = False, t = [3]
  • No more even numbers for position 1
  • Backtrack: Remove 3, vis[3] = False, t = []

Final Result: ans = [[1, 2, 3], [3, 2, 1]]

Notice how:

  • Starting with 2 (even) led to dead ends because we only have one even number when n=3
  • The algorithm automatically prunes invalid branches by checking parity before recursing
  • Lexicographical order is maintained by trying numbers 1, 2, 3 in that order at each position

Solution Implementation

1class Solution:
2    def permute(self, n: int) -> List[List[int]]:
3        """
4        Generate all permutations of numbers 1 to n where adjacent elements
5        have different parity (alternating odd/even or even/odd).
6      
7        Args:
8            n: The upper bound of numbers to permute (1 to n)
9      
10        Returns:
11            List of all valid permutations
12        """
13        def backtrack(position: int) -> None:
14            """
15            Recursively build permutations using backtracking.
16          
17            Args:
18                position: Current position being filled in the permutation
19            """
20            # Base case: if we've filled all positions, add current permutation to results
21            if position >= n:
22                result.append(current_permutation[:])  # Make a copy of the current permutation
23                return
24          
25            # Try placing each number from 1 to n at the current position
26            for number in range(1, n + 1):
27                # Check if number is available and satisfies parity constraint
28                # For first position (position == 0), no parity check needed
29                # For other positions, check that current number has different parity from previous
30                if not used[number] and (position == 0 or current_permutation[-1] % 2 != number % 2):
31                    # Place the number
32                    current_permutation.append(number)
33                    used[number] = True
34                  
35                    # Recursively fill the next position
36                    backtrack(position + 1)
37                  
38                    # Backtrack: remove the number and mark as unused
39                    used[number] = False
40                    current_permutation.pop()
41      
42        # Initialize data structures
43        result = []  # Store all valid permutations
44        current_permutation = []  # Build permutation incrementally
45        used = [False] * (n + 1)  # Track which numbers have been used (index 0 unused)
46      
47        # Start the backtracking process from position 0
48        backtrack(0)
49      
50        return result
51
1class Solution {
2    // List to store all valid permutations
3    private List<int[]> permutations = new ArrayList<>();
4    // Track which numbers have been used in current permutation
5    private boolean[] used;
6    // Current permutation being built
7    private int[] currentPermutation;
8    // Size of permutation
9    private int size;
10
11    /**
12     * Generate all permutations of numbers 1 to n where adjacent elements have different parity
13     * @param n the size of permutation (using numbers 1 to n)
14     * @return 2D array containing all valid permutations
15     */
16    public int[][] permute(int n) {
17        this.size = n;
18        currentPermutation = new int[n];
19        used = new boolean[n + 1]; // Index 0 unused, indices 1 to n for numbers 1 to n
20      
21        // Start building permutations from position 0
22        backtrack(0);
23      
24        // Convert list to 2D array
25        return permutations.toArray(new int[0][]);
26    }
27
28    /**
29     * Recursively build permutations using backtracking
30     * @param position current position in the permutation being filled
31     */
32    private void backtrack(int position) {
33        // Base case: complete permutation found
34        if (position >= size) {
35            // Add a copy of current permutation to results
36            permutations.add(currentPermutation.clone());
37            return;
38        }
39      
40        // Try placing each number from 1 to n at current position
41        for (int number = 1; number <= size; ++number) {
42            // Check if number is available and satisfies parity constraint
43            if (!used[number] && isValidPlacement(position, number)) {
44                // Mark number as used
45                used[number] = true;
46                // Place number at current position
47                currentPermutation[position] = number;
48              
49                // Recursively fill next position
50                backtrack(position + 1);
51              
52                // Backtrack: mark number as unused for other branches
53                used[number] = false;
54            }
55        }
56    }
57  
58    /**
59     * Check if placing a number at given position maintains the parity constraint
60     * @param position current position in permutation
61     * @param number the number to place
62     * @return true if placement is valid
63     */
64    private boolean isValidPlacement(int position, int number) {
65        // First position has no constraint
66        if (position == 0) {
67            return true;
68        }
69        // Adjacent elements must have different parity (one odd, one even)
70        int previousNumber = currentPermutation[position - 1];
71        return previousNumber % 2 != number % 2;
72    }
73}
74
1class Solution {
2public:
3    vector<vector<int>> permute(int n) {
4        vector<vector<int>> result;                    // Store all valid permutations
5        vector<bool> isUsed(n + 1, false);            // Track which numbers are already used (1-indexed)
6        vector<int> currentPermutation;                // Build current permutation
7      
8        // Recursive DFS function to generate permutations
9        function<void(int)> generatePermutations = [&](int position) -> void {
10            // Base case: completed a full permutation
11            if (position >= n) {
12                result.push_back(currentPermutation);
13                return;
14            }
15          
16            // Try each number from 1 to n
17            for (int num = 1; num <= n; ++num) {
18                // Check if number is available and satisfies parity constraint
19                bool isValidChoice = !isUsed[num] && 
20                    (position == 0 || currentPermutation[position - 1] % 2 != num % 2);
21              
22                if (isValidChoice) {
23                    // Make choice: use this number
24                    isUsed[num] = true;
25                    currentPermutation.push_back(num);
26                  
27                    // Explore further positions
28                    generatePermutations(position + 1);
29                  
30                    // Backtrack: undo the choice
31                    currentPermutation.pop_back();
32                    isUsed[num] = false;
33                }
34            }
35        };
36      
37        // Start generating from position 0
38        generatePermutations(0);
39        return result;
40    }
41};
42
1/**
2 * Generates all permutations of numbers from 1 to n where adjacent elements have different parity
3 * (one is odd and one is even)
4 * @param n - The upper bound of numbers to permute (1 to n)
5 * @returns An array of all valid permutations
6 */
7function permute(n: number): number[][] {
8    // Store all valid permutations
9    const result: number[][] = [];
10  
11    // Track which numbers (1 to n) have been used in current permutation
12    // Index 0 is unused, indices 1 to n correspond to numbers 1 to n
13    const isVisited: boolean[] = Array(n + 1).fill(false);
14  
15    // Current permutation being built
16    const currentPermutation: number[] = Array(n).fill(0);
17  
18    /**
19     * Depth-first search to build permutations recursively
20     * @param position - Current position in the permutation being filled
21     */
22    const dfs = (position: number): void => {
23        // Base case: complete permutation found
24        if (position >= n) {
25            // Add a copy of the current permutation to results
26            result.push([...currentPermutation]);
27            return;
28        }
29      
30        // Try each number from 1 to n
31        for (let num = 1; num <= n; num++) {
32            // Check if number is available and satisfies parity constraint
33            const isAvailable = !isVisited[num];
34            const satisfiesParityConstraint = position === 0 || 
35                currentPermutation[position - 1] % 2 !== num % 2;
36          
37            if (isAvailable && satisfiesParityConstraint) {
38                // Mark number as used
39                isVisited[num] = true;
40                currentPermutation[position] = num;
41              
42                // Recursively fill next position
43                dfs(position + 1);
44              
45                // Backtrack: mark number as unused for other branches
46                isVisited[num] = false;
47            }
48        }
49    };
50  
51    // Start building permutations from position 0
52    dfs(0);
53  
54    return result;
55}
56

Time and Space Complexity

Time Complexity: O(n × n!)

The algorithm uses backtracking to generate permutations with a specific constraint (alternating parity). In the worst case:

  • The DFS explores a tree where each level represents choosing one number from 1 to n
  • At each position, we iterate through all n numbers to find valid candidates
  • The number of valid permutations can be up to n! in certain cases
  • For each valid permutation found, we create a copy of the current path t[:] which takes O(n) time
  • Therefore, the total time complexity is O(n × n!) where the n factor comes from both the iteration at each level and the copying operation

Space Complexity: O(n)

The space complexity consists of:

  • The recursion stack depth which can go up to n levels: O(n)
  • The temporary array t storing the current permutation path: O(n)
  • The visited array vis of size n+1: O(n)
  • The output array ans stores all valid permutations, but this is typically not counted as auxiliary space since it's the required output

The dominant space usage comes from the recursion stack and auxiliary arrays, giving us O(n) space complexity.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Create a Copy When Adding to Results

The Pitfall: A very common mistake is adding the reference to the current permutation list directly to the results:

if position >= n:
    result.append(current_permutation)  # WRONG! This adds a reference

This causes all elements in result to point to the same list object. Since backtracking modifies current_permutation in place, all stored "permutations" will end up being empty lists after the algorithm completes.

The Solution: Always create a copy when storing the permutation:

if position >= n:
    result.append(current_permutation[:])  # Correct: creates a new list
    # Alternative: result.append(list(current_permutation))

2. Incorrect Parity Check Logic

The Pitfall: Writing the parity check incorrectly, such as:

# WRONG: This checks if both are odd or both are even
if not used[number] and (position == 0 or current_permutation[-1] % 2 == number % 2):

Or using bitwise operations incorrectly:

# WRONG: & has lower precedence than !=
if not used[number] and (position == 0 or current_permutation[-1] & 1 != number & 1):

The Solution: Ensure the parity check verifies that adjacent numbers have different remainders when divided by 2:

# Correct: ensures different parity
if not used[number] and (position == 0 or current_permutation[-1] % 2 != number % 2):
# Alternative with proper parentheses for bitwise:
if not used[number] and (position == 0 or (current_permutation[-1] & 1) != (number & 1)):

3. Off-by-One Error in the Visited Array

The Pitfall: Creating a visited array of size n instead of n+1:

used = [False] * n  # WRONG: can't directly index by number

This leads to index errors when trying to access used[n] or requires adjusting indices throughout the code (using used[number-1]), which is error-prone.

The Solution: Create the array with size n+1 to allow direct indexing by number:

used = [False] * (n + 1)  # Correct: index 0 unused, indices 1 to n for numbers

4. Not Properly Resetting State During Backtracking

The Pitfall: Forgetting to reset the used array or remove the number from the current permutation:

current_permutation.append(number)
used[number] = True
backtrack(position + 1)
# WRONG: Missing backtracking steps

This causes numbers to remain marked as used even after backtracking, preventing valid permutations from being found.

The Solution: Always include both backtracking operations after the recursive call:

current_permutation.append(number)
used[number] = True
backtrack(position + 1)
used[number] = False  # Reset the used flag
current_permutation.pop()  # Remove the number

5. Inefficient Base Case Check

The Pitfall: Using len(current_permutation) == n instead of position >= n:

if len(current_permutation) == n:  # Works but less efficient
    result.append(current_permutation[:])

While this works, it requires computing the length at each recursive call, which is O(n) in some implementations.

The Solution: Use the position parameter which is already being tracked:

if position >= n:  # More efficient
    result.append(current_permutation[:])
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

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

Load More