Facebook Pixel

2930. Number of Strings Which Can Be Rearranged to Contain Substring

Problem Description

You need to find how many strings of length n can be rearranged to contain "leet" as a substring.

A string is considered "good" if:

  1. It contains only lowercase English characters (a-z)
  2. Its characters can be rearranged to form a string that has "leet" as a substring

For a string to be rearrangeable into containing "leet", it must have:

  • At least 1 letter 'l'
  • At least 2 letters 'e'
  • At least 1 letter 't'

The problem asks you to count all possible strings of length n that meet these criteria. Since we're dealing with arrangements, the order of characters in the original string doesn't matter - what matters is having the minimum required frequency of each character.

For example:

  • "lteer" is good because it has 1 'l', 2 'e's, 1 't', and 1 'r'. These can be rearranged to "leetr" which contains "leet"
  • "letl" is not good because it only has 1 'e', but we need at least 2 'e's to form "leet"

The solution uses dynamic programming with memoization to track:

  • i: remaining positions to fill in the string
  • l: whether we have at least 1 'l' (capped at 1)
  • e: how many 'e's we have (capped at 2, since we need exactly 2)
  • t: whether we have at least 1 't' (capped at 1)

At each position, we can either:

  • Add one of 23 other lowercase letters (not l, e, or t)
  • Add an 'l' and update our 'l' count
  • Add an 'e' and update our 'e' count
  • Add a 't' and update our 't' count

The answer should be returned modulo 10^9 + 7 to handle large numbers.

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

Intuition

The key insight is that we don't care about the exact arrangement of characters in our string - we only care about having enough of each required character to form "leet" somewhere in a rearrangement.

Think about it this way: if we have a string with at least 1 'l', 2 'e's, and 1 't', we can always rearrange it to place "leet" together, then put any remaining characters before or after. So the problem reduces to counting strings with the right character frequencies.

We can build the string character by character. At each position, we have 26 choices (any lowercase letter). As we build, we need to track whether we've satisfied our requirements:

  • Have we added at least 1 'l'?
  • Have we added at least 2 'e's?
  • Have we added at least 1 't'?

This naturally leads to a recursive solution where our state is (remaining_positions, l_count, e_count, t_count). But tracking exact counts would be inefficient - we don't care if we have 3 'e's or 10 'e's, as long as we have at least 2. So we cap our tracking:

  • l is either 0 (need more) or 1 (have enough)
  • e can be 0, 1, or 2 (need exactly 2)
  • t is either 0 (need more) or 1 (have enough)

At each step with i positions remaining, we can:

  1. Add any of the 23 "other" letters (not l, e, or t) - this doesn't change our requirement tracking
  2. Add an 'l' - this might change l from 0 to 1
  3. Add an 'e' - this might increment e up to 2
  4. Add a 't' - this might change t from 0 to 1

The base case is when i = 0 (string is complete). We return 1 if we've met all requirements (l = 1, e = 2, t = 1), otherwise 0.

Since we'll revisit the same states multiple times (e.g., reaching state (5, 1, 2, 0) through different paths), we use memoization to avoid redundant calculations.

Learn more about Math, Dynamic Programming and Combinatorics patterns.

Solution Approach

The solution implements a top-down dynamic programming approach with memoization using Python's @cache decorator.

State Definition:

  • dfs(i, l, e, t) represents the number of valid strings when:
    • i: remaining positions to fill
    • l: count of 'l' characters (capped at 1)
    • e: count of 'e' characters (capped at 2)
    • t: count of 't' characters (capped at 1)

Base Case: When i = 0, the string is complete. We check if we have exactly the required characters:

if i == 0:
    return int(l == 1 and e == 2 and t == 1)

This returns 1 if all requirements are met, 0 otherwise.

Recursive Transitions: For each remaining position, we have 4 choices:

  1. Add one of 23 other characters (not 'l', 'e', or 't'):

    a = dfs(i - 1, l, e, t) * 23 % mod

    Since there are 26 lowercase letters total and we exclude 'l', 'e', 't', we have 23 choices. The counts remain unchanged.

  2. Add character 'l':

    b = dfs(i - 1, min(1, l + 1), e, t)

    We use min(1, l + 1) to cap the count at 1 since we only need to track whether we have at least one 'l'.

  3. Add character 'e':

    c = dfs(i - 1, l, min(2, e + 1), t)

    We use min(2, e + 1) to cap at 2 since we need exactly 2 'e's for "leet".

  4. Add character 't':

    d = dfs(i - 1, l, e, min(1, t + 1))

    Similar to 'l', we cap at 1.

