Facebook Pixel

784. Letter Case Permutation

MediumBit ManipulationStringBacktracking
Leetcode Link

Problem Description

You are given a string s that contains letters and possibly other characters. Your task is to generate all possible strings that can be created by changing the case of each letter in the original string.

For each letter in the string, you have two choices:

  • Keep it as lowercase
  • Convert it to uppercase

Non-letter characters (like digits or special characters) remain unchanged.

You need to return a list containing all possible combinations of case transformations. The order of strings in the output list doesn't matter.

For example:

  • If s = "a1b2", the output would be ["a1b2", "a1B2", "A1b2", "A1B2"]
  • If s = "3z4", the output would be ["3z4", "3Z4"]

The solution uses a depth-first search (DFS) approach to explore all possibilities. Starting from the first character, it recursively processes each position in the string. When encountering a letter, the algorithm explores two branches: one keeping the current case and another switching the case using XOR operation with 32 (since the ASCII difference between uppercase and lowercase letters is 32). When reaching the end of the string, each complete path represents one valid permutation that gets added to the result 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 transforming characters in a string, not traversing nodes and edges in a graph structure.

Need to solve for kth smallest/largest?

  • No: We're not looking for a specific kth element; we need to generate all possible combinations.

Involves Linked Lists?

  • No: The problem works with strings/arrays, not linked list data structures.

Does the problem have small constraints?

  • Yes: The problem typically has small constraints since we're generating all possible case permutations. With each letter having 2 choices (uppercase/lowercase), the total number of permutations is at most 2^n where n is the number of letters. This exponential growth means the constraints must be small (usually n ≤ 12-15).

Brute force / Backtracking?

  • Yes: We need to explore all possible combinations of letter cases. This is a classic backtracking scenario where at each letter position, we make a choice (keep current case or switch case), explore that path, and then backtrack to explore the alternative choice.

Conclusion: The flowchart correctly leads us to using a Backtracking approach. This aligns perfectly with the solution, which uses DFS to systematically explore all possible case transformations by making decisions at each letter position and backtracking to explore all branches of the decision tree.

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

Intuition

When we look at this problem, we need to generate all possible variations of a string where each letter can be either uppercase or lowercase. This immediately suggests that we're dealing with a decision tree where at each letter, we branch into two possibilities.

Think of it like walking through the string from left to right. At each position:

  • If it's not a letter (like a digit), we have no choice - just keep it as is
  • If it's a letter, we have exactly 2 choices: keep it in its current case or flip it

This creates a binary decision tree. For example, with string "a1b":

  • At position 0 ('a'): we can choose 'a' or 'A'
  • At position 1 ('1'): no choice, keep '1'
  • At position 2 ('b'): we can choose 'b' or 'B'

The total number of leaf nodes in this tree represents all possible permutations.

The natural way to explore such a decision tree is through backtracking. We start from the first character and recursively explore both branches whenever we encounter a letter. The key insight is that we don't need to create new strings at each step - we can modify the current string in-place and then backtrack by flipping the character back.

The clever trick in the solution is using XOR with 32 to flip between uppercase and lowercase. This works because in ASCII, the difference between a lowercase letter and its uppercase counterpart is exactly 32. So 'a' (97) XOR 32 gives 'A' (65), and 'A' (65) XOR 32 gives back 'a' (97). This makes the backtracking elegant - the same operation that transforms the character also reverses the transformation.

Learn more about Backtracking patterns.

Solution Approach

The solution implements a depth-first search (DFS) with backtracking to generate all possible case permutations. Let's walk through the implementation step by step:

Data Structure Setup:

  • Convert the input string s to a list t for in-place modifications (strings are immutable in Python)
  • Use a result list ans to store all generated permutations

DFS Function Logic: The recursive dfs(i) function processes the string starting from index i:

  1. Base Case: When i >= len(t), we've processed all characters. Join the list t back into a string and add it to ans.

  2. Recursive Exploration:

    • First, always make a recursive call dfs(i + 1) without modifying the current character. This explores the branch where we keep the character as-is.
    • Then check if t[i] is a letter using isalpha():
      • If it's a letter, flip its case using t[i] = chr(ord(t[i]) ^ 32)
      • Make another recursive call dfs(i + 1) to explore the branch with the flipped case
      • The character automatically gets flipped back when the recursive call returns (backtracking happens implicitly)

