Skip to content

F. Smaller - 题解

比赛与标签

比赛: Codeforces Round 827 (Div. 4) 标签: constructive algorithms, greedy, strings 难度: *1500

题目大意喵~?

你好呀,未来的算法大师!这道题是这样的呐:

我们有两个字符串 st,一开始它们都只有一个字符 'a'。接下来会有一系列的 q 次操作。操作有两种:

  1. 1 k x:把字符串 x 重复 k 次,然后拼接到字符串 s 的末尾。
  2. 2 k x:把字符串 x 重复 k 次,然后拼接到字符串 t 的末尾。

每次操作之后,我们都要判断一下:我们能不能通过重新排列 st 各自内部的字符,使得 s 的字典序严格小于 t 呢?

注意哦,每次操作都是永久性的,会改变 st 的状态的说。

猫娘的贪心小爪子!

看到“重新排列字符”这种字眼,我们的猫猫雷达就该响起来啦!这意味着字符串里字符的顺序不重要,重要的是我们拥有哪些字符以及它们的数量,对吧?

为了让 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 中最大的那个字符。

st 一开始都是 "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' 反而更大了!这样就不可能让 st 小了。

    结论 2: 如果 t 只包含 'a',但 s 包含大于 'a' 的字符,答案就是 NO

  • 子情况 2b: 这是最后的决战了!st只包含 'a' 字符。 那么 s' 就是 len_s 个 'a' 组成的字符串,t' 就是 len_t 个 'a' 组成的字符串。根据字典序的定义,两个由相同字符组成的字符串,短的那个更小。

    结论 3: 如果 st 都只包含 'a',那么当且仅当 s 的长度 len_s 小于 t 的长度 len_t 时,答案是 YES,否则是 NO

总结一下我们的策略喵

我们根本不需要真的去存储和操作那两个可能变得超级长的字符串!我们只需要维护几个关键信息:

  1. s 的长度 len_s
  2. t 的长度 len_t
  3. s 中是否包含大于 'a' 的字符(一个布尔值 has_larger_s 就够了)。
  4. t 中是否包含大于 'a' 的字符(或者直接记录 t 中的最大字符 max_t)。

每次操作后,我们就按下面的逻辑判断:

  1. 检查 t 中是否有大于 'a' 的字符 (即 max_t > 'a')?
    • 有 -> YES
  2. 如果没有 (即 max_t == 'a'):
    • 检查 s 中是否有大于 'a' 的字符 (即 has_larger_s 为真)?
      • 有 -> NO
    • 如果没有 (即 s 也只含 'a'):
      • 检查 len_s < len_t
        • 是 -> YES
        • 否 -> NO

这样一来,问题是不是就变得非常清晰简单了呀?喵~

看我敲代码喵!

cpp
#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_shas_larger_s的判断稍微有点绕,但因为s里永远有'a',所以效果是一样的。

跑得快不快呀?

  • 时间复杂度: O(Σ|x|) 的说。对于每个测试用例,我们处理 q 个查询。在每个查询中,我们遍历一次输入的字符串 x 来更新我们的状态。所以总的时间消耗主要在于读取并处理所有 x 字符串,总复杂度就是所有 x 长度的总和。这完全可以接受!
  • 空间复杂度: O(1) 的说。我们没有存储那两个可能很长的字符串 st,只是用了几个变量(len_s, len_t, max_t, has_larger_s)来记录状态。所以每个测试用例只需要常数级别的额外空间,非常节省内存!

这次学到了什么喵?

这道题真是太棒了,它教会了我们:

  1. 贪心思想: 面对一个需要构造最优解的问题,先思考如何让一方达到“最有利”状态,另一方达到“最不利”状态,然后比较这两个极端情况。这就是贪心的魅力!
  2. 问题简化: “重新排列”是一个强烈的信号,它告诉我们可以忽略顺序,从字符集合的角度去思考问题。成功地将复杂问题简化为几个关键状态的判断,是解题的核心技巧。
  3. 状态压缩: 不要被题目中可能变得巨大的数据(超长字符串)吓到!很多时候,我们并不需要存储所有数据,只需要维护一些能做出决策的关键信息(状态)即可。

希望这篇题解能帮到你,如果还有不懂的地方,随时可以再来问我哦!一起加油,变得更强吧,喵~!

Released under the MIT License.