Final Calculation: The total number of ways is the sum of all four choices:

return (a + b + c + d) % mod

Memoization: The @cache decorator automatically memoizes the function results, preventing redundant calculations for the same state. This is crucial since many different paths can lead to the same state.

Time Complexity: O(n × 2 × 3 × 2) = O(n) since we have at most n values for i, 2 for l, 3 for e, and 2 for t.

Space Complexity: O(n) for the memoization cache and recursion stack.

The answer is obtained by calling dfs(n, 0, 0, 0), starting with an empty string (0 of each required character) and n positions to fill.

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 a small example with n = 5 to illustrate how the solution works.

We start with dfs(5, 0, 0, 0) - we have 5 positions to fill and haven't added any required characters yet.

Step 1: From state (5, 0, 0, 0) We have 4 choices for the first character:

  • Add one of 23 other letters → leads to dfs(4, 0, 0, 0) × 23
  • Add 'l' → leads to dfs(4, 1, 0, 0)
  • Add 'e' → leads to dfs(4, 0, 1, 0)
  • Add 't' → leads to dfs(4, 0, 0, 1)

Let's follow one path: adding 'l' first.

Step 2: From state (4, 1, 0, 0) Now we have 'l' covered (l=1). Our choices:

  • Add one of 23 other letters → dfs(3, 1, 0, 0) × 23
  • Add another 'l' → dfs(3, 1, 0, 0) (l stays at 1, capped)
  • Add 'e' → dfs(3, 1, 1, 0)
  • Add 't' → dfs(3, 1, 0, 1)

Let's add 'e' next.

Step 3: From state (3, 1, 1, 0) We have 1 'l' and 1 'e'. Our choices:

  • Add one of 23 other letters → dfs(2, 1, 1, 0) × 23
  • Add 'l' → dfs(2, 1, 1, 0) (already have enough 'l')
  • Add 'e' → dfs(2, 1, 2, 0) (e becomes 2)
  • Add 't' → dfs(2, 1, 1, 1)

Let's add another 'e'.

Step 4: From state (2, 1, 2, 0) We have 1 'l' and 2 'e's. We still need a 't'. Our choices:

  • Add one of 23 other letters → dfs(1, 1, 2, 0) × 23
  • Add 'l' → dfs(1, 1, 2, 0)
  • Add 'e' → dfs(1, 1, 2, 0) (e stays at 2, capped)
  • Add 't' → dfs(1, 1, 2, 1) ✓ This completes our requirements!

Let's add 't'.

Step 5: From state (1, 1, 2, 1) We have all required characters! With 1 position left:

  • Add any of 23 other letters → dfs(0, 1, 2, 1) × 23 = 1 × 23 = 23
  • Add 'l' → dfs(0, 1, 2, 1) = 1
  • Add 'e' → dfs(0, 1, 2, 1) = 1
  • Add 't' → dfs(0, 1, 2, 1) = 1

Total for this state: 23 + 1 + 1 + 1 = 26

Base Case Example: When we reach dfs(0, 1, 2, 1):

  • i = 0 (string is complete)
  • l = 1 ✓ (have at least 1 'l')
  • e = 2 ✓ (have exactly 2 'e's)
  • t = 1 ✓ (have at least 1 't')
  • Returns 1 (this is a valid string)

If we had reached dfs(0, 1, 1, 1) instead:

  • e = 1 ✗ (need 2 'e's but only have 1)
  • Returns 0 (invalid string)

The path we traced ('l', 'e', 'e', 't', any character) represents strings like "leetx", "leeta", etc. The algorithm counts all possible paths that lead to valid strings, using memoization to avoid recalculating states that can be reached through different character orderings.

Solution Implementation

