1177. Can Make Palindrome from Substring
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?
- Rearrange the characters in the substring in any order
- 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"
andk = 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⌋
whereodd_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"
)
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 fromss[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 substrings[l...r]
isss[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
- Even numbers:
- 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 mostk
replacements
- If
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 EvaluatorExample 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:
- Building the prefix sum array: The outer loop runs
n
times (for each character in strings
). For each iteration, we copy the previous row which takesO(26)
time and update one position. This gives usO(n × 26)
time. - Processing queries: For each of the
m
queries, we calculate the count of characters with odd frequencies by iterating through all 26 letters. This takesO(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 positionleft
- Prefix sum
prefix_counts[i]
contains counts fors[0:i]
(firsti
characters) - To get substring
s[left:right+1]
, we needprefix_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
, needs1//2 = 0
replacements ✓ - No replacements (k=0): Only palindromes with at most 1 odd-count character work ✓
- Empty queries: Returns empty list ✓
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!