Facebook Pixel

1177. Can Make Palindrome from Substring

MediumBit ManipulationArrayHash TableStringPrefix Sum
Leetcode Link

Problem Description

You are given a string s and an array of queries. Each query is represented as queries[i] = [left_i, right_i, k_i] and asks: can the substring s[left_i...right_i] be made into a palindrome by performing the following operations?

  1. Rearrange the characters in the substring in any order
  2. Replace up to k_i characters with any lowercase English letter

For each query, you need to determine if it's possible to create a palindrome using these operations and return true if possible, false otherwise.

Key Points:

  • Each character replacement is counted individually. For example, if the substring is "aaa" and k = 2, you can only replace 2 of the 'a' characters
  • The original string s is never modified - each query is independent
  • Characters can be rearranged before replacements are made

Understanding Palindrome Formation: To form a palindrome from a substring with replacements:

  • Characters that appear an even number of times can be paired and placed symmetrically
  • Characters that appear an odd number of times need special handling:
    • At most one character can have an odd count (it goes in the middle)
    • If there are multiple characters with odd counts, we need to replace some to make them even
    • The minimum replacements needed is ⌊odd_count/2⌋ where odd_count is the number of characters with odd frequency

Example Walkthrough: For s = "abcda" and query [0, 3, 2] (substring "abcd" with up to 2 replacements):

  • Character frequencies: a=1, b=1, c=1, d=1 (all odd)
  • We have 4 characters with odd counts
  • Minimum replacements needed: ⌊4/2⌋ = 2
  • Since 2 ≤ 2, we can form a palindrome (e.g., change to "abba")
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that to form a palindrome, characters must be paired symmetrically, except for at most one character that can sit in the middle (for odd-length palindromes).

When we count character frequencies in a substring:

  • Characters with even counts are already "palindrome-ready" - they can be split into pairs and placed symmetrically
  • Characters with odd counts are problematic - only one can remain odd (in the middle), the rest must be made even

For example, if we have the substring "aabbbcc":

  • a appears 2 times (even) ✓
  • b appears 3 times (odd) ✗
  • c appears 2 times (even) ✓

We have 1 character with odd count. This can form a palindrome like "cabbbac" without any replacements.

But what if we have "abcd" where all 4 characters appear once (odd)? We need to convert some odd counts to even counts through replacements. The optimal strategy is to replace characters in pairs - replacing one occurrence of a character reduces its odd count by 1, making it even.

If we have x characters with odd counts:

  • We can keep 1 character odd (for the middle position)
  • The remaining x - 1 characters need to become even
  • Each replacement fixes one odd-count character
  • So we need at least ⌊x/2⌋ replacements

This explains why the solution checks if cnt // 2 <= k, where cnt is the number of characters with odd counts.

To efficiently count character frequencies for any substring [l, r], we use prefix sums. Instead of recounting characters for each query, we precompute cumulative counts: ss[i][j] stores how many times character j appears in s[0...i-1]. Then the count of character j in substring [l, r] is simply ss[r+1][j] - ss[l][j].

Learn more about Prefix Sum patterns.

Solution Approach

The solution uses prefix sum to efficiently count character frequencies in any substring. Here's the step-by-step implementation:

1. Build Prefix Sum Array

We create a 2D array ss where ss[i][j] represents the count of character j in the first i characters of string s:

ss = [[0] * 26 for _ in range(n + 1)]
for i, c in enumerate(s, 1):
    ss[i] = ss[i - 1][:]  # Copy previous counts
    ss[i][ord(c) - ord("a")] += 1  # Increment current character
  • Initialize ss[0] with all zeros (empty prefix)
  • For each position i, copy the counts from ss[i-1]
  • Increment the count for the current character c
  • Convert character to index using ord(c) - ord("a") (maps 'a'→0, 'b'→1, ..., 'z'→25)

2. Process Each Query

For each query [l, r, k]:

