3518. Smallest Palindromic Rearrangement II
題目 / Problem
中文: 給你一個回文字串 s(正著讀和反著讀一樣)和一個整數 k。請回傳把 s 的字元重新排列後、仍然是回文的所有字串中,字典序第 k 小的那一個。如果不同的回文排列少於 k 個,回傳空字串 ""。注意:不同的重排若拼出同一個字串,只算一次。
English: You are given a palindromic string s (reads the same forwards and backwards) and an integer k. Return the k-th lexicographically smallest string that is a palindrome and uses exactly the same multiset of characters as s. If there are fewer than k distinct palindromic permutations, return "". Rearrangements that produce the same string are counted once.
Constraints:
- 1 <= s.length <= 10^4
- s consists of lowercase English letters
- s is guaranteed to be palindromic
- 1 <= k <= 10^6
Worked example: s = "abba", k = 2. The two distinct palindromes are "abba" and "baab". In lexicographic order that's abba (1st) then baab (2nd). Since k = 2, the answer is "baab".
名詞解釋 / Glossary
- 回文 / Palindrome: A string equal to its own reverse, e.g.
abcba. A palindrome is fully determined by its left half plus an optional single middle character. - 字典序 / Lexicographic order: Dictionary order — compare character by character from the left;
"aab" < "aba"because at the first differing positiona < b. - 排列 / Permutation: An arrangement of the given characters. "Distinct permutations" means we ignore identical results (swapping two equal letters changes nothing).
- 字元計數 / Frequency count: An array
cnt[26]wherecnt[c]is how many times lettercappears. Built by scanning the string once. - 多重集合排列數 / Multinomial coefficient: The number of distinct orderings of a multiset of length
rwith group sizesc_1,…,c_misr! / (c_1!·c_2!·…·c_m!). This counts palindrome halves for us. - 封頂 / Capping (clamping): These counts can be astronomically large, but
k ≤ 10^6. So whenever a running count exceeds10^6we just clamp it to10^6 + 1("big enough"). This keeps every number inside a 64-bit integer. - 貪心逐位構造 / Greedy digit-by-digit construction: Instead of listing all palindromes, we decide each position from left to right, always trying the smallest letter first and counting how many palindromes that choice would allow.
思路
中文: 最直接的想法是列出所有回文排列、排序、取第 k 個。但長度可到 10^4,回文數量是天文數字,完全不可行。關鍵觀察有兩點。第一,回文只由左半邊決定:右半邊是左半邊的鏡像,中間最多一個字元(出現奇數次的那個,因為 s 保證是回文,這種字元最多一個)。所以我們只需構造 floor(n/2) 個字元,每個字元可用的次數是 cnt[c] / 2。這樣「第 k 小的回文」就等價於「用這些半數字元、第 k 小的排列」。第二,要找第 k 小的排列,不必真的列舉:從最左位置開始,按字母 a 到 z 嘗試,假設這一位放字元 c,剩下的位置能組成的排列數就是一個多重集合排列數 r!/∏cnt!。若這個數量 ≥ k,代表第 k 個就落在「這一位是 c」的區間裡,於是固定 c;否則把 k 減掉這個數量,換下一個更大的字母。因為我們按字母由小到大掃,第一個滿足 ≥ k 的就是正確選擇。由於 k ≤ 10^6,計算排列數時只要一超過 10^6 就封頂成 10^6+1,所有數值都能安全放進 64 位整數,不會溢位。開始前先算左半邊的總排列數,若小於 k 直接回傳空字串。
English: The brute force — enumerate every palindromic permutation, sort, take the k-th — is hopeless because with length up to 10^4 the count is enormous. Two observations unlock the problem. First, a palindrome is determined entirely by its left half: the right half is the mirror, and there is at most one middle character (the one appearing an odd number of times; since s is guaranteed palindromic there is at most one such character). So we only need to build floor(n/2) characters, where each letter c is available cnt[c] / 2 times. Finding the k-th smallest palindrome is exactly finding the k-th smallest arrangement of this half-multiset. Second, we never enumerate: we build the half left to right. At each position we try letters a…z; if we tentatively place letter c, the number of ways to fill the remaining positions is the multinomial r!/∏cnt!. If that count is ≥ k, the k-th arrangement starts with c, so we fix it; otherwise we subtract the count from k and move to the next larger letter. Scanning smallest-first guarantees the first letter that satisfies ≥ k is the correct one. Because k ≤ 10^6, we cap every count at 10^6 + 1 the moment it exceeds 10^6, keeping all arithmetic safely inside 64-bit integers. Before building, we compute the total number of half-arrangements; if it is below k, we immediately return the empty string.
逐步走查 / Walkthrough
Input s = "abba", k = 2. Counts: a:2, b:2. Half counts half = {a:1, b:1}, no odd character, half length h = 2. Total half-arrangements = 2!/(1!·1!) = 2 ≥ k, so an answer exists.
| Step / 步驟 | Position | Try letter | half before try |
ways = perms of the rest | Decision / 決定 | k after |
|---|---|---|---|---|---|---|
| 1 | pos 0 | a |
{a:1,b:1} |
place a → {a:0,b:1} → 1!/(1!) = 1 |
1 ≥ 2? No → k -= 1, undo |
1 |
| 2 | pos 0 | b |
{a:1,b:1} |
place b → {a:1,b:0} → 1!/(1!) = 1 |
1 ≥ 1? Yes → fix b |
1 |
| 3 | pos 1 | a |
{a:1,b:0} |
place a → {} → 0! = 1 |
1 ≥ 1? Yes → fix a |
1 |
Left half built = "ba". No middle character. Right half = reverse of left = "ab". Full palindrome = "ba" + "ab" = "baab". ✅ Matches the expected output.
Solution — C
#include <stdlib.h>
#include <string.h>
// 演算法 / Algorithm:
// 回文由左半邊決定;只構造 n/2 個字元。從左到右、字母由小到大貪心,
// 每步用「多重集合排列數」數出剩餘排列數,決定第 k 個落在哪個字母。
// A palindrome is fixed by its left half; build only n/2 chars. Greedily pick
// each position (smallest letter first), using multinomial counts to locate the k-th.
// 計算 r!/∏cnt[c]!(r = cnt 總和),一超過 cap 就封頂回傳 cap+1。
// Compute r!/∏cnt[c]! (r = sum of cnt), clamped to cap+1 once it exceeds cap.
static long long multinomial(int *cnt, long long cap) {
long long perm = 1; // 目前的排列數 / running permutation count
long long used = 0; // 已放入的字元總數 / how many items placed so far
for (int c = 0; c < 26; c++) { // 掃過每個字母 / for each letter
for (int j = 1; j <= cnt[c]; j++) { // 逐一加入該字母 / add its copies one by one
used++; // 多放一個字元 / one more item placed
// perm * used / j 逐步搭出組合數,每步都是整數,不會有分數。
// perm * used / j builds binomial products; each step stays an exact integer.
perm = perm * used / j;
if (perm > cap) return cap + 1; // 太大就封頂 / clamp when it grows past cap
}
}
return perm; // 未超過 cap 的精確值 / exact value under cap
}
// LeetCode 函式簽名 / LeetCode signature
char* smallestPalindrome(char* s, int k) {
int n = strlen(s); // 字串長度 / length of s
int cnt[26] = {0}; // 每個字母出現次數 / frequency of each letter
for (int i = 0; i < n; i++) cnt[s[i] - 'a']++; // s[i]-'a' 把字母映成 0..25 / map letter to index
int half[26]; // 左半邊可用次數 / usable counts for the left half
int oddChar = -1; // 出現奇數次的字母(中間) / the odd-count letter (middle)
for (int c = 0; c < 26; c++) {
if (cnt[c] & 1) oddChar = c; // &1 判斷是否為奇數 / bitwise test for oddness
half[c] = cnt[c] / 2; // 半數字元 / half of each count
}
int h = n / 2; // 左半邊長度 / length of the left half
long long cap = 1000000; // k <= 1e6,封頂上限 / cap since k <= 1e6
char *res = malloc(n + 1); // 配置輸出空間,+1 給結尾 '