题目描述
难度分:1700
输入T(≤105)表示T组数据。所有字符串的长度之和≤105。
每组数据输入长度≤105的字符串s,只包含小写字母。
你可以重新排列s中的字母。
定义t=max(s,rev(s)),其中rev(s)为s反转后的字符串。
输出字典序最小的t。
输入样例
12
a
aab
abb
abc
aabb
aabbb
aaabb
abbb
abbbb
abbcc
eaga
ffcaba
输出样例
a
aba
bab
bca
abba
abbba
ababa
bbab
bbabb
bbcca
agea
acffba
算法
贪心构造
这个贪心策略非常难想,结合样例错了好多次连蒙带猜才过掉。大致的策略是这样,按照字典序从小到大遍历字母的频数:
- 如果字母c的频数是偶数,就将字母c平均分配在答案串t的两翼。
- 否则就先留下一个c,把剩下偶数个c平均分配在答案串t的两翼,此时又有两种情况:(1) 如果除了c只剩下一种字母c′没有填过,就将那个字母c′先填在t串中间,然后把c填在那一段连续c′的中间(如果c′的频数为奇数,就将c填在那段连续c′偏右的中间位置)。(2) 否则先将剩下的所有字母按从左往右的顺序在t串中间填完,最后一个c填在仅剩的那个位置即可。
复杂度分析
时间复杂度
遍历了一遍s统计各个字母的频数,然后根据频数表来构造答案串t,每往答案串中填一个字母的时间复杂度为O(1)(最多就是遍历一遍26个字母的频数表才能确定,仍然可以看成一个与字符串长度n无关的常数级别操作)。因此,算法整体的时间复杂度为O(n)。
空间复杂度
不算输出串的空间,空间瓶颈就在于各字母的频数表,这个表的规模与字符串长度无关,恒定为26(实现的时候稍微开大了一点),这仍然可以看成是常数空间,因此算法的额外空间复杂度为O(1)。
C++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 30;
int cnt[N];
string solve(string& s) {
int n = s.size();
int p = -1, l = 0, r = n - 1;
string ans;
ans.resize(n);
memset(cnt, 0, sizeof(cnt));
for(char c: s) {
cnt[c - 'a']++;
}
for(int i = 0; i < 26; i++) {
while(cnt[i] > 1) {
ans[l++] = ans[r--] = i + 'a', cnt[i] -= 2;
}
if(cnt[i] == 1) {
// 频数为奇数的字母
p = i;
break;
}
}
if(~p) {
if(*max_element(cnt + p + 1, cnt + 26) == r - l) {
// 后面的字母只剩一种
for(int i = p + 1; i < 26; i++) {
while(cnt[i] > 1) {
ans[l++] = ans[r--] = i + 'a';
cnt[i] -= 2;
}
}
}
for(int i = p + 1; i < 26; i++) {
while(cnt[i]--) {
ans[l++] = i + 'a';
}
}
ans[r--] = p + 'a';
cnt[p]--;
}
return ans;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
string s;
while(t--) {
cin >> s;
cout << solve(s) << endl;
}
return 0;
}