LeetCode 1177 - Can Make Palindrome from Substring
The problem asks us to determine whether certain substrings of a given string s can be rearranged and partially modified to form a palindrome. A palindrome is a string that reads the same forward and backward.
Difficulty: 🟡 Medium
Topics: Array, Hash Table, String, Bit Manipulation, Prefix Sum
Solution
Problem Understanding
The problem asks us to determine whether certain substrings of a given string s can be rearranged and partially modified to form a palindrome. A palindrome is a string that reads the same forward and backward. For each query [lefti, righti, ki], we are allowed to take the substring s[lefti...righti], rearrange its characters in any order, and replace up to ki characters with any lowercase letter. We then need to check if the resulting substring can be a palindrome.
The input consists of the string s and a list of queries. Each query gives the start and end indices of the substring and the maximum number of character replacements allowed. The output is a boolean array where each element indicates whether the corresponding substring can be transformed into a palindrome.
Constraints indicate that s and queries can be very large, up to 10^5 elements. This rules out naive solutions that examine each substring character by character or attempt all permutations. Edge cases include substrings of length 1, substrings with all identical letters, and queries with ki equal to zero.
The core challenge is efficiently determining, for any substring, how many characters would need to be changed to form a palindrome without explicitly rearranging or iterating over the substring each time.
Approaches
Brute Force
The brute-force solution would extract each substring for every query, count the occurrences of each character, and check if a palindrome is possible given ki. For a palindrome, at most one character can appear an odd number of times. We would compute how many characters have odd counts and see if ki is sufficient to convert enough of them to even counts. This approach is correct but extremely slow, since counting character frequencies for each substring takes O(n) time per query, resulting in O(n * q) complexity.
Optimal Approach
The optimal approach leverages prefix bitmasks to track character counts efficiently. Each letter a-z can be represented as a bit in a 26-bit integer. XOR operations allow us to compute the character count parity for any substring in constant time using a prefix array. A palindrome can tolerate at most one character with an odd count per half-length of the substring. We check if the number of odd-count characters divided by 2 is less than or equal to ki.
This approach avoids substring iteration and reduces the complexity from O(n * q) to O(n + q), which is feasible for the input size.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n * q) | O(26) per query | Count character frequencies for each substring |
| Optimal | O(n + q) | O(n) | Use prefix bitmasks and XOR to compute odd counts efficiently |
Algorithm Walkthrough
- Initialize a prefix array
prefix_maskof sizelen(s)+1whereprefix_mask[i]stores a 26-bit integer representing the parity of character counts fors[0...i-1]. Each bit corresponds to a letter: 1 if the count is odd, 0 if even. - Iterate through the string
s. For each charactercat indexi, updateprefix_mask[i+1] = prefix_mask[i] ^ (1 << (ord(c) - ord('a'))). This XOR operation flips the corresponding bit, tracking the parity of occurrences. - For each query
[l, r, k], calculate the substring bitmaskmask = prefix_mask[r+1] ^ prefix_mask[l]. The XOR cancels out characters outside the substring, leaving only the odd/even counts fors[l...r]. - Count the number of set bits in
maskusing a population count function. Each set bit corresponds to a character with an odd count. - Compute
needed_replacements = odd_count // 2. This represents the minimum number of changes needed to make the substring a palindrome, since two odd-count characters can be fixed with one replacement. - If
needed_replacements <= k, appendTrueto the answer array; otherwise, appendFalse.
Why it works: The XOR-based prefix array encodes parity information for each prefix of the string. Substring XORs isolate the odd-count letters, allowing a constant-time check for each query. Palindromes require at most one odd character for odd-length strings, and even-length strings require all even counts. Dividing the odd count by 2 correctly accounts for pairwise transformations needed.
Python Solution
from typing import List
class Solution:
def canMakePaliQueries(self, s: str, queries: List[List[int]]) -> List[bool]:
n = len(s)
prefix_mask = [0] * (n + 1)
for i in range(n):
prefix_mask[i + 1] = prefix_mask[i] ^ (1 << (ord(s[i]) - ord('a')))
result = []
for l, r, k in queries:
mask = prefix_mask[r + 1] ^ prefix_mask[l]
odd_count = bin(mask).count('1')
result.append(odd_count // 2 <= k)
return result
In this implementation, prefix_mask tracks parity for each character using a 26-bit integer. XOR operations efficiently compute substring parities. The population count gives the number of odd characters, and dividing by 2 determines the required replacements. The final boolean is appended to the result for each query.
Go Solution
func canMakePaliQueries(s string, queries [][]int) []bool {
n := len(s)
prefixMask := make([]int, n+1)
for i := 0; i < n; i++ {
prefixMask[i+1] = prefixMask[i] ^ (1 << (s[i] - 'a'))
}
result := make([]bool, len(queries))
for i, q := range queries {
l, r, k := q[0], q[1], q[2]
mask := prefixMask[r+1] ^ prefixMask[l]
oddCount := 0
for mask > 0 {
oddCount += mask & 1
mask >>= 1
}
result[i] = oddCount/2 <= k
}
return result
}
The Go implementation mirrors the Python logic, using an integer slice for prefix masks. Population count is performed manually using bitwise operations since Go lacks a built-in bin().count('1').
Worked Examples
For s = "abcda" and queries = [[3,3,0],[1,2,0],[0,3,1],[0,3,2],[0,4,1]]:
| Query | Substring | Mask (binary) | Odd count | Needed replacements | Result |
|---|---|---|---|---|---|
| [3,3,0] | "d" | 0001000 | 1 | 0 | True |
| [1,2,0] | "bc" | 0110 | 2 | 1 | False |
| [0,3,1] | "abcd" | 1111 | 4 | 2 | False |
| [0,3,2] | "abcd" | 1111 | 4 | 2 | True |
| [0,4,1] | "abcda" | 11101 | 3 | 1 | True |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + q) | O(n) to compute prefix masks, O(q) to process queries |
| Space | O(n) | Store prefix mask for each character position |
The linear time complexity allows the solution to handle the maximum constraints efficiently, while space scales linearly with the input string length.
Test Cases
# Provided examples
assert Solution().canMakePaliQueries("abcda", [[3,3,0],[1,2,0],[0,3,1],[0,3,2],[0,4,1]]) == [True, False, False, True, True]
assert Solution().canMakePaliQueries("lyb", [[0,1,0],[2,2,1]]) == [False, True]
# Edge cases
assert Solution().canMakePaliQueries("a", [[0,0,0]]) == [True] # Single letter is always palindrome
assert Solution().canMakePaliQueries("aa", [[0,1,0]]) == [True] # Already palindrome
assert Solution().canMakePaliQueries("ab", [[0,1,0]]) == [False] # Needs 1 replacement, ki=0
assert Solution().canMakePaliQueries("abcd", [[0,3,2]]) == [True] # Needs 2 replacements
assert Solution().canMakePaliQueries("abcd", [[0,3,1]]) == [False] # Not enough replacements
| Test | Why |
|---|---|
| Single letter | Minimal case, trivially palindrome |
| Two identical letters | Already palindrome, no replacement needed |
| Two different letters, ki=0 | Cannot become palindrome |
| Substring longer, ki sufficient | Tests correct counting of needed replacements |
| Substring longer, ki insufficient | Verifies handling when replacements are insufficient |
Edge Cases
- Single-character substring: Any single character is trivially a palindrome. Implementation handles this correctly because
odd_count = 1, `needed_replacements