F. Smaller - 题解
比赛与标签
比赛: Codeforces Round 827 (Div. 4) 标签: constructive algorithms, greedy, strings 难度: *1500
题目大意喵~?
你好呀,未来的算法大师!这道题是这样的呐:
我们有两个字符串 s
和 t
,一开始它们都只有一个字符 'a'。接下来会有一系列的 q
次操作。操作有两种:
1 k x
:把字符串x
重复k
次,然后拼接到字符串s
的末尾。2 k x
:把字符串x
重复k
次,然后拼接到字符串t
的末尾。
每次操作之后,我们都要判断一下:我们能不能通过重新排列 s
和 t
各自内部的字符,使得 s
的字典序严格小于 t
呢?
注意哦,每次操作都是永久性的,会改变 s
和 t
的状态的说。
猫娘的贪心小爪子!
看到“重新排列字符”这种字眼,我们的猫猫雷达就该响起来啦!这意味着字符串里字符的顺序不重要,重要的是我们拥有哪些字符以及它们的数量,对吧?
为了让 s
尽可能地小,我们应该怎么排列它的字符呢?当然是把最小的字符放在最前面,也就是升序排列啦!比如 s
的字符是 {'c', 'a', 'b'}
,那排列成的 s'
就是 "abc"
,这是它能变成的最小字典序的字符串。
反过来,为了让 t
尽可能地大,我们就应该把最大的字符放在最前面,也就是降序排列!如果 t
的字符也是 {'c', 'a', 'b'}
,那排列成的 t'
就是 "cba"
,这是它能变成的最大字典序的字符串。
所以,问题就转化成:每次操作后,把 s
的字符升序排列得到的 s'
,和把 t
的字符降序排列得到的 t'
,s'
是否能严格小于 t'
呢?
让我们来分析一下 s' < t'
的条件吧,喵~
情况一:最简单的情况!
s'
的第一个字符一定是 s
中最小的那个字符。t'
的第一个字符一定是 t
中最大的那个字符。
s
和 t
一开始都是 "a",所以 s
里面永远都至少有一个 'a' 字符。这意味着,s
中最小的字符永远都是 'a'!所以 s'
永远以 'a' 开头。
那么,如果 t
中出现了任何一个比 'a' 大的字符(比如 'b', 'c', ...),我们就可以把这个最大的字符放到 t'
的开头。s'
以 'a' 开头,t'
以一个比 'a' 大的字符开头,那 s'
肯定比 t'
小呀!
结论 1: 只要
t
字符串中包含任何一个大于 'a' 的字符,答案就是 YES!
情况二:有点棘手的情况
如果 t
中只含有 'a' 这个字符呢?那么 t'
就只能是 "aaaa..." 这样一长串 'a'。
现在 s'
要和 "aaaa..." 比较,s'
还有机会更小吗?
子情况 2a: 如果
s
中也出现了比 'a' 大的字符(比如 'b'),那么s'
排列出来就会是 "aaaa...b..." 的形式。和t'
的 "aaaa..." 相比,s'
在某个位置出现了 'b',而t'
对应位置还是 'a',所以s'
反而更大了!这样就不可能让s
比t
小了。结论 2: 如果
t
只包含 'a',但s
包含大于 'a' 的字符,答案就是 NO。子情况 2b: 这是最后的决战了!
s
和t
都只包含 'a' 字符。 那么s'
就是len_s
个 'a' 组成的字符串,t'
就是len_t
个 'a' 组成的字符串。根据字典序的定义,两个由相同字符组成的字符串,短的那个更小。结论 3: 如果
s
和t
都只包含 'a',那么当且仅当s
的长度len_s
小于t
的长度len_t
时,答案是 YES,否则是 NO。
总结一下我们的策略喵
我们根本不需要真的去存储和操作那两个可能变得超级长的字符串!我们只需要维护几个关键信息:
s
的长度len_s
。t
的长度len_t
。s
中是否包含大于 'a' 的字符(一个布尔值has_larger_s
就够了)。t
中是否包含大于 'a' 的字符(或者直接记录t
中的最大字符max_t
)。
每次操作后,我们就按下面的逻辑判断:
- 检查
t
中是否有大于 'a' 的字符 (即max_t > 'a'
)?- 有 -> YES
- 如果没有 (即
max_t == 'a'
):- 检查
s
中是否有大于 'a' 的字符 (即has_larger_s
为真)?- 有 -> NO
- 如果没有 (即
s
也只含 'a'):- 检查
len_s < len_t
?- 是 -> YES
- 否 -> NO
- 检查
- 检查
这样一来,问题是不是就变得非常清晰简单了呀?喵~
看我敲代码喵!
#include <iostream>
#include <algorithm>
#include <string> // 引入 string 头文件是个好习惯哦
using namespace std;
// 主函数,一切魔法的开始!
int main() {
// 加速输入输出,让程序跑得飞快,喵~
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin >> t;
while (t--) {
int q;
cin >> q;
// 初始化 s 和 t 的状态
long long len_s = 1, len_t = 1; // 初始长度都是 1 ("a")
// s 中最小的字符,因为初始是 "a",所以永远是 'a'
// char min_s = 'a';
// t 中最大的字符,初始是 'a'
char max_t = 'a';
// s 中是否包含过大于 'a' 的字符?初始为 false
bool has_larger_s = false;
for (int i = 0; i < q; i++) {
int d, k;
string x;
cin >> d >> k >> x;
if (d == 1) { // 操作 s
len_s += (long long)k * x.size();
// 检查新加入的字符串 x 是否有比 'a' 大的字符
for (char c : x) {
if (c > 'a') {
has_larger_s = true;
}
}
} else if (d == 2) { // 操作 t
len_t += (long long)k * x.size();
// 检查新加入的字符串 x,并更新 t 的最大字符
for (char c : x) {
if (c > max_t) {
max_t = c;
}
}
}
// --- 开始根据我们的策略进行判断 ---
// 情况 1: t 中有比 'a' 大的字符
if (max_t > 'a') {
cout << "YES\n";
}
// 情况 2: t 中只有 'a'
else {
// 子情况 2a: s 中有比 'a' 大的字符
if (has_larger_s) {
cout << "NO\n";
}
// 子情况 2b: s 中也只有 'a'
else {
if (len_s < len_t) {
cout << "YES\n";
} else {
cout << "NO\n";
}
}
}
}
}
return 0;
}
小声说:我稍微简化了一下AC代码里的逻辑,让它和我们的思路更贴合,但本质是一样的哦!原来的代码里min_s
和has_larger_s
的判断稍微有点绕,但因为s
里永远有'a',所以效果是一样的。
跑得快不快呀?
- 时间复杂度: O(Σ|x|) 的说。对于每个测试用例,我们处理
q
个查询。在每个查询中,我们遍历一次输入的字符串x
来更新我们的状态。所以总的时间消耗主要在于读取并处理所有x
字符串,总复杂度就是所有x
长度的总和。这完全可以接受! - 空间复杂度: O(1) 的说。我们没有存储那两个可能很长的字符串
s
和t
,只是用了几个变量(len_s
,len_t
,max_t
,has_larger_s
)来记录状态。所以每个测试用例只需要常数级别的额外空间,非常节省内存!
这次学到了什么喵?
这道题真是太棒了,它教会了我们:
- 贪心思想: 面对一个需要构造最优解的问题,先思考如何让一方达到“最有利”状态,另一方达到“最不利”状态,然后比较这两个极端情况。这就是贪心的魅力!
- 问题简化: “重新排列”是一个强烈的信号,它告诉我们可以忽略顺序,从字符集合的角度去思考问题。成功地将复杂问题简化为几个关键状态的判断,是解题的核心技巧。
- 状态压缩: 不要被题目中可能变得巨大的数据(超长字符串)吓到!很多时候,我们并不需要存储所有数据,只需要维护一些能做出决策的关键信息(状态)即可。
希望这篇题解能帮到你,如果还有不懂的地方,随时可以再来问我哦!一起加油,变得更强吧,喵~!