Case Conversion Technique: The expression chr(ord(t[i]) ^ 32) performs case conversion:

  • ord(t[i]) gets the ASCII value of the character
  • XOR with 32 flips the 6th bit, which differentiates uppercase from lowercase in ASCII
  • chr() converts the result back to a character

Why This Works:

  • For non-letter characters, only one path is explored (the unmodified path)
  • For letters, two paths are explored: original case and flipped case
  • The DFS naturally generates all 2^n combinations where n is the number of letters
  • Since we modify the list in-place and the XOR operation is self-reversing, we don't need explicit backtracking code

Time Complexity: O(2^n × n) where n is the length of the string (worst case when all characters are letters) Space Complexity: O(2^n × n) for storing all the permutations in the result list

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 solution with s = "a1b" to see how the DFS generates all permutations.

Initial Setup:

  • Convert string to list: t = ['a', '1', 'b']
  • Start DFS from index 0

DFS Execution Tree:

Start: dfs(0), t = ['a', '1', 'b']
├─ First branch (keep 'a'): dfs(1)
│  └─ At index 1 ('1' - not a letter): dfs(2)
│     └─ At index 2 ('b' - is a letter)
│        ├─ Keep 'b': dfs(3)
│        │  └─ Base case: Add "a1b" to result
│        └─ Flip to 'B': t = ['a', '1', 'B'], dfs(3)
│           └─ Base case: Add "a1B" to result
│           └─ (Backtrack: 'B' flips back to 'b')
└─ Second branch (flip 'a' to 'A'): t = ['A', '1', 'b'], dfs(1)
   └─ At index 1 ('1' - not a letter): dfs(2)
      └─ At index 2 ('b' - is a letter)
         ├─ Keep 'b': dfs(3)
         │  └─ Base case: Add "A1b" to result
         └─ Flip to 'B': t = ['A', '1', 'B'], dfs(3)
            └─ Base case: Add "A1B" to result

Step-by-step execution:

  1. dfs(0): Character 'a' is a letter

    • Call dfs(1) without modification (exploring 'a')
    • After returning, flip 'a' to 'A' using XOR 32
    • Call dfs(1) again (exploring 'A')
  2. First dfs(1) with t = ['a', '1', 'b']: Character '1' is not a letter

    • Only call dfs(2) without modification
  3. First dfs(2) with t = ['a', '1', 'b']: Character 'b' is a letter

    • Call dfs(3) → base case reached → add "a1b"
    • Flip 'b' to 'B' using XOR 32
    • Call dfs(3) → base case reached → add "a1B"
    • 'B' automatically flips back to 'b' after returning
  4. Second dfs(1) with t = ['A', '1', 'b']: Character '1' is not a letter

    • Only call dfs(2) without modification
  5. Second dfs(2) with t = ['A', '1', 'b']: Character 'b' is a letter

    • Call dfs(3) → base case reached → add "A1b"
    • Flip 'b' to 'B' using XOR 32
    • Call dfs(3) → base case reached → add "A1B"

Final Result: ["a1b", "a1B", "A1b", "A1B"]

The XOR trick works because:

  • 'a' (ASCII 97) XOR 32 = 65 (ASCII for 'A')
  • 'b' (ASCII 98) XOR 32 = 66 (ASCII for 'B')
  • Applying XOR 32 again reverses the operation, providing automatic backtracking

Solution Implementation

