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:

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings

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

Load More