cnt = sum((ss[r + 1][j] - ss[l][j]) & 1 for j in range(26))
ans.append(cnt // 2 <= k)
  • Calculate character frequency: For character j, its count in substring s[l...r] is ss[r + 1][j] - ss[l][j]
  • Count odd frequencies: Use & 1 (bitwise AND with 1) to check if a count is odd
    • Even numbers: even & 1 = 0
    • Odd numbers: odd & 1 = 1
  • Sum odd counts: cnt stores the total number of characters with odd frequencies
  • Check palindrome possibility: We need ⌊cnt/2⌋ replacements to make a palindrome
    • If cnt // 2 <= k, we can form a palindrome with at most k replacements

Time Complexity: O(n + q) where n is the length of string and q is the number of queries

  • Building prefix sum: O(n × 26) = O(n)
  • Processing each query: O(26) = O(1)
  • Total for all queries: O(q)

Space Complexity: O(n) for the prefix sum array ss with dimensions (n+1) × 26

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 s = "bdbc" and query [1, 3, 1] (checking substring "dbc" with up to 1 replacement).

Step 1: Build Prefix Sum Array

We create a prefix sum array to track character counts up to each position:

Position:  0    1    2    3    4
String:    ""   "b"  "bd" "bdb" "bdbc"

For each position, we track counts of all 26 letters. Here are the relevant ones:

  • ss[0]: b=0, c=0, d=0 (empty prefix)
  • ss[1]: b=1, c=0, d=0 (after "b")
  • ss[2]: b=1, c=0, d=1 (after "bd")
  • ss[3]: b=2, c=0, d=1 (after "bdb")
  • ss[4]: b=2, c=1, d=1 (after "bdbc")

Step 2: Process Query [1, 3, 1]

We want substring s[1...3] = "dbc" with k=1 replacement allowed.

Calculate character frequencies in substring using prefix sums:

  • b: ss[4][1] - ss[1][1] = 2 - 1 = 1 (odd)
  • c: ss[4][2] - ss[1][2] = 1 - 0 = 1 (odd)
  • d: ss[4][3] - ss[1][3] = 1 - 0 = 1 (odd)

Count characters with odd frequencies:

  • b appears 1 time (odd) → contributes 1
  • c appears 1 time (odd) → contributes 1
  • d appears 1 time (odd) → contributes 1
  • Total: cnt = 3

Step 3: Check Palindrome Possibility

With 3 characters having odd counts, we need ⌊3/2⌋ = 1 replacement to form a palindrome.

Since 1 ≤ 1 (needed replacements ≤ k), the answer is true.

We could form palindrome "cbc" by replacing the 'd' with 'c', then rearranging.

Solution Implementation

1class Solution:
2    def canMakePaliQueries(self, s: str, queries: List[List[int]]) -> List[bool]:
3        """
4        Determines if substrings can be rearranged into palindromes with at most k changes.
5      
6        Args:
7            s: Input string containing lowercase letters
8            queries: List of [left, right, k] where we check substring s[left:right+1]
9                    and k is the maximum number of character changes allowed
10      
11        Returns:
12            List of boolean values indicating if each query substring can form a palindrome
13        """
14        string_length = len(s)
15      
16        # Build prefix sum array for character frequencies
17        # prefix_counts[i][j] represents count of character j in s[0:i]
18        prefix_counts = [[0] * 26 for _ in range(string_length + 1)]
19      
20        # Populate prefix sum array
21        for index, char in enumerate(s, start=1):
22            # Copy previous counts
23            prefix_counts[index] = prefix_counts[index - 1][:]
24            # Increment count for current character
25            char_position = ord(char) - ord('a')
26            prefix_counts[index][char_position] += 1
27      
28        # Process each query
29        result = []
30        for left, right, max_changes in queries:
31            # Count characters with odd frequency in substring s[left:right+1]
32            odd_count = 0
33            for char_index in range(26):
34                # Get frequency of character in substring using prefix sums
35                char_frequency = prefix_counts[right + 1][char_index] - prefix_counts[left][char_index]
36                # Check if frequency is odd
37                if char_frequency & 1:  # Bitwise AND with 1 checks if odd
38                    odd_count += 1
39          
40            # For a palindrome, at most 1 character can have odd frequency
41            # We need to pair up odd_count // 2 characters
42            # Check if we can fix unpaired characters with available changes
43            can_form_palindrome = (odd_count // 2) <= max_changes
44            result.append(can_form_palindrome)
45      
46        return result
47
1class Solution {
2    public List<Boolean> canMakePaliQueries(String s, int[][] queries) {
3        int n = s.length();
4      
5        // Create prefix sum array to store character frequencies
6        // prefixSum[i][j] represents count of character 'a'+j in s[0...i-1]
7        int[][] prefixSum = new int[n + 1][26];
8      
9        // Build the prefix sum array
10        for (int i = 1; i <= n; i++) {
11            // Copy previous row's counts
12            for (int j = 0; j < 26; j++) {
13                prefixSum[i][j] = prefixSum[i - 1][j];
14            }
15            // Increment count for current character
16            int charIndex = s.charAt(i - 1) - 'a';
17            prefixSum[i][charIndex]++;
18        }
19      
20        // Process each query and store results
21        List<Boolean> result = new ArrayList<>();
22      
23        for (int[] query : queries) {
24            int left = query[0];
25            int right = query[1];
26            int maxReplacements = query[2];
27          
28            // Count characters with odd frequency in substring s[left...right]
29            int oddFrequencyCount = 0;
30            for (int j = 0; j < 26; j++) {
31                // Calculate frequency of character j in substring
32                int frequency = prefixSum[right + 1][j] - prefixSum[left][j];
33                // Check if frequency is odd using bitwise AND
34                oddFrequencyCount += frequency & 1;
35            }
36          
37            // A palindrome can have at most one character with odd frequency
38            // We need to pair up characters with odd frequency
39            // Each replacement can fix 2 characters with odd frequency
40            // So we need oddFrequencyCount/2 replacements
41            boolean canFormPalindrome = (oddFrequencyCount / 2) <= maxReplacements;
42            result.add(canFormPalindrome);
43        }
44      
45        return result;
46    }
47}
48
1class Solution {
2public:
3    vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
4        int n = s.size();
5      
6        // Create prefix sum array to store character counts
7        // prefixSum[i][j] represents count of character 'a'+j in s[0...i-1]
8        int prefixSum[n + 1][26];
9        memset(prefixSum, 0, sizeof(prefixSum));
10      
11        // Build prefix sum array
12        for (int i = 1; i <= n; ++i) {
13            // Copy previous counts
14            for (int j = 0; j < 26; ++j) {
15                prefixSum[i][j] = prefixSum[i - 1][j];
16            }
17            // Increment count for current character
18            prefixSum[i][s[i - 1] - 'a']++;
19        }
20      
21        vector<bool> result;
22      
23        // Process each query
24        for (auto& query : queries) {
25            int left = query[0];
26            int right = query[1]; 
27            int maxReplacements = query[2];
28          
29            // Count characters with odd frequency in substring s[left...right]
30            int oddCount = 0;
31            for (int j = 0; j < 26; ++j) {
32                // Get character count in range using prefix sum
33                int charCount = prefixSum[right + 1][j] - prefixSum[left][j];
34                // Check if count is odd (using bitwise AND with 1)
35                oddCount += charCount & 1;
36            }
37          
38            // To form palindrome, we need to pair up characters with odd frequency
39            // Each replacement can fix 2 odd characters (change one to match another)
40            // So we need oddCount/2 replacements at most
41            result.emplace_back(oddCount / 2 <= maxReplacements);
42        }
43      
44        return result;
45    }
46};
47
1/**
2 * Determines if substrings can be rearranged into palindromes with at most k character replacements
3 * @param s - The input string containing lowercase English letters
4 * @param queries - Array of queries where each query is [left, right, k]
5 * @returns Array of boolean values indicating if each query's substring can form a palindrome
6 */
7function canMakePaliQueries(s: string, queries: number[][]): boolean[] {
8    const stringLength: number = s.length;
9  
10    // Create prefix sum array to store character frequency counts
11    // prefixSum[i][j] represents count of character j in s[0...i-1]
12    const prefixSum: number[][] = Array(stringLength + 1)
13        .fill(0)
14        .map(() => Array(26).fill(0));
15  
16    // Build prefix sum array by accumulating character counts
17    for (let i = 1; i <= stringLength; i++) {
18        // Copy previous row's counts
19        prefixSum[i] = prefixSum[i - 1].slice();
20      
21        // Increment count for current character (convert to 0-based index)
22        const charIndex: number = s.charCodeAt(i - 1) - 97;
23        prefixSum[i][charIndex]++;
24    }
25  
26    const results: boolean[] = [];
27  
28    // Process each query
29    for (const [left, right, maxReplacements] of queries) {
30        // Count characters with odd frequency in the substring
31        let oddFrequencyCount: number = 0;
32      
33        // Check each letter from 'a' to 'z'
34        for (let charIndex = 0; charIndex < 26; charIndex++) {
35            // Calculate character frequency in substring using prefix sums
36            const frequencyInRange: number = prefixSum[right + 1][charIndex] - prefixSum[left][charIndex];
37          
38            // Add 1 if frequency is odd (using bitwise AND with 1)
39            oddFrequencyCount += frequencyInRange & 1;
40        }
41      
42        // For a palindrome, at most 1 character can have odd frequency
43        // We need to pair up oddFrequencyCount/2 characters
44        // Check if we can fix unpaired characters with at most k replacements
45        const replacementsNeeded: number = oddFrequencyCount >> 1; // Equivalent to Math.floor(oddFrequencyCount / 2)
46        results.push(replacementsNeeded <= maxReplacements);
47    }
48  
49    return results;
50}
51

Time and Space Complexity

Time Complexity: O((n + m) × 26) or O((n + m) × C) where C = 26

The time complexity breaks down into two main parts:

  1. Building the prefix sum array: The outer loop runs n times (for each character in string s). For each iteration, we copy the previous row which takes O(26) time and update one position. This gives us O(n × 26) time.
  2. Processing queries: For each of the m queries, we calculate the count of characters with odd frequencies by iterating through all 26 letters. This takes O(m × 26) time.

Therefore, the total time complexity is O(n × 26 + m × 26) = O((n + m) × 26).

Space Complexity: O(n × 26) or O(n × C) where C = 26

The space complexity is dominated by the prefix sum array ss, which has dimensions (n + 1) × 26. This stores the cumulative count of each of the 26 lowercase letters at each position from 0 to n. The answer list ans takes O(m) space, but since m could be smaller than n × 26, the overall space complexity remains O(n × 26).

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

Common Pitfalls

1. Off-by-One Errors in Prefix Sum Indexing

A frequent mistake is incorrectly calculating the substring frequency using prefix sums. Developers often write:

# WRONG: This calculates s[left-1:right] instead of s[left:right+1]
char_frequency = prefix_counts[right][char_index] - prefix_counts[left][char_index]

Why it happens: The confusion arises because:

  • String indexing is 0-based: s[left] is the character at position left
  • Prefix sum prefix_counts[i] contains counts for s[0:i] (first i characters)
  • To get substring s[left:right+1], we need prefix_counts[right+1] - prefix_counts[left]

Solution: Always remember that for substring s[left...right] (inclusive):

# CORRECT: Gets frequency in s[left:right+1]
char_frequency = prefix_counts[right + 1][char_index] - prefix_counts[left][char_index]

2. Misunderstanding the Replacement Count

Another common error is incorrectly calculating how many replacements are needed:

# WRONG: Thinks we need to replace all odd-count characters
replacements_needed = odd_count

Why it happens: The intuition that "all odd-count characters need fixing" misses the key insight that:

  • One character with odd count can stay in the middle of the palindrome
  • We only need to pair up the excess odd-count characters
  • Each replacement fixes two odd-count characters (changes one odd to even, allowing pairing)

Solution: The correct formula is:

# CORRECT: Only need to fix pairs of odd-count characters
replacements_needed = odd_count // 2

3. Inefficient Character Frequency Calculation

Without prefix sums, a naive approach recalculates frequencies for each query:

# INEFFICIENT: O(q * n) time complexity
for left, right, k in queries:
    freq = [0] * 26
    for i in range(left, right + 1):
        freq[ord(s[i]) - ord('a')] += 1
    # ... rest of logic

Why it happens: It seems straightforward to count characters for each query independently, but this becomes prohibitively slow for large inputs.

Solution: Use prefix sums for O(1) frequency calculation per query:

# EFFICIENT: O(n) preprocessing, then O(1) per query
# Build prefix sums once
prefix_counts = [[0] * 26 for _ in range(n + 1)]
for i, c in enumerate(s, 1):
    prefix_counts[i] = prefix_counts[i - 1][:]
    prefix_counts[i][ord(c) - ord('a')] += 1

# Use prefix sums for each query
for left, right, k in queries:
    # O(26) = O(1) to calculate all frequencies
    for j in range(26):
        freq = prefix_counts[right + 1][j] - prefix_counts[left][j]

4. Forgetting to Handle Edge Cases

Some implementations fail on edge cases like:

  • Single character substrings (always palindromes)
  • Empty queries list
  • k = 0 (no replacements allowed)

Solution: The provided algorithm naturally handles these cases:

  • Single character: odd_count = 1, needs 1//2 = 0 replacements ✓
  • No replacements (k=0): Only palindromes with at most 1 odd-count character work ✓
  • Empty queries: Returns empty list ✓
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

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

Load More