Smallest Palindromic Rearrangement II
HardPrompt
You are given a palindromic string s and an integer k.
Return the k-th lexicographically smallest palindromic permutation of s. If there are fewer than k distinct palindromic permutations, return an empty string.
Note: Different rearrangements that yield the same palindromic string are considered identical and are counted once.
Example 1:
Input: s = "abba", k = 2
Output: "baab"
Explanation:
- The two distinct palindromic rearrangements of
"abba"are"abba"and"baab". - Lexicographically,
"abba"comes before"baab". Sincek = 2, the output is"baab".
Example 2:
Input: s = "aa", k = 2
Output: ""
Explanation:
- There is only one palindromic rearrangement:
"aa". - The output is an empty string since
k = 2exceeds the number of possible rearrangements.
Example 3:
Input: s = "bacab", k = 1
Output: "abcba"
Explanation:
- The two distinct palindromic rearrangements of
"bacab"are"abcba"and"bacab". - Lexicographically,
"abcba"comes before"bacab". Sincek = 1, the output is"abcba".
Constraints:
1 <= s.length <= 104sconsists of lowercase English letters.sis guaranteed to be palindromic.1 <= k <= 106
Approaches
2 approaches with complexity analysis and trade-offs.
This approach is a straightforward brute-force method. The core idea is that any palindromic rearrangement is defined by the first half of the string. We first determine the characters that make up this first half (for "abba", it's "ab"; for "bacab", it's "ab"). Then, we generate every possible unique arrangement (permutation) of this half-string. After generating all of them, we sort them lexicographically and pick the k-th one. Finally, we construct the full palindrome from this k-th half-string.
Algorithm
- Count Character Frequencies: Traverse the input string
sto count the occurrences of each character. - Form Half-String and Middle Character: Based on the counts, construct the 'half-string'. For each character
cwith countn, the half-string will containn / 2instances ofc. If there's a character with an odd count, that will be the middle character of the palindrome. - Generate All Permutations: Use a recursive backtracking algorithm to generate all unique permutations of the half-string.
- Store and Sort: Store all generated unique permutations in a list.
- Sort Lexicographically: Sort the list of permutations in lexicographical order.
- Select k-th Permutation: If
kis larger than the number of unique permutations, it's impossible to find the k-th one, so return an empty string. Otherwise, retrieve the permutation at indexk-1from the sorted list. - Construct Final Palindrome: Take the selected half-string permutation, append the middle character (if any), and then append the reverse of the half-string permutation to form the final palindromic string.
Walkthrough
The algorithm begins by analyzing the character composition of the input string s. Since s is a palindrome, each character appears an even number of times, with at most one character appearing an odd number of times. This odd-count character will be the center of any new palindrome.
The characters for the first half of the new palindrome are determined by taking half of the count of each character. For example, if s = "aabbaa", the half-string will be composed of three 'a's and one 'b'.
We then use a standard backtracking algorithm to find all unique permutations of this half-string. A frequency map (or an array) is used to keep track of available characters to avoid duplicates in the generation process.
Once all unique permutations are generated and stored, the list is sorted. If the list size is less than k, we return "". Otherwise, we select the (k-1)-th element. This element, combined with the middle character and its own reverse, forms the desired k-th smallest palindromic permutation.
import java.util.*; class Solution { List<String> permutations = new ArrayList<>(); public String kthSmallestPalindrome(String s, int k) { int[] counts = new int[26]; for (char c : s.toCharArray()) { counts[c - 'a']++; } String mid = ""; StringBuilder half = new StringBuilder(); for (int i = 0; i < 26; i++) { if (counts[i] % 2 != 0) { mid += (char) ('a' + i); } for (int j = 0; j < counts[i] / 2; j++) { half.append((char) ('a' + i)); } } int[] halfCounts = new int[26]; for(char c : half.toString().toCharArray()) { halfCounts[c - 'a']++; } generatePermutations(new StringBuilder(), half.length(), halfCounts); Collections.sort(permutations); if (k > permutations.size()) { return ""; } String halfPerm = permutations.get(k - 1); String reversedHalf = new StringBuilder(halfPerm).reverse().toString(); return halfPerm + mid + reversedHalf; } private void generatePermutations(StringBuilder current, int len, int[] counts) { if (current.length() == len) { permutations.add(current.toString()); return; } for (int i = 0; i < 26; i++) { if (counts[i] > 0) { counts[i]--; current.append((char) ('a' + i)); generatePermutations(current, len, counts); current.deleteCharAt(current.length() - 1); counts[i]++; } } }}Complexity
Time
O(P * L + P log P), where `P` is the number of unique permutations of the half-string and `L` is its length. Generating all permutations is the dominant factor. For a half-string of length `L` with distinct characters, `P` would be `L!`, which grows extremely fast.
Space
O(P * L), where `P` is the number of unique permutations and `L` is the length of the half-string. This is because we need to store all `P` permutations, each of length `L`.
Trade-offs
Pros
Conceptually simple and easy to follow.
Cons
Extremely high time complexity, making it infeasible for the given constraints.
High space complexity due to storing all permutations.
This efficient approach avoids generating all permutations by directly constructing the k-th one mathematically. It builds the permutation of the half-string from left to right. At each position, it determines which character should be placed by calculating how many permutations would follow if we chose a particular character. By comparing this count with the current value of k, it decides whether to pick that character or to skip that block of permutations and try the next character.
Algorithm
- Character Analysis: Count character frequencies in
s. Determine the characters for the half-string (half_counts) and the middle character (mid_char). Let the length of the half-string beL. - Permutation Count Function: Create a helper function,
countPerms(counts, len, limit), to calculate the number of unique permutations of a multiset. This function should use combinatorics (C(n, k)) and cap the result atlimitto avoid overflow and for efficiency. - Feasibility Check: Use
countPermsto calculate the total number of unique permutations of the half-string. If this total is less thank, no solution exists, so return an empty string. - Iterative Construction: Build the k-th permutation of the half-string,
half_perm, character by character from left to right (forifrom 0 toL-1). - Character Selection: For each position
i, iterate through possible characterscfrom 'a' to 'z'. - Lookahead: If character
cis available (its count > 0), tentatively place it at positioni. Then, calculate how many permutations (p) can be formed with the remaining characters usingcountPerms. - Decision:
- If
k <= p, it means the desired permutation lies within this group. Fixcas the character for positioni, append it tohalf_perm, update the character counts, and proceed to the next position (i+1). - If
k > p, the desired permutation is not in this group. Skip over theseppermutations by updatingk = k - p, and try the next available character for the current positioni.
- If
- Final Assembly: Once the
half_permof lengthLis constructed, create the full palindrome:half_perm + mid_char + reverse(half_perm).
Walkthrough
The algorithm leverages a standard technique for finding the k-th lexicographical permutation of a multiset. First, we process the input string s to get the character counts for the first half of the palindrome (half_counts) and the potential middle character.
The crucial part is a helper function that can calculate the number of permutations for a given set of character counts. This is calculated using multinomial coefficients: n! / (c1! * c2! * ...) where n is the total number of items and ci are the counts of each unique item. To handle potentially huge numbers and to optimize, this calculation is capped at k, as any count larger than k is equivalent for our decision-making process.
We initialize k to k-1 for 0-based indexing. Then, we determine the first character of our target permutation. We try the smallest character, 'a'. We calculate how many permutations start with 'a'. If this count is greater than k, we know our target permutation must also start with 'a'. If the count is less than or equal to k, we subtract this count from k and try the next character, 'b'. This process is repeated for each position in the half-string, progressively narrowing down k and building the result. The small value of k is a key constraint that makes this approach very fast in practice, as the number of permutations of the remaining string quickly exceeds k, meaning for most of the initial positions, we will simply pick the lexicographically smallest available character.
import java.util.Arrays; class Solution { long[][] C; public String kthSmallestPalindrome(String s, int k) { int[] counts = new int[26]; for (char c : s.toCharArray()) { counts[c - 'a']++; } String mid = ""; int[] halfCounts = new int[26]; int halfLen = 0; for (int i = 0; i < 26; i++) { if (counts[i] % 2 != 0) { mid += (char) ('a' + i); } halfCounts[i] = counts[i] / 2; halfLen += halfCounts[i]; } if (halfLen == 0) { return k == 1 ? mid : ""; } // Precompute combinations C(n, k) up to halfLen // Capped at k to prevent overflow and for efficiency C = new long[halfLen + 1][halfLen + 1]; for (int i = 0; i <= halfLen; i++) { C[i][0] = 1; for (int j = 1; j <= i; j++) { C[i][j] = C[i - 1][j - 1] + C[i - 1][j]; if (C[i][j] > k) C[i][j] = k; // Cap at k } } long totalPerms = countPerms(halfCounts, halfLen, k); if (totalPerms < k) { return ""; } StringBuilder halfPerm = new StringBuilder(); int currentLen = halfLen; long kRem = k; for (int i = 0; i < halfLen; i++) { for (int j = 0; j < 26; j++) { if (halfCounts[j] > 0) { halfCounts[j]--; long perms = countPerms(halfCounts, currentLen - 1, kRem); if (kRem <= perms) { halfPerm.append((char) ('a' + j)); currentLen--; break; } else { kRem -= perms; halfCounts[j]++; } } } } String reversedHalf = new StringBuilder(halfPerm).reverse().toString(); return halfPerm.toString() + mid + reversedHalf; } private long countPerms(int[] counts, int len, long limit) { if (len == 0) return 1; long res = 1; int remLen = len; for (int count : counts) { if (count > 0) { long combinations = C[remLen][count]; // Check for overflow before multiplication if (combinations > 0 && res > limit / combinations) { return limit; } res *= combinations; if (res >= limit) return limit; remLen -= count; } } return res; }}Complexity
Time
The worst-case complexity appears to be O(d * L^2), where `d` is the alphabet size and `L` is the half-string length. However, because `k` is small (`<= 10^6`), the permutation counts calculated in each step quickly exceed `k`. This causes the `countPerms` function to return early. The practical time complexity is much better, closer to O(d^2 * L), which is efficient enough for the given constraints.
Space
O(L^2) for precomputing combinations, where `L` is the half-string length. If not precomputed, space is O(d) or O(1) for character counts, where `d` is the alphabet size (26).
Trade-offs
Pros
Highly efficient, capable of handling large inputs within typical time limits.
Constant space complexity (relative to input size), as it doesn't store large intermediate structures.
Cons
More complex to implement due to the need for combinatorial calculations.
Requires careful handling of large numbers, even with capping, to prevent overflow and precision issues.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.