1class Solution:
2    def letterCasePermutation(self, s: str) -> List[str]:
3        def backtrack(index: int) -> None:
4            # Base case: if we've processed all characters, add the current permutation to results
5            if index >= len(char_list):
6                result.append("".join(char_list))
7                return
8          
9            # Recursive case 1: keep the current character as is
10            backtrack(index + 1)
11          
12            # Recursive case 2: if the current character is a letter, toggle its case
13            if char_list[index].isalpha():
14                # Toggle case using XOR with 32 (difference between uppercase and lowercase ASCII values)
15                char_list[index] = chr(ord(char_list[index]) ^ 32)
16                backtrack(index + 1)
17                # Backtrack: restore the original character for other branches
18                char_list[index] = chr(ord(char_list[index]) ^ 32)
19      
20        # Convert string to list for in-place modifications
21        char_list = list(s)
22        # Store all letter case permutations
23        result = []
24        # Start the backtracking process from index 0
25        backtrack(0)
26      
27        return result
28
1class Solution {
2    // List to store all permutations
3    private List<String> result = new ArrayList<>();
4    // Character array to manipulate the string
5    private char[] charArray;
6
7    /**
8     * Generates all possible letter case permutations of a string.
9     * For each letter in the string, we can have it in lowercase or uppercase.
10     * Numbers and special characters remain unchanged.
11     * 
12     * @param s The input string
13     * @return List of all possible letter case permutations
14     */
15    public List<String> letterCasePermutation(String s) {
16        // Convert string to char array for easier manipulation
17        charArray = s.toCharArray();
18        // Start DFS from index 0
19        dfs(0);
20        return result;
21    }
22
23    /**
24     * Depth-first search to generate all permutations.
25     * At each position, we have two choices:
26     * 1. Keep the current character as is
27     * 2. If it's a letter, toggle its case
28     * 
29     * @param index Current position in the character array
30     */
31    private void dfs(int index) {
32        // Base case: reached the end of the string
33        if (index >= charArray.length) {
34            // Add current permutation to result list
35            result.add(new String(charArray));
36            return;
37        }
38      
39        // Choice 1: Keep current character as is and move to next position
40        dfs(index + 1);
41      
42        // Choice 2: If current character is a letter, toggle its case
43        if (Character.isLetter(charArray[index])) {
44            // XOR with 32 toggles between uppercase and lowercase
45            // 'A' = 65 (01000001), 'a' = 97 (01100001), difference is 32 (00100000)
46            charArray[index] ^= 32;
47            // Explore with the toggled character
48            dfs(index + 1);
49            // Note: We don't need to backtrack here because each recursive call
50            // will eventually toggle back when it returns
51        }
52    }
53}
54
1class Solution {
2public:
3    vector<string> letterCasePermutation(string s) {
4        string current = s;  // Current string being modified during DFS
5        vector<string> result;  // Store all permutations
6      
7        // Recursive DFS function to generate all letter case permutations
8        auto dfs = [&](this auto&& dfs, int index) -> void {
9            // Base case: reached end of string, add current permutation to result
10            if (index >= current.size()) {
11                result.push_back(current);
12                return;
13            }
14          
15            // Branch 1: Keep current character as is and continue
16            dfs(index + 1);
17          
18            // Branch 2: If character is a letter, toggle its case and continue
19            if (isalpha(current[index])) {
20                // XOR with 32 toggles between uppercase and lowercase
21                // 'A' = 65 (01000001), 'a' = 97 (01100001), difference = 32 (00100000)
22                current[index] ^= 32;
23                dfs(index + 1);
24                // Note: No need to restore since each path creates its own modifications
25            }
26        };
27      
28        // Start DFS from index 0
29        dfs(0);
30        return result;
31    }
32};
33
1/**
2 * Generates all possible letter case permutations of a string.
3 * For each letter in the string, creates versions with both uppercase and lowercase.
4 * Digits and special characters remain unchanged.
5 * 
6 * @param s - The input string to generate permutations for
7 * @returns An array of all possible letter case permutations
8 */
9function letterCasePermutation(s: string): string[] {
10    // Convert string to character array for easier manipulation
11    const characters: string[] = s.split('');
12  
13    // Store all generated permutations
14    const result: string[] = [];
15  
16    /**
17     * Depth-first search to generate all permutations
18     * @param index - Current position in the character array
19     */
20    const generatePermutations = (index: number): void => {
21        // Base case: reached end of string, add current permutation to result
22        if (index >= characters.length) {
23            result.push(characters.join(''));
24            return;
25        }
26      
27        // Branch 1: Keep current character as-is and continue
28        generatePermutations(index + 1);
29      
30        // Branch 2: If current character is a letter (ASCII >= 65), toggle its case
31        // ASCII 65 is 'A', so this checks if character is a letter (not a digit)
32        if (characters[index].charCodeAt(0) >= 65) {
33            // XOR with 32 toggles between uppercase and lowercase
34            // (uppercase ASCII + 32 = lowercase ASCII)
35            characters[index] = String.fromCharCode(characters[index].charCodeAt(0) ^ 32);
36            generatePermutations(index + 1);
37          
38            // Restore original character for backtracking (implicit due to function scope)
39        }
40    };
41  
42    // Start the DFS from index 0
43    generatePermutations(0);
44  
45    return result;
46}
47

