Hard

题目描述

给你一个回文字符串 s 和一个整数 k

返回 s 的第 k 小字典序回文排列。如果不同的回文排列少于 k 个,返回空字符串。

注意:产生相同回文字符串的不同重排被视为相同,只计算一次。

示例 1:

输入:s = "abba", k = 2
输出:"baab"

解释:
"abba" 的两个不同回文重排是 "abba" 和 "baab"。
按字典序,"abba" 在 "baab" 之前。由于 k = 2,输出是 "baab"。

示例 2:

输入:s = "aa", k = 2
输出:""

解释:
只有一个回文重排:"aa"。
由于 k = 2 超过了可能重排的数量,输出空字符串。

示例 3:

输入:s = "bacab", k = 1
输出:"abcba"

解释:
"bacab" 的两个不同回文重排是 "abcba" 和 "bacab"。
按字典序,"abcba" 在 "bacab" 之前。由于 k = 1,输出是 "abcba"。

约束条件:

  • 1 <= s.length <= 10^4
  • s 由小写英文字母组成
  • s 保证是回文的
  • 1 <= k <= 10^6

解题思路

这道题需要找到回文字符串的第 k 小字典序排列。关键思路如下:

核心观察:

  1. 对于回文字符串,只需要确定前一半字符,后一半由对称性决定
  2. 统计每个字符的频率,用频率的一半来构造前半部分
  3. 使用组合数学计算每个位置选择特定字符后的排列数量

算法步骤:

  1. 统计字符频率,计算前半部分长度 len = n/2
  2. 对于前半部分的每个位置,从小到大尝试每个可用字符
  3. 计算选择当前字符后剩余位置的排列数(使用多项式系数)
  4. 如果排列数 ≥ k,选择该字符;否则减去排列数继续尝试下一个字符
  5. 构造完前半部分后,利用对称性生成完整回文

组合数学关键: 使用多项式系数计算排列数:n! / (c1! × c2! × ... × cm!),其中 ci 是字符 i 的剩余数量。

时间复杂度主要在于计算阶乘和组合数,需要预处理阶乘表以优化性能。

代码实现

class Solution {
public:
    string smallestPalindrome(string s, int k) {
        int n = s.length();
        vector<int> count(26, 0);
        
        for (char c : s) {
            count[c - 'a']++;
        }
        
        // Use half counts for front half
        for (int i = 0; i < 26; i++) {
            count[i] /= 2;
        }
        
        int len = n / 2;
        
        // Precompute factorials
        vector<long long> fact(len + 1);
        fact[0] = 1;
        for (int i = 1; i <= len; i++) {
            fact[i] = fact[i - 1] * i;
        }
        
        string front = "";
        
        for (int pos = 0; pos < len; pos++) {
            bool found = false;
            for (int ch = 0; ch < 26; ch++) {
                if (count[ch] == 0) continue;
                
                count[ch]--;
                int remaining = len - pos - 1;
                
                // Calculate multinomial coefficient
                long long ways = fact[remaining];
                for (int i = 0; i < 26; i++) {
                    for (int j = 0; j < count[i]; j++) {
                        ways /= (j + 1);
                    }
                }
                
                if (ways >= k) {
                    front += (char)('a' + ch);
                    found = true;
                    break;
                } else {
                    k -= ways;
                    count[ch]++;
                }
            }
            
            if (!found) return "";
        }
        
        string back = front;
        reverse(back.begin(), back.end());
        
        if (n % 2 == 1) {
            // Find the middle character
            for (int i = 0; i < 26; i++) {
                if (count[i] > 0) {
                    front += (char)('a' + i);
                    break;
                }
            }
        }
        
        return front + back;
    }
};
class Solution:
    def smallestPalindrome(self, s: str, k: int) -> str:
        from collections import Counter
        import math
        
        n = len(s)
        count = Counter(s)
        
        # Use half counts for front half
        for ch in count:
            count[ch] //= 2
        
        length = n // 2
        
        # Precompute factorials
        fact = [1] * (length + 1)
        for i in range(1, length + 1):
            fact[i] = fact[i - 1] * i
        
        front = ""
        
        for pos in range(length):
            found = False
            for ch in sorted(count.keys()):
                if count[ch] == 0:
                    continue
                
                count[ch] -= 1
                remaining = length - pos - 1
                
                # Calculate multinomial coefficient
                ways = fact[remaining]
                for cnt in count.values():
                    ways //= math.factorial(cnt)
                
                if ways >= k:
                    front += ch
                    found = True
                    break
                else:
                    k -= ways
                    count[ch] += 1
            
            if not found:
                return ""
        
        back = front[::-1]
        
        if n % 2 == 1:
            # Find the middle character
            for ch in sorted(count.keys()):
                if count[ch] > 0:
                    front += ch
                    break
        
        return front + back