1class Solution:
2    def stringCount(self, n: int) -> int:
3        from functools import cache
4      
5        # Define modulo constant for large number handling
6        MOD = 10**9 + 7
7      
8        @cache
9        def count_valid_strings(remaining_chars: int, l_count: int, e_count: int, t_count: int) -> int:
10            """
11            Dynamic programming function to count valid strings.
12          
13            Args:
14                remaining_chars: Number of characters left to place
15                l_count: Number of 'l' characters used (capped at 1)
16                e_count: Number of 'e' characters used (capped at 2)
17                t_count: Number of 't' characters used (capped at 1)
18          
19            Returns:
20                Number of valid strings that can be formed
21            """
22            # Base case: no more characters to place
23            if remaining_chars == 0:
24                # Check if we have exactly the required letters for "leet"
25                # Need at least 1 'l', 2 'e's, and 1 't'
26                return int(l_count == 1 and e_count == 2 and t_count == 1)
27          
28            # Option 1: Add any of 23 other letters (not l, e, or t)
29            # There are 26 letters total, minus l, e, t = 23 letters
30            other_letters = count_valid_strings(remaining_chars - 1, l_count, e_count, t_count) * 23 % MOD
31          
32            # Option 2: Add letter 'l' (cap at 1 since we only need 1)
33            add_l = count_valid_strings(remaining_chars - 1, min(1, l_count + 1), e_count, t_count)
34          
35            # Option 3: Add letter 'e' (cap at 2 since we need exactly 2)
36            add_e = count_valid_strings(remaining_chars - 1, l_count, min(2, e_count + 1), t_count)
37          
38            # Option 4: Add letter 't' (cap at 1 since we only need 1)
39            add_t = count_valid_strings(remaining_chars - 1, l_count, e_count, min(1, t_count + 1))
40          
41            # Return total combinations modulo MOD
42            return (other_letters + add_l + add_e + add_t) % MOD
43      
44        # Start with n characters to place and no special letters used yet
45        return count_valid_strings(n, 0, 0, 0)
46
1class Solution {
2    // Modulo value for preventing integer overflow
3    private static final int MOD = 1_000_000_007;
4  
5    // Memoization table: [remainingPositions][hasL][countE][hasT]
6    // hasL: 0 or 1 (whether we have at least one 'l')
7    // countE: 0, 1, or 2 (number of 'e's, capped at 2)
8    // hasT: 0 or 1 (whether we have at least one 't')
9    private Long[][][][] memo;
10
11    public int stringCount(int n) {
12        // Initialize memoization table
13        memo = new Long[n + 1][2][3][2];
14      
15        // Start DFS with n positions remaining and no required letters yet
16        return (int) countValidStrings(n, 0, 0, 0);
17    }
18
19    /**
20     * Counts the number of valid strings that can be formed.
21     * A valid string must contain at least: 1 'l', 2 'e's, and 1 't'.
22     * 
23     * @param remainingPositions Number of positions left to fill
24     * @param hasL Whether we have at least one 'l' (0 or 1)
25     * @param countE Number of 'e's we have (0, 1, or 2)
26     * @param hasT Whether we have at least one 't' (0 or 1)
27     * @return Number of valid strings that can be formed
28     */
29    private long countValidStrings(int remainingPositions, int hasL, int countE, int hasT) {
30        // Base case: no positions left
31        if (remainingPositions == 0) {
32            // Check if we have the required letters: 1 'l', 2 'e's, 1 't'
33            return (hasL == 1 && countE == 2 && hasT == 1) ? 1 : 0;
34        }
35      
36        // Check memoization table
37        if (memo[remainingPositions][hasL][countE][hasT] != null) {
38            return memo[remainingPositions][hasL][countE][hasT];
39        }
40      
41        // Calculate possibilities for each choice at current position
42      
43        // Choice 1: Use any of the 23 other letters (not 'l', 'e', or 't')
44        long useOtherLetter = countValidStrings(remainingPositions - 1, hasL, countE, hasT) * 23 % MOD;
45      
46        // Choice 2: Use letter 'l'
47        long useLetterL = countValidStrings(remainingPositions - 1, Math.min(1, hasL + 1), countE, hasT);
48      
49        // Choice 3: Use letter 'e'
50        long useLetterE = countValidStrings(remainingPositions - 1, hasL, Math.min(2, countE + 1), hasT);
51      
52        // Choice 4: Use letter 't'
53        long useLetterT = countValidStrings(remainingPositions - 1, hasL, countE, Math.min(1, hasT + 1));
54      
55        // Store result in memoization table and return
56        long totalCount = (useOtherLetter + useLetterL + useLetterE + useLetterT) % MOD;
57        memo[remainingPositions][hasL][countE][hasT] = totalCount;
58      
59        return totalCount;
60    }
61}
62
1class Solution {
2public:
3    int stringCount(int n) {
4        const int MOD = 1e9 + 7;
5        using ll = long long;
6      
7        // DP memoization table
8        // dp[remaining_positions][has_L][count_E][has_T]
9        // has_L: 0 or 1 (whether we have at least one 'l')
10        // count_E: 0, 1, or 2 (number of 'e's, capped at 2 since we need at least 2)
11        // has_T: 0 or 1 (whether we have at least one 't')
12        ll dp[n + 1][2][3][2];
13        memset(dp, -1, sizeof(dp));
14      
15        // Recursive function with memoization
16        function<ll(int, int, int, int)> solve = [&](int remainingPos, int hasL, int countE, int hasT) -> ll {
17            // Base case: no positions left
18            if (remainingPos == 0) {
19                // Valid string must have at least 1 'l', 2 'e's, and 1 't'
20                return (hasL == 1 && countE == 2 && hasT == 1) ? 1 : 0;
21            }
22          
23            // Return memoized result if already computed
24            if (dp[remainingPos][hasL][countE][hasT] != -1) {
25                return dp[remainingPos][hasL][countE][hasT];
26            }
27          
28            // Option 1: Place any of the 23 other letters (not 'l', 'e', or 't')
29            ll placeOtherLetter = solve(remainingPos - 1, hasL, countE, hasT) * 23 % MOD;
30          
31            // Option 2: Place letter 'l'
32            ll placeL = solve(remainingPos - 1, min(1, hasL + 1), countE, hasT) % MOD;
33          
34            // Option 3: Place letter 'e'
35            ll placeE = solve(remainingPos - 1, hasL, min(2, countE + 1), hasT) % MOD;
36          
37            // Option 4: Place letter 't'
38            ll placeT = solve(remainingPos - 1, hasL, countE, min(1, hasT + 1)) % MOD;
39          
40            // Store and return the total count
41            return dp[remainingPos][hasL][countE][hasT] = (placeOtherLetter + placeL + placeE + placeT) % MOD;
42        };
43      
44        // Start with n positions and no letters placed yet
45        return solve(n, 0, 0, 0);
46    }
47};
48
1/**
2 * Counts the number of strings of length n that contain at least one 'l', 
3 * at least two 'e's, and at least one 't' from a 26-letter alphabet
4 * @param n - The length of the string
5 * @returns The count of valid strings modulo 10^9 + 7
6 */
7function stringCount(n: number): number {
8    const MOD = 10 ** 9 + 7;
9  
10    // Memoization table: memo[remainingChars][hasL][eCount][hasT]
11    // hasL: 0 or 1 (whether we have at least one 'l')
12    // eCount: 0, 1, or 2 (capped at 2 since we need at least 2 'e's)
13    // hasT: 0 or 1 (whether we have at least one 't')
14    const memo: number[][][][] = Array.from({ length: n + 1 }, () =>
15        Array.from({ length: 2 }, () =>
16            Array.from({ length: 3 }, () => 
17                Array.from({ length: 2 }, () => -1)
18            )
19        )
20    );
21  
22    /**
23     * Dynamic programming function to count valid strings
24     * @param remainingChars - Number of characters left to place
25     * @param hasL - Whether we have placed at least one 'l' (0 or 1)
26     * @param eCount - Number of 'e's placed so far (capped at 2)
27     * @param hasT - Whether we have placed at least one 't' (0 or 1)
28     * @returns Number of valid strings from this state
29     */
30    const dfs = (remainingChars: number, hasL: number, eCount: number, hasT: number): number => {
31        // Base case: no more characters to place
32        if (remainingChars === 0) {
33            // Valid only if we have at least 1 'l', exactly 2 'e's, and at least 1 't'
34            return hasL === 1 && eCount === 2 && hasT === 1 ? 1 : 0;
35        }
36      
37        // Check memoization
38        if (memo[remainingChars][hasL][eCount][hasT] !== -1) {
39            return memo[remainingChars][hasL][eCount][hasT];
40        }
41      
42        // Option 1: Place any of the 23 other letters (not 'l', 'e', or 't')
43        const placeOtherLetter = (dfs(remainingChars - 1, hasL, eCount, hasT) * 23) % MOD;
44      
45        // Option 2: Place an 'l'
46        const placeL = dfs(remainingChars - 1, Math.min(1, hasL + 1), eCount, hasT);
47      
48        // Option 3: Place an 'e'
49        const placeE = dfs(remainingChars - 1, hasL, Math.min(2, eCount + 1), hasT);
50      
51        // Option 4: Place a 't'
52        const placeT = dfs(remainingChars - 1, hasL, eCount, Math.min(1, hasT + 1));
53      
54        // Store and return the total count
55        memo[remainingChars][hasL][eCount][hasT] = (placeOtherLetter + placeL + placeE + placeT) % MOD;
56        return memo[remainingChars][hasL][eCount][hasT];
57    };
58  
59    // Start with n characters to place and no letters placed yet
60    return dfs(n, 0, 0, 0);
61}
62

