[LeetCode 1177] Can Make Palindrome from Substring
Given a string s
, we make queries on substrings of s
.
For each query queries[i] = [left, right, k]
, we may rearrange the substring s[left], ..., s[right]
, and then choose up to k
of them to replace with any lowercase English letter.
If the substring is possible to be a palindrome string after the operations above, the result of the query is true
. Otherwise, the result is false
.
Return an array answer[]
, where answer[i]
is the result of the i
-th query queries[i]
.
Note that: Each letter is counted individually for replacement so if for example s[left..right] = "aaa"
, and k = 2
, we can only replace two of the letters. (Also, note that the initial string s
is never modified by any query.)
Example :
Input: s = "abcda", queries = [[3,3,0],[1,2,0],[0,3,1],[0,3,2],[0,4,1]] Output: [true,false,false,true,true] Explanation: queries[0] : substring = "d", is palidrome. queries[1] : substring = "bc", is not palidrome. queries[2] : substring = "abcd", is not palidrome after replacing only 1 character. queries[3] : substring = "abcd", could be changed to "abba" which is palidrome. Also this can be changed to "baab" first rearrange it "bacd" then replace "cd" with "ab". queries[4] : substring = "abcda", could be changed to "abcba" which is palidrome.
Because we can freely rearrange any letters in a given substring, for a query on s[i, j], its result is determined by the relation between the parity sum of all letters in s[i, j] and k. If ParitySum / 2 <= k, the result is true; otherwise the result is false. Another property of this problem is that since there are only at most 26 unique letters, if k >= 13, the query result will always be true. We can use this to prune queries with k >= 13.
There are a few solutions to solve this problem.
Solution 1. Binary Search; O(s.length + queries.length * 26 * log(s.length)) runtime; O(s.length) space
1. For each letter from a to z, create a sorted list of their index in s.
2. For each query on s[i, j], find the total sum of letters whose ocurrence count is odd. Do a binary search on each letter's index list to find the occurence count.
3. if sum / 2 <= k, query returns true otherwise false.
class Solution { public List<Boolean> canMakePaliQueries(String s, int[][] queries) { List<Boolean> res = new ArrayList<>(); List<Integer>[] indices = new List[26]; for(int i = 0; i < 26; i++) { indices[i] = new ArrayList<>(); } //O(s.length) for(int i = 0; i < s.length(); i++) { indices[s.charAt(i) - 'a'].add(i); } //O(queries.length * 26 * log(s.length)) for(int i = 0; i < queries.length; i++) { res.add(canMake(indices, queries[i][0], queries[i][1], queries[i][2])); } return res; } private boolean canMake(List[] indices, int left, int right, int k) { int sum = 0; //O(26 * log(s.length)) for(int i = 0; i < 26; i++) { List<Integer> list = indices[i]; int r = getRightBound(list, right); int l = getLeftBound(list, left); if(l >= 0 && r >= 0 && l <= r) { sum += (r -l + 1) % 2; } } return sum / 2 <= k; } private int getLeftBound(List<Integer> list, int target) { if(list.size() == 0) { return -1; } int left = 0, right = list.size() - 1; while(left < right - 1) { int mid = left + (right - left) / 2; if(list.get(mid) < target) { left = mid + 1; } else { right = mid; } } if(list.get(left) >= target) { return left; } else if(list.get(right) >= target) { return right; } return -1; } private int getRightBound(List<Integer> list, int target) { if(list.size() == 0) { return -1; } int left = 0, right = list.size() - 1; while(left < right - 1) { int mid = left + (right - left) / 2; if(list.get(mid) > target) { right = mid - 1; } else { left = mid; } } if(list.get(right) <= target) { return right; } else if(list.get(left) <= target) { return left; } return -1; } }
Solution 2. TreeMap; Same idea with Solution 1 but use TreeMap to find the occurence count of a letter in s[i, j].
Each treemap stores the mapping relation from a letter's index in s to this index's relative position in current letter's occurence throughout s.
class Solution { public List<Boolean> canMakePaliQueries(String s, int[][] queries) { List<Boolean> res = new ArrayList<>(); TreeMap<Integer, Integer>[] indices = new TreeMap[26]; for(int i = 0; i < 26; i++) { indices[i] = new TreeMap<>(); } //O(s.length) for(int i = 0; i < s.length(); i++) { int idx = s.charAt(i) - 'a'; indices[idx].put(i, indices[idx].size()); } //O(queries.length * 26 * log(s.length)) for(int i = 0; i < queries.length; i++) { res.add(canMake(indices, queries[i][0], queries[i][1], queries[i][2])); } return res; } private boolean canMake(TreeMap[] indices, int left, int right, int k) { int sum = 0; //O(26 * log(s.length)) for(int i = 0; i < 26; i++) { TreeMap<Integer, Integer> map = indices[i]; Map.Entry<Integer, Integer> leftBound = map.ceilingEntry(left); Map.Entry<Integer, Integer> rightBound = map.floorEntry(right); if(leftBound != null && rightBound != null && leftBound.getValue() <= rightBound.getValue()) { sum += ((rightBound.getValue() -leftBound.getValue() + 1) % 2); } } return sum / 2 <= k; } }
Solution 3. PrefixSum; O(26 * s.length + 26 * queries.length) runtime; O(26 * s.length) space
1. Compute prefix sum of occurences for all 26 letters; prefixSum[i][j] represents the occurence count of letter 'a' + i in substring s[0, j]. To find out the occurence count of a letter in substring s[j1, j2], we do prefixSum[i][j2] - prefixSum[i][j1 - 1] or 0 if j1 == 0.
2. For each query, sum up the number of letters whose count is odd within the substring.
3. if sum / 2 <= k, query returns true otherwise false.
class Solution { public List<Boolean> canMakePaliQueries(String s, int[][] queries) { List<Boolean> res = new ArrayList<>(); int n = s.length(); int[][] prefixSum = new int[26][n]; for(int i = 0; i < n; i++) { int idx = s.charAt(i) - 'a'; for(int j = 0; j < 26; j++) { if(j == idx) { prefixSum[j][i] = (i == 0 ? 0 : prefixSum[j][i - 1]) + 1; } else { prefixSum[j][i] = (i == 0 ? 0 : prefixSum[j][i - 1]); } } } for(int i = 0; i < queries.length; i++) { if(queries[i][2] >= 13) { res.add(true); continue; } int sum = 0; for(int j = 0; j < 26; j++) { sum += (prefixSum[j][queries[i][1]] - (queries[i][0] == 0 ? 0 : prefixSum[j][queries[i][0] - 1])) % 2; } res.add(sum / 2 <= queries[i][2]); } return res; } }
Solution 4. Xor bitwise operation.
Same idea with prefix sum of each letter's occurence count in a substring. Because there are only 26 lower case English letters and we only care about the parity of each letter in a substring, we can use bitmap of an integer instead of using a 2D array for each letter. Each bit represents a letter's parity, 1 for odd, 0 for even. To get a letter's parity in s[i, j], we do prefixParity[j] ^ prefixParity[i - 1].
Why it works?
prefixParity[i]: the parity of all letters in s[0, i]. Given s[i, j], for each letter, we have two possible cases:
Either it has odd or even parity in both s[0, i] and s[0, j]. In this case, s[i, j] must have a parity of 0 for this letter. (even - even = even, odd - odd = even)
Or it does not have the same parity. In this case, s[i, j] must have a parity of 1 for this letter. (odd - even = odd, even - odd = odd)
For two bits, xor returns 1 if they are different, 0 otherwise.
class Solution { public List<Boolean> canMakePaliQueries(String s, int[][] queries) { List<Boolean> res = new ArrayList<>(); int n = s.length(); int[] prefixParity = new int[n]; prefixParity[0] = (1 << (s.charAt(0) - 'a')); for(int i = 1; i < n; i++) { prefixParity[i] = prefixParity[i - 1] ^ (1 << (s.charAt(i) - 'a')); } for(int i = 0; i < queries.length; i++) { if(queries[i][2] >= 13) { res.add(true); } else { int parity = queries[i][0] == 0 ? prefixParity[queries[i][1]] : prefixParity[queries[i][1]]^prefixParity[queries[i][0] - 1]; res.add(Integer.bitCount(parity) / 2 <= queries[i][2]); } } return res; } }