Skip to content

Problem: 1659A - Red Versus Blue

嘿嘿,主人!这道题可好玩啦~ 想象一下,有两支队伍,红队 (Red) 和蓝队 (Blue),他们打了 n 场比赛。红队赢了 r 场,蓝队赢了 b 场,而且我们知道红队更厉害一些,所以 r > b

虽然我们错过了直播,但我们想脑补一个最最精彩的比赛过程!所谓“精彩”,就是指两队比分咬得很紧,没有任何一队出现“超长连胜”的情况。我们的任务就是,构造一个长度为 n 的字符串(由 'R' 和 'B' 组成),代表比赛结果,使得其中连续相同字符(即连胜)的最大长度尽可能小

举个栗子喵:比如 RBRBRBR,这里面最长的连胜是 1(单个 'R' 或 'B'),这就非常“焦灼”啦。而 RRRBRR 里面,最长连胜就是 3(开头的 'RRR')。

我们要做的,就是找出那个让最大连胜长度最小的比赛记录字符串。如果有多种答案,随便输出一种就可以啦,喵~


解题思路 (ฅ'ω'ฅ)

主人请看,这道题的关键在于“最小化最大连胜”。因为题目告诉我们红队赢的次数 r 总是比蓝队 b 多 (r > b),所以最长的连胜队伍,毫无疑问会是红队 'R' 啦。所以我们的目标就变成了:如何打散这些 'R',让连续的 'R' 尽可能短?

最好的打散工具是什么呢?当然是我们的蓝队 'B' 啦!我们可以把 b 个 'B' 看作是“隔板”,用来把一大长串的 'R' 分割成一小段一小段的。

想象一下,我们有 b 个 'B'。把它们像这样排开:

_ B _ B _ B _ ... _ B _

看到了吗?b 个 'B' 可以创造出 b + 1 个可以放置 'R' 的空位(或者叫“小区块”)!

为了让每个小区块里的 'R' 数量尽可能平均(从而让最长的那个区块也尽可能短),我们就要采用“平均分配”的策略。这就像给 b+1 只小猫分 r 条小鱼干一样!

  1. 先算一下每个区块至少能分到多少个 'R': 这就是一个简单的整除运算 r / (b + 1)。我们叫它 base_run_length 吧。

  2. 剩下的 'R' 怎么办呢? 分完之后,可能还剩下一些 'R',数量是 r % (b + 1)。这些多出来的 'R',我们就一个一个地分给前面的区块,每个区块分一个,直到分完为止。

这样一来,有些区块会有 base_run_length + 1 个 'R',剩下的区块会有 base_run_length 个 'R'。这样分配是最均匀的,所以最长的一段 'R' 的长度就是 base_run_length + 1 (如果余数不为0) 或者 base_run_length (如果余数是0),这已经是我们能做到的最小极限啦!

所以,我们的构建步骤就是: 循环 b + 1 次,每次构建一个区块。 在每个区块里,先放 base_run_length 个 'R'。 如果还有多余的 'R' 需要分配,就在这个区块里再多放一个 'R'。 除了最后一个区块外,每个区块后面都跟上一个 'B' 作为分隔符。

搞定啦,是不是很简单喵~ ( ´ ▽ ` )ノ


代码题解

下面就是把我们刚才的想法变成代码啦,主人请看~

cpp
#include <iostream>

// 这个函数是解决单个测试用例的核心逻辑喵~
void solve() {
    int n, r, b;
    std::cin >> n >> r >> b;

    // 核心思想就是用 'b' 个蓝队胜利 'B' 作为隔板,
    // 去分割 'r' 个红队胜利 'R'。
    // 这样就创造出了 b+1 个小区块,可以用来放 'R'。
    int num_segments = b + 1;

    // 为了让 'R' 的最大连胜长度最小,我们要把 'r' 个 'R' 尽可能均匀地
    // 分配到这 b+1 个区块里。
    // 根据除法原理,每个区块至少能分到 r / (b+1) 个 'R'。
    // 这就是每个 'R' 连胜串的基础长度啦。
    int base_run_length = r / num_segments;

    // r % (b+1) 是余数,它告诉我们有多少个区块能幸运地多得到一个 'R'。
    int extra_r_count = r % num_segments;

    // 我们开始遍历这 b+1 个区块来构建字符串。
    for (int i = 0; i < num_segments; ++i) {
        // 先为当前区块打印基础数量的 'R'。
        for (int j = 0; j < base_run_length; ++j) {
            std::cout << 'R';
        }

        // 如果还有多余的 'R' 没分配完,就给当前这个区块多加一个 'R',
        // 然后把待分配的额外 'R' 数量减一。
        if (extra_r_count > 0) {
            std::cout << 'R';
            extra_r_count--;
        }

        // 在每个 'R' 区块后面,我们都放一个 'B' 作为隔板。
        // 总共有 'b' 个 'B' 要放,所以最后一个 'R' 区块后面就不用放啦。
        if (i < b) {
            std::cout << 'B';
        }
    }
    std::cout << '\n'; // 一个测试用例结束,换行!
}

int main() {
    // 这两行是加速输入输出的魔法哦,让程序跑得更快!
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int t;
    std::cin >> t; // 读取有几组测试数据
    while (t--) {
        solve(); // 循环解决每一组
    }

    return 0; // 完美结束,喵~
}

相关知识点介绍

这道题虽然简单,但背后蕴含着一些非常重要的算法思想呢!

1. 贪心算法 (Greedy Algorithm)

贪心算法是这道题的灵魂所在!它的核心思想是“目光短浅”,在每一步决策时,都采取当前看起来最优的选择,而不从整体上考虑,期望通过一系列局部最优解,最终能得到全局最优解。

在这道题里,我们的“贪心”体现在:

  • 目标:最小化最大连胜。
  • 策略:用 'B' 把 'R' 分割开。
  • 每一步的局部最优:在分配 'R' 到 b+1 个区块时,我们让每个区块的 'R' 数量尽可能地接近,而不是让某个区块有很多 'R' 而其他区块很少。这种“均匀分配”的策略,就是当前情况下的最优选择,因为它直接作用于减小可能出现的最大值。

事实证明,对于这个问题,这种贪心策略恰好可以得到全局最优解。

2. 抽屉原理 (Pigeonhole Principle)

抽屉原理,也叫鸽巢原理,是一个非常可爱的数学原理!它说的是:如果你有 n+1 只鸽子,要放进 n 个鸽巢里,那么至少有一个鸽巢里会有两只或更多的鸽子。

在我们的问题里,可以这么看:

  • 鸽子r 个红队胜利 'R'。
  • 鸽巢b+1 个由 'B' 分割出的区块。

我们要把 r 只“鸽子”放进 b+1 个“鸽巢”。根据抽屉原理的推广,至少有一个鸽巢里要放 ⌈r / (b+1)⌉(向上取整)只鸽子。这告诉我们,无论我们怎么分配,最大连胜长度的最小值不可能小于 r / (b+1) 向上取整。而我们上面那种均匀分配的贪心算法,恰好就构造出了一个最大长度为 r / (b+1) 向上取整的结果!这从数学上证明了我们的贪心解法是最优的,喵~

希望本猫娘的讲解对主人有帮助哦!下次再见啦!(ฅ>ω<*ฅ)

Released under the MIT License.