Time and Space Complexity

The time complexity is O(n). The recursive function dfs has 4 parameters: i ranges from 0 to n, l can be 0 or 1, e can be 0, 1, or 2, and t can be 0 or 1. This gives us at most n × 2 × 3 × 2 = 12n unique states. Due to memoization with @cache, each state is computed only once. Each state computation involves constant time operations (4 recursive calls and modular arithmetic), so the total time complexity is O(n).

The space complexity is O(n). The memoization cache stores up to 12n states, which is O(n) space. Additionally, the recursion depth is at most n (when i goes from n to 0), contributing O(n) to the call stack space. Therefore, the overall space complexity is O(n).

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

Common Pitfalls

1. Incorrect State Capping Logic

A critical pitfall is failing to properly cap the character counts or capping them at wrong values. This leads to either counting invalid strings or missing valid ones.

Wrong Implementation:

# Incorrect: Not capping the counts
def dfs(i, l, e, t):
    if i == 0:
        return int(l >= 1 and e >= 2 and t >= 1)  # Wrong check!
    # ...
    add_l = dfs(i - 1, l + 1, e, t)  # No capping!
    add_e = dfs(i - 1, l, e + 1, t)  # No capping!

Why it's wrong: Without capping, we create exponentially more states than necessary. For example, having 5 'e's vs 2 'e's doesn't matter for forming "leet", but without capping, these become different states, destroying memoization efficiency and causing TLE.

