LeetCode 3518. 最小回文排列 II 的核心思路是:回文串由前半部分唯一确定(奇数长度时中间字符固定),因此问题转化为求前半部分字符串的第 k 小不同排列。

具体做法:
1. 提取前半部分 `half` 和中间字符 `mid`(若有)。
2. 用组合数连乘计算多重集排列数(避免阶乘溢出):`C(n, v1) * C(n-v1, v2) * ...`。由于 `k ≤ 10^6`,当乘积超过 `1_000_001` 时直接截断。
3. 若总排列数 `< k`,返回空串。
4. 逐位贪心构造:从小到大枚举 `'a'~'z'`,尝试将当前字符填入该位,计算剩余位置的排列数 `ways`。若 `k ≤ ways`,则确定该位;否则 `k -= ways` 并尝试下一个字符。
5. 最后将构造好的左半部分翻转拼接。

时间复杂度 `O(|half| * 26 * log(k))`,空间复杂度 `O(1)`。

```rust
impl Solution {
    const MAX_K: i64 = 1_000_001;

    pub fn smallest_palindrome(s: String, k: i32) -> String {
        let mut k = k as i64;
        let n = s.len();
        let half = &s[..n / 2];
        let mid = if n % 2 == 0 { "" } else { &s[n / 2..n / 2 + 1] };

        let mut cnt = [0i32; 26];
        for c in half.chars() {
            cnt[(c as u8 - b'a') as usize] += 1;
        }

        // 总排列数不足 k
        if Self::count_arrangements(&cnt) < k {
            return String::new();
        }

        let half_len = half.len();
        let mut ans = String::with_capacity(half_len);

        for _ in 0..half_len {
            for i in 0..26 {
                if cnt[i] == 0 {
                    continue;
                }
                // 尝试填入字符 i
                cnt[i] -= 1;
                let ways = Self::count_arrangements(&cnt);
                if k <= ways {
                    ans.push((b'a' + i as u8) as char);
                    break;
                } else {
                    // 不在该分支,恢复并跳过
                    cnt[i] += 1;
                    k -= ways;
                }
            }
        }

        let rev: String = ans.chars().rev().collect();
        ans + mid + &rev
    }

    /// 计算多重集排列数,超过 MAX_K 时截断
    fn count_arrangements(cnt: &[i32; 26]) -> i64 {
        let mut total = 0i32;
        for &c in cnt.iter() {
            total += c;
        }
        let mut res: i64 = 1;
        for &c in cnt.iter() {
            if c == 0 {
                continue;
            }
            res *= Self::comb(total, c);
            if res >= Self::MAX_K {
                return Self::MAX_K;
            }
            total -= c;
        }
        res
    }

    /// 组合数 C(n, k),超过 MAX_K 时截断
    fn comb(n: i32, k: i32) -> i64 {
        let n = n as i64;
        let k = k as i64;
        let k = k.min(n - k);
        let mut res: i64 = 1;
        for i in 1..=k {
            res = res * (n - i + 1) / i;
            if res >= Self::MAX_K {
                return Self::MAX_K;
            }
        }
        res
    }
}
```

关键点说明:
- `count_arrangements` 将多重集排列分解为依次选位置的组合数连乘,避免了先算大阶乘再除法的溢出问题。
- `comb` 利用 `C(n,k) = C(n,k-1) * (n-k+1) / k` 递推,并取 `k = min(k, n-k)` 减少循环次数,同时保证中间结果始终可被整除。
- 截断值 `MAX_K = 1_000_001` 略大于题目 `k` 的上限,既防溢出又不会影响正确判断。

 

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