Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1177. Can Make Palindrome from Substring - Leetcode Solution

Problem Description

Given a string s and an array of queries queries, where each query is represented as [left, right, k], your task is to determine for each query whether the substring s[left:right+1] can be transformed into a palindrome by changing at most k characters.

  • You must answer for each query whether it's possible (True) or not (False).
  • Each character in the substring can be changed at most once per query.
  • You cannot reuse changes between queries; each query is independent.
  • The string s consists only of lowercase English letters.
  • The number of queries can be large, so efficiency matters.

Thought Process

At first glance, it seems we need to check for each substring if it can be rearranged (with changes) into a palindrome, considering the allowed number of modifications k. The naive approach is to, for each query, extract the substring, count letter frequencies, and check how many changes are needed. However, this would be slow for large numbers of queries because substring extraction and frequency counting are both costly.

To optimize, we notice that for a substring to be a palindrome, at most one character can have an odd count (for odd-length substrings), and all must be even for even-length substrings. If there are more than one odd-count characters, we must change some of them. The minimum number of changes needed is half the number of odd-count characters (since we can pair them up).

To efficiently compute character counts for any substring, we can use prefix sums (or prefix bitmasks) to preprocess the string, enabling O(1) queries for character parity.

Solution Approach

  • Preprocessing:
    • We build a prefix parity array (or bitmask), where each position i stores a bitmask representing the parity (even/odd) of character counts up to index i in s.
    • For each character, toggle its corresponding bit in the bitmask.
  • Answering Queries:
    • For each query [left, right, k], compute the parity mask for the substring by XOR-ing the prefix masks at right+1 and left.
    • Count the number of set bits in the result; this gives the number of characters with odd counts in the substring.
    • The minimum number of changes needed is odd_count // 2, because each change can fix two odd-count characters by pairing them.
    • If odd_count // 2 ≤ k, the substring can be converted into a palindrome with at most k changes.
  • Why Bitmask?
    • It allows us to represent the parity of all 26 characters in a single integer, and XOR-ing gives us the difference in parity between two positions.
    • Bit operations are very fast and memory-efficient.

Example Walkthrough

Suppose s = "abcda" and queries = [[0,4,1], [1,2,0]].

  • First, build the prefix bitmask:
    • Start with 0 (all even counts).
    • At each character, toggle its bit.
    • Resulting prefix masks: [0, 1, 3, 7, 15, 14] (in binary: 00000, 00001, 00011, 00111, 01111, 01110)
  • Query 1: [0,4,1]
    • Substring = "abcda"
    • Parity = prefix[5] ^ prefix[0] = 14 ^ 0 = 14 (01110)
    • There are 3 set bits (b, c, d have odd count).
    • Minimum changes needed = 3 // 2 = 1
    • Since k=1, answer is True.
  • Query 2: [1,2,0]
    • Substring = "bc"
    • Parity = prefix[3] ^ prefix[1] = 7 ^ 1 = 6 (00110)
    • There are 2 set bits (b, c have odd count).
    • Minimum changes needed = 2 // 2 = 1
    • Since k=0, answer is False.

Time and Space Complexity

  • Brute-force approach:
    • For each query, extract substring and count frequencies: O(L) per query, where L is substring length.
    • Total time: O(Q * L), can be very slow if Q and L are large.
  • Optimized approach:
    • Preprocessing: O(N), where N is the length of s (to build prefix mask).
    • Each query: O(1) (bitmask XOR and counting set bits, which is at most 26).
    • Total time: O(N + Q).
    • Space: O(N) for prefix mask array.

Summary

The key insight is that the number of character pairs with odd counts determines the minimum changes needed to form a palindrome. By using prefix bitmasks, we can answer each query in constant time after a linear-time preprocessing step. This approach is both elegant and efficient, leveraging bitwise operations for fast substring analysis, and avoids redundant computation by precomputing parity information.

Code Implementation

class Solution:
    def canMakePaliQueries(self, s, queries):
        n = len(s)
        prefix = [0] * (n + 1)
        for i in range(n):
            prefix[i+1] = prefix[i] ^ (1 << (ord(s[i]) - ord('a')))
        res = []
        for left, right, k in queries:
            mask = prefix[right+1] ^ prefix[left]
            odd_count = bin(mask).count('1')
            res.append((odd_count // 2) <= k)
        return res
      
class Solution {
public:
    vector<bool> canMakePaliQueries(string s, vector<vector<int>>& queries) {
        int n = s.size();
        vector<int> prefix(n + 1, 0);
        for (int i = 0; i < n; ++i) {
            prefix[i+1] = prefix[i] ^ (1 << (s[i] - 'a'));
        }
        vector<bool> res;
        for (auto& q : queries) {
            int left = q[0], right = q[1], k = q[2];
            int mask = prefix[right+1] ^ prefix[left];
            int odd_count = __builtin_popcount(mask);
            res.push_back((odd_count / 2) <= k);
        }
        return res;
    }
};
      
class Solution {
    public List<Boolean> canMakePaliQueries(String s, int[][] queries) {
        int n = s.length();
        int[] prefix = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            prefix[i+1] = prefix[i] ^ (1 << (s.charAt(i) - 'a'));
        }
        List<Boolean> res = new ArrayList<>();
        for (int[] q : queries) {
            int left = q[0], right = q[1], k = q[2];
            int mask = prefix[right+1] ^ prefix[left];
            int odd_count = Integer.bitCount(mask);
            res.add((odd_count / 2) <= k);
        }
        return res;
    }
}
      
var canMakePaliQueries = function(s, queries) {
    const n = s.length;
    const prefix = new Array(n + 1).fill(0);
    for (let i = 0; i < n; ++i) {
        prefix[i+1] = prefix[i] ^ (1 << (s.charCodeAt(i) - 97));
    }
    const res = [];
    for (const [left, right, k] of queries) {
        const mask = prefix[right+1] ^ prefix[left];
        let odd_count = 0;
        let m = mask;
        while (m) {
            odd_count += m & 1;
            m >>= 1;
        }
        res.push(Math.floor(odd_count / 2) <= k);
    }
    return res;
};