public class Solution {
    public string SmallestPalindrome(string s, int k) {
        int n = s.Length;
        int[] count = new int[26];
        
        foreach (char c in s) {
            count[c - 'a']++;
        }
        
        // Use half counts for front half
        for (int i = 0; i < 26; i++) {
            count[i] /= 2;
        }
        
        int len = n / 2;
        
        // Precompute factorials
        long[] fact = new long[len + 1];
        fact[0] = 1;
        for (int i = 1; i <= len; i++) {
            fact[i] = fact[i - 1] * i;
        }
        
        string front = "";
        
        for (int pos = 0; pos < len; pos++) {
            bool found = false;
            for (int ch = 0; ch < 26; ch++) {
                if (count[ch] == 0) continue;
                
                count[ch]--;
                int remaining = len - pos - 1;
                
                // Calculate multinomial coefficient
                long ways = fact[remaining];
                for (int i = 0; i < 26; i++) {
                    for (int j = 0; j < count[i]; j++) {
                        ways /= (j + 1);
                    }
                }
                
                if (ways >= k) {
                    front += (char)('a' + ch);
                    found = true;
                    break;
                } else {
                    k -= (int)ways;
                    count[ch]++;
                }
            }
            
            if (!found) return "";
        }
        
        char[] backArray = front.ToCharArray();
        Array.Reverse(backArray);
        string back = new string(backArray);
        
        if (n % 2 == 1) {
            // Find the middle character
            for (int i = 0; i < 26; i++) {
                if (count[i] > 0) {
                    front += (char)('a' + i);
                    break;
                }
            }
        }
        
        return front + back;
    }
}
var smallestPalindrome = function(s, k) {
    const n = s.length;
    const freq = {};
    
    // Count frequency of each character
    for (let char of s) {
        freq[char] = (freq[char] || 0) + 1;
    }
    
    // Get characters that appear in pairs and the middle character (if any)
    const pairs = [];
    let middle = '';
    
    for (let char of Object.keys(freq).sort()) {
        const count = freq[char];
        for (let i = 0; i < Math.floor(count / 2); i++) {
            pairs.push(char);
        }
        if (count % 2 === 1) {
            middle = char;
        }
    }
    
    // Calculate total number of palindromic permutations
    const factorial = (num) => {
        let result = 1n;
        for (let i = 2; i <= num; i++) {
            result *= BigInt(i);
        }
        return result;
    };
    
    let total = factorial(pairs.length);
    const charCount = {};
    for (let char of pairs) {
        charCount[char] = (charCount[char] || 0) + 1;
    }
    
    for (let count of Object.values(charCount)) {
        total /= factorial(count);
    }
    
    if (BigInt(k) > total) {
        return "";
    }
    
    // Generate the k-th permutation of the first half
    const result = [];
    const available = [...pairs];
    let remaining = BigInt(k - 1);
    
    while (available.length > 0) {
        // Count frequency of remaining characters
        const remainingFreq = {};
        for (let char of available) {
            remainingFreq[char] = (remainingFreq[char] || 0) + 1;
        }
        
        const uniqueChars = Object.keys(remainingFreq).sort();
        
        for (let char of uniqueChars) {
            // Calculate permutations if we choose this character
            const tempAvailable = available.slice();
            const idx = tempAvailable.indexOf(char);
            tempAvailable.splice(idx, 1);
            
            let perms = factorial(tempAvailable.length);
            const tempCharCount = {};
            for (let c of tempAvailable) {
                tempCharCount[c] = (tempCharCount[c] || 0) + 1;
            }
            for (let count of Object.values(tempCharCount)) {
                perms /= factorial(count);
            }
            
            if (remaining < perms) {
                result.push(char);
                available.splice(idx, 1);
                break;
            }
            remaining -= perms;
        }
    }
    
    // Build the palindrome
    const firstHalf = result.join('');
    const secondHalf = result.slice().reverse().join('');
    
    return firstHalf + middle + secondHalf;
};

复杂度分析

复杂度类型
时间复杂度O(n² × 26)
空间复杂度O(n)

其中 n 是字符串长度。时间复杂度主要来源于每个位置计算多项式系数的过程。