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_icharacters 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
sis 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_countis 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":
aappears 2 times (even) ✓bappears 3 times (odd) ✗cappears 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 - 1characters 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:
cntstores 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 mostkreplacements
- 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
471class 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}
481class 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};
471/**
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}
51Time 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
ntimes (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
mqueries, 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](firsticharacters) - 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 = 0replacements ✓ - No replacements (k=0): Only palindromes with at most 1 odd-count character work ✓
- Empty queries: Returns empty list ✓
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Prefix Sum Given an array of integers arr and a target value find a subarray that sums to the target Return the start and end indices of the subarray Input arr 1 20 3 30 5 4 target 7 Output 1 4 Explanation The subarray 20 3 30 from index 1 to
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!