题目描述
PIS\texttt{PIS}PIS (排列不敏感字符串) 是一种字符串,即使字符的位置互换,其值也不会改变。例如,若一个 PIS\texttt{PIS}PIS 的值是 abc\texttt{abc}abc ,它也可以写作 acb\texttt{acb}acb 、 bca\texttt{bca}bca 等。
FIS\texttt{FIS}FIS (频率不敏感字符串) 是一种字符串,如果任何字符的频率增加或减少(不改变总长度,且不完全删除任何字符),其值也不会改变。例如,若一个 FIS\texttt{FIS}FIS 的值是 aabc\texttt{aabc}aabc ,它也可以是 abbc\texttt{abbc}abbc 或 abcc\texttt{abcc}abcc 。
FPIS\texttt{FPIS}FPIS (频率和排列不敏感字符串) 是同时满足排列不敏感和频率不敏感的字符串。给定一个 FPIS\texttt{FPIS}FPIS ,你需要输出其字典序最小的版本。
输入格式
第一行包含一个整数 TTT ( T≤1000T \le 1000T≤1000 ),表示接下来有 TTT 个字符串。
之后的 TTT 行每行包含一个 FPIS\texttt{FPIS}FPIS 字符串,最多包含 100010001000 个字符,所有字符均为小写英文字母。
输出格式
对于每个输入字符串,输出一行,包含其字典序最小的 FPIS\texttt{FPIS}FPIS 版本。
字典序比较规则
- 从左到右比较两个字符串的第一个字符,若不同,则按字符顺序决定大小( a<b<⋯<z\texttt{a} < \texttt{b} < \dots < \texttt{z}a<b<⋯<z )。
- 若相同,则比较下一个字符。
- 若一个字符串先耗尽字符,则它更小。
- 若同时耗尽,则相等。
样例输入
4
bca
pqab
aabb
c
样例输出
abc
abpq
aaab
c
题目分析与思路
核心理解
题目定义中的 FPIS\texttt{FPIS}FPIS 具有两个关键性质:
- 排列不敏感 :字符串的值与字符的排列顺序无关,只与字符的集合有关。
- 频率不敏感 :字符串的值与每个字符出现的频率无关,只要保持总长度不变且每个在原始字符串中出现的字符类型至少出现一次。
因此,对于一个给定的 FPIS\texttt{FPIS}FPIS 字符串,我们可以任意重新排列字符,并任意改变每个字符的个数,只要:
- 总长度 nnn 不变。
- 原始字符串中出现的所有字符种类 kkk 都至少出现一次。
问题转化
我们需要构造一个新的长度为 nnn 的字符串,包含给定的 kkk 种字符,每种至少出现一次,使得这个字符串的字典序最小。
贪心构造策略
假设 kkk 种字符按字母顺序排序为 c1<c2<⋯<ckc_1 < c_2 < \dots < c_kc1<c2<⋯<ck 。
我们的目标是让最终的字符串尽可能靠前的字符小。
贪心思路 :
- 对于当前最小的字符 cic_ici ,我们尽可能多地放置它,但要保证后面剩余的 k−ik-ik−i 种字符每种至少有一个位置。
- 设已经使用了 used\texttt{used}used 个位置,则剩余位置为 n−usedn - \texttt{used}n−used 。
为了保证后面 k−ik-ik−i 种字符各有一个位置,当前字符 cic_ici 最多能放:
canTake=n−used−(k−i)\texttt{canTake} = n - \texttt{used} - (k - i)canTake=n−used−(k−i)
这个值必须至少为 111 (因为 cic_ici 至少要出现一次)。 - 将 cic_ici 重复 canTake\texttt{canTake}canTake 次加入结果字符串,然后更新已使用位置数。
- 对下一个字符重复此过程,直到所有字符处理完毕。
为什么这样贪心是正确的?
- 为了让字典序最小,我们希望最左边的字符尽可能小。
- 如果我们不在前面尽可能多地放 c1c_1c1 ,而留出位置给后面的字符,那么这些位置会被更大的字符占据,导致字典序变大。
- 我们唯一不能将全部位置都给 c1c_1c1 的原因是必须保证 c2,…,ckc_2, \dots, c_kc2,…,ck 每种至少出现一次,因此我们只需预留最少的位置给它们(每种一个位置),其余位置全部用于放置当前最小的字符。
算法步骤
- 读入字符串 sss ,获取长度 nnn 和字符种类集合。
- 将字符种类按字母顺序排序。
- 初始化结果字符串和已使用位置计数器 used=0\texttt{used} = 0used=0 。
- 对于排序后的字符列表中的每个字符 cic_ici ( iii 从 000 开始):
- 计算剩余需要保证的字符种类数 needAfter=k−i−1\texttt{needAfter} = k - i - 1needAfter=k−i−1 。
- 计算当前字符能放置的最大数量 canTake=n−used−needAfter\texttt{canTake} = n - \texttt{used} - \texttt{needAfter}canTake=n−used−needAfter 。
- 将 cic_ici 重复 canTake\texttt{canTake}canTake 次加入结果字符串。
- 更新 used←used+canTake\text{used} \gets \texttt{used} + \texttt{canTake}used←used+canTake 。
- 输出结果字符串。
复杂度分析
- 字符种类数 k≤26k \le 26k≤26 (小写字母)。
- 每个字符处理的复杂度为 O(n)O(n)O(n) ,总复杂度 O(n⋅k)O(n \cdot k)O(n⋅k) ,在 n≤1000n \le 1000n≤1000 时完全可行。
代码实现
// Lexicographically Smallest FPIS
// UVa ID: 13088
// Verdict: Accepted
// Submission Date: 2025-12-13
// UVa Run Time: 0.020s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
int main() {
int T; cin >> T;
while (T--) {
string s; cin >> s;
int n = s.length();
set<char> chars(s.begin(), s.end());
vector<char> sortedChars(chars.begin(), chars.end());
int k = sortedChars.size();
string result;
int used = 0;
for (int i = 0; i < k; i++) {
int needAfter = k - i - 1; // 后面还需要保证的字符种类数
int canTake = n - used - needAfter; // 当前字符能放的最大数量
// 将当前字符重复 canTake 次加入结果
for (int j = 0; j < canTake; j++) {
result += sortedChars[i];
used++;
}
}
cout << result << endl;
}
return 0;
}
样例验证
以 aabb\texttt{aabb}aabb ( n=4n=4n=4 , k=2k=2k=2 ,字符集合 {a,b}\{\texttt{a},\texttt{b}\}{a,b} )为例:
- i=0i=0i=0 (字符 a\texttt{a}a ): needAfter=1\texttt{needAfter}=1needAfter=1 , canTake=4−0−1=3\texttt{canTake}=4-0-1=3canTake=4−0−1=3 → 放入 333 个 a\texttt{a}a 。
- i=1i=1i=1 (字符 b\texttt{b}b ): needAfter=0\texttt{needAfter}=0needAfter=0 , canTake=4−3−0=1\texttt{canTake}=4-3-0=1canTake=4−3−0=1 → 放入 111 个 b\texttt{b}b 。
得到 aaab\texttt{aaab}aaab ,与样例输出一致。
其他样例同理验证通过。
总结
本题的关键在于理解 FPIS\texttt{FPIS}FPIS 的定义,并转化为一个在总长度固定、每种字符至少出现一次的限制下构造字典序最小字符串的贪心问题。通过按字母顺序处理字符,并尽可能多地放置当前最小的字符,同时为后面的字符预留最少的位置,即可得到最优解。

737

被折叠的 条评论
为什么被折叠?