Time and Space Complexity

Time Complexity: O(n × 2^n), where n is the length of the string s.

The analysis breaks down as follows:

  • The DFS explores a binary tree where at each letter position, we have 2 choices: keep the original case or toggle it (using XOR with 32)
  • For non-letter characters, we have only 1 choice (continue without modification)
  • In the worst case where all characters are letters, we have 2^n different paths from root to leaf
  • Each complete path generates one permutation, requiring O(n) time to join the characters into a string with "".join(t)
  • Therefore, the total time complexity is O(n × 2^n)

Space Complexity: O(n × 2^n) for the output storage, or O(n) if we only consider auxiliary space.

The space analysis:

  • The ans list stores all 2^k permutations (where k is the number of letters), each of length n, requiring O(n × 2^n) space
  • The recursion stack depth is at most n, contributing O(n) space
  • The list t is a copy of the input string, using O(n) space
  • If we exclude the output storage and only consider auxiliary space, the complexity is O(n) for the recursion stack and temporary list

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

Common Pitfalls

1. Forgetting to Restore State After Backtracking

One of the most common mistakes is not properly restoring the character after exploring the alternate case branch. Without explicit restoration, subsequent iterations would work with corrupted data.

Incorrect Implementation:

if char_list[index].isalpha():
    char_list[index] = chr(ord(char_list[index]) ^ 32)
    backtrack(index + 1)
    # Missing restoration - char_list[index] remains modified!

Correct Implementation:

if char_list[index].isalpha():
    char_list[index] = chr(ord(char_list[index]) ^ 32)
    backtrack(index + 1)
    char_list[index] = chr(ord(char_list[index]) ^ 32)  # Restore original

2. Using String Concatenation Instead of List

Attempting to modify strings directly leads to errors since strings are immutable in Python. Some might try to build strings character by character, which is inefficient.

Incorrect Approach:

def backtrack(index: int, current: str):
    if index >= len(s):
        result.append(current)
        return
  
    # This creates new strings repeatedly - inefficient!
    backtrack(index + 1, current + s[index])
    if s[index].isalpha():
        backtrack(index + 1, current + s[index].swapcase())

Better Approach: Use a mutable list and join only when adding to results, as shown in the original solution.

3. Incorrect Case Toggle Logic

Using methods like upper() or lower() without checking the current case can lead to exploring the same branch twice.

Incorrect Implementation:

if char_list[index].isalpha():
    original = char_list[index]
    char_list[index] = char_list[index].upper()  # Wrong! What if it's already uppercase?
    backtrack(index + 1)
    char_list[index] = original

Correct Implementation: Use XOR with 32 or swapcase() to ensure the case is actually toggled:

if char_list[index].isalpha():
    char_list[index] = char_list[index].swapcase()
    backtrack(index + 1)
    char_list[index] = char_list[index].swapcase()  # Toggle back

4. Order of Recursive Calls

The order matters for consistency. Always explore the "no change" branch first, then the "change" branch. Mixing this order can make debugging difficult and may cause confusion when tracing through the recursion.

Consistent Pattern:

backtrack(index + 1)  # First: explore without change
if char_list[index].isalpha():
    # Modify
    backtrack(index + 1)  # Second: explore with change
    # Restore
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More