UVa 11027 Palindromic Permutation
题目描述
给定一个由小写字母组成的字符串,我们可以对其字符进行排列,生成新的字符串,并将这些字符串按字典序排序。在这些排列中,我们需要找出第 nnn 个回文排列(按字典序排序后的回文串列表中的第 nnn 个)。如果回文排列的总数小于 nnn,则输出 "XXX"。
示例
输入:
3
abba 1
abba 2
abba 3
输出:
Case 1: abba
Case 2: baab
Case 3: XXX
题目分析
1. 回文排列的性质
一个字符串能够排列成回文串,必须满足以下条件之一:
- 如果字符串长度为偶数,那么每个字符的出现次数必须都是偶数。
- 如果字符串长度为奇数,那么恰好有一个字符的出现次数为奇数,其余字符的出现次数均为偶数。
如果上述条件不满足,则无法形成任何回文排列,直接输出 "XXX"。
2. 回文排列的构造方法
对于能够形成回文排列的字符串,我们可以将其分成两半:
- 左半部分:由每个字符取 出现次数2\frac{\text{出现次数}}{2}2出现次数 个组成。
- 中间字符(仅当字符串长度为奇数时):即出现次数为奇数的那个字符。
- 右半部分:左半部分的镜像反转。
因此,问题转化为:
- 检查字符串是否能形成回文。
- 构造左半部分的字符集合。
- 计算左半部分的不同排列数。
- 找到左半部分的第 nnn 个排列(按字典序)。
- 组合成完整的回文串。
3. 排列计数与生成
由于字符串长度最多为 303030,左半部分长度最多为 151515。虽然 15!15!15! 的数值很大,但由于字符可能重复,实际排列数会少很多。
我们可以使用递归 + 计数的方法,按位置依次确定字符,并计算以某前缀开头的排列数,从而直接生成第 nnn 个排列,而不需要枚举所有排列。
排列计数公式(含重复字符)
对于字符计数 cnt0,cnt1,…,cnt25cnt_0, cnt_1, \dots, cnt_{25}cnt0,cnt1,…,cnt25,左半部分的排列数为:
total=(∑cnti)!∏(cnti!) \text{total} = \frac{(\sum cnt_i)!}{\prod (cnt_i!)} total=∏(cnti!)(∑cnti)!
在递归生成第 kkk 个排列时,我们按字典序尝试每个可能的字符,计算如果当前位放置该字符,后面有多少种排列。如果 kkk 大于这个数,就跳过并减去;否则选定该字符,并递归到下一层。
解题思路
步骤概述
- 统计字符频率:记录每个字符的出现次数。
- 检查回文可行性:
- 如果长度为偶数,所有字符出现次数必须为偶数。
- 如果长度为奇数,恰好有一个字符出现次数为奇数。
- 构造左半部分:每个字符取 出现次数2\frac{\text{出现次数}}{2}2出现次数 个。
- 计算回文总数:即左半部分的不同排列数。
- 生成第 nnn 个回文:
- 如果 n>totaln > \text{total}n>total,输出
"XXX"。 - 否则,生成左半部分的第 nnn 个排列,并组合成完整回文。
- 如果 n>totaln > \text{total}n>total,输出
参考代码
// Palindromic Permutation
// UVa ID: 11027
// Verdict: Accepted
// Submission Date: 2025-11-04
// UVa Run Time: 0.000s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net#include <bits/stdc++.h>
using namespace std;typedef long long ll;// 计算阶乘
ll factorial[20];void init_factorial() {factorial[0] = 1;for (int i = 1; i <= 15; i++) {factorial[i] = factorial[i - 1] * i;}
}// 计算给定字符频率下的排列数
ll count_permutations(const vector<int>& counts) {int total = 0;for (int c : counts) total += c;ll result = factorial[total];for (int c : counts) {result /= factorial[c];}return result;
}// 递归生成左半部分的第 k 个排列
string build_kth(vector<int>& counts, int len, ll k) {if (len == 0) return "";int total_chars = 0;for (int i = 0; i < 26; i++) total_chars += counts[i];for (int i = 0; i < 26; i++) {if (counts[i] == 0) continue;// 尝试将字符 'a' + i 放在当前位置counts[i]--;ll ways = count_permutations(counts);if (k <= ways) {return string(1, 'a' + i) + build_kth(counts, len - 1, k);} else {k -= ways;counts[i]++;}}return ""; // 不会执行到这里
}void solve(int case_num) {string s;ll n;cin >> s >> n;// 统计频率vector<int> freq(26, 0);for (char c : s) {freq[c - 'a']++;}// 检查能否形成回文int odd_count = 0;char odd_char = 0;for (int i = 0; i < 26; i++) {if (freq[i] % 2 == 1) {odd_count++;odd_char = 'a' + i;}}int len = s.length();if ((len % 2 == 0 && odd_count != 0) || (len % 2 == 1 && odd_count != 1)) {cout << "Case " << case_num << ": XXX" << endl;return;}// 构造左半部分的字符计数vector<int> half_counts(26, 0);for (int i = 0; i < 26; i++) {half_counts[i] = freq[i] / 2;}int half_len = len / 2;// 计算左半部分的不同排列数ll total_palindromes = count_permutations(half_counts);if (n > total_palindromes) {cout << "Case " << case_num << ": XXX" << endl;return;}// 生成左半部分的第 n 个排列string left_half = build_kth(half_counts, half_len, n);// 构造完整回文string right_half = left_half;reverse(right_half.begin(), right_half.end());string palindrome;if (len % 2 == 0) {palindrome = left_half + right_half;} else {palindrome = left_half + odd_char + right_half;}cout << "Case " << case_num << ": " << palindrome << endl;
}int main() {init_factorial();int T;cin >> T;for (int t = 1; t <= T; t++) {solve(t);}return 0;
}