Correct Implementation:

def dfs(i, l, e, t):
    if i == 0:
        return int(l == 1 and e == 2 and t == 1)  # Exact check
    # ...
    add_l = dfs(i - 1, min(1, l + 1), e, t)  # Cap at 1
    add_e = dfs(i - 1, l, min(2, e + 1), t)  # Cap at 2
    add_t = dfs(i - 1, l, e, min(1, t + 1))  # Cap at 1

2. Forgetting to Apply Modulo at Each Step

Another common mistake is only applying modulo at the final return, which can cause integer overflow in languages with fixed integer sizes.

Wrong Implementation:

def dfs(i, l, e, t):
    # ...
    other_letters = dfs(i - 1, l, e, t) * 23  # Missing modulo!
    # ...
    return (other_letters + add_l + add_e + add_t) % MOD

Correct Implementation:

def dfs(i, l, e, t):
    # ...
    other_letters = dfs(i - 1, l, e, t) * 23 % MOD  # Apply modulo immediately
    # ...
    return (other_letters + add_l + add_e + add_t) % MOD

3. Misunderstanding the Problem Requirements

A conceptual pitfall is thinking we need to track the exact positions of 'l', 'e', 'e', 't' or that we need the substring "leet" to appear consecutively in our constructed string.

Wrong Understanding:

  • Thinking we need to place "leet" as a consecutive substring during construction
  • Tracking positions where each character appears
  • Checking if the final string contains "leet" as-is

Correct Understanding:

  • We only need to ensure the string has enough of each required character
  • The characters can be rearranged later to form "leet" as a substring
  • We're counting strings that CAN BE rearranged, not strings that already contain "leet"

4. Incorrect Base Case Logic

Using >= instead of == in the base case check is a subtle but critical error.

Wrong Implementation:

if i == 0:
    return int(l >= 1 and e >= 2 and t >= 1)  # Wrong!

Why it's wrong: Due to our capping logic, l can only be 0 or 1, e can only be 0, 1, or 2, and t can only be 0 or 1. Using >= would incorrectly count states where we have fewer than required characters (since 0 >= 1 is False, but so is 0 == 1). The == check ensures we have exactly the capped values that represent "at least" the required amounts.

Correct Implementation:

if i == 0:
    return int(l == 1 and e == 2 and t == 1)  # Correct!
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

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

Load More