Skip to content

喵~ 主人你好呀!今天我们来解决一道关于在聊天室里发表情,一不小心就会被封号的有趣问题,嘿嘿。这道题叫做 C. Chat Ban,让我们一起来看看怎么才能在不被封号的情况下,尽可能多地发出可爱的表情包吧!

题目大意

是这样的喵,我们想在一个聊天室里发送一个酷炫的 "表情包三角"。

这个三角由一个参数 k 决定,总共包含 2k-1 条消息。它的形状是这样的:

  • 第 1 条消息:1 个表情
  • 第 2 条消息:2 个表情
  • ...
  • k 条消息:k 个表情 (这是顶峰啦!)
  • k+1 条消息:k-1 个表情
  • ...
  • 2k-1 条消息:1 个表情

举个例子,如果 k=3,我们就会发送像这样 1, 2, 3, 2, 1 个表情的五条消息。

但是!聊天室里有个严格的管理员,如果我们发送的表情总数累计达到了 x,我们就会立刻被封号(被抓走关小黑屋!)。被封号的那条消息也算在内。

我们的任务就是计算,对于给定的 kx,我们最多能发送多少条消息才会被封号。当然,如果我们能成功发完整个三角而不被封号,那就算我们成功啦!

解题思路

主人请看,这道题的 k 可以达到 10^9x 更是大得吓人。如果我们一条一条消息地去模拟计算,肯定会超时哒,呜... 所以我们需要找到更聪明的办法,喵!

关键点在于,我们发送的消息越多,累计的表情总数也一定是越多的。这是一个非常重要的单调性!每当我们遇到这种单调递增或递减的关系时,就应该立刻想到——二分查找

我们可以二分查找“最终发送了多少条消息”。假设我们二分查找到的答案是 m 条消息,我们只需要快速计算出发送 m 条消息所需要的表情总数,然后和 x 比较就行啦。

  1. 判断是否会被封号: 首先,我们可以算一下,把整个表情包三角(2k-1 条消息)发完,一共需要多少表情呢?

    • 上升部分:1 + 2 + ... + k = k * (k + 1) / 2
    • 下降部分:(k-1) + (k-2) + ... + 1 = (k-1) * k / 2
    • 总数 = k*(k+1)/2 + (k-1)*k/2 = (k^2+k+k^2-k)/2 = 2k^2/2 = k*k 哇!原来总共是 k*k 个表情!如果给定的 x 大于等于 k*k,说明我们无论如何也凑不够 x 个表情,可以安全地发完所有 2k-1 条消息。
  2. 二分查找答案: 如果 x < k*k,说明我们一定会在中途被封号。这时就需要二分查找了。我们可以把查找分为两个阶段:

    • 阶段一:在上升阶段被封号 (消息数 ≤ k) 如果我们发送 m 条消息(m <= k),那么表情总数就是 1 + 2 + ... + m = m * (m + 1) / 2。 我们可以二分查找 m 的范围 [1, k],找到最小的 m 使得 m * (m + 1) / 2 >= x

    • 阶段二:在下降阶段被封号 (消息数 > k) 如果发完前 k 条消息还没被封号,说明 x 大于前 k 条消息的表情总数 k * (k + 1) / 2。 此时我们已经发送了 k 条消息,接下来发送的第 k+p 条消息(p >= 1)会包含 k-p 个表情。 假设我们在下降阶段又发送了 p 条消息,那么总消息数是 k+p。 这额外的 p 条消息带来的表情数是 (k-1) + (k-2) + ... + (k-p)。 总表情数就是 (前k条总数) + (后p条总数)。 我们同样可以二分查找 p 的范围 [1, k-1],找到最小的 p 使得总表情数大于等于 x。最终答案就是 k+p

将这两个阶段合在一起,我们就可以用二分查找高效地找到答案啦!

代码实现

这是猫娘为您准备的 C++ 代码,里面加了详细的注释哦~

cpp
#include <iostream>

// 这个函数用来解决单个测试用例喵
void solve() {
    long long k, x;
    std::cin >> k >> x;

    // 一个完整的 k 阶表情包三角总共有 k*k 个表情
    // 计算方法:(1+...+k) + (1+...+k-1) = k*(k+1)/2 + (k-1)*k/2 = k*k
    // 如果 x 大于等于 k*k,说明我们永远不会被封号,可以发完所有 2k-1 条消息
    if (x >= k * k) {
        std::cout << 2 * k - 1 << "\n";
        return;
    }

    // 如果程序运行到这里,说明我们一定会在中途被封号
    // 因为发送的消息数和表情总数是单调递增的,所以可以用二分查找
    
    // 我们先判断封号是发生在上升阶段还是下降阶段
    // 上升阶段(前k条消息)的表情总数
    long long emotes_at_k = k * (k + 1) / 2;

    if (x <= emotes_at_k) {
        // 情况一:在上升阶段(前k条消息内)被封号
        // 我们需要找到最小的消息数 m (1 <= m <= k),使得表情总数 >= x
        // 表情总数公式是 m * (m + 1) / 2
        // 在 [1, k] 范围内二分查找 m
        long long low = 1, high = k;
        long long ans = k; // 初始化为一个安全的最大值
        while (low <= high) {
            long long mid = low + (high - low) / 2;
            // 注意:这里需要检查 mid*(mid+1) 是否会溢出,但在此题 k <= 10^9, mid*(mid+1) 不会超过 long long
            if (mid * (mid + 1) / 2 >= x) {
                ans = mid;      // mid 是一个可能的答案,尝试找更小的
                high = mid - 1;
            } else {
                low = mid + 1;  // 表情还不够,需要更多消息
            }
        }
        std::cout << ans << "\n";
    } else {
        // 情况二:在下降阶段(k条消息后)被封号
        // 我们已经发送了 k 条消息,累计了 emotes_at_k 个表情
        // 还需要凑够的表情数是
        long long emotes_needed = x - emotes_at_k;

        // 下降阶段的消息表情数是 k-1, k-2, ..., 1
        // 我们需要找到最小的 p (1 <= p <= k-1),使得下降阶段前 p 条消息的表情总数 >= emotes_needed
        // 这个总数是 (k-1) + (k-2) + ... + (k-p),求和公式为 p * (2*k - p - 1) / 2
        // 在 [1, k-1] 范围内二分查找 p
        long long low = 1, high = k - 1;
        long long ans_p = k - 1; // 初始化为一个安全的最大值
        while (low <= high) {
            long long p = low + (high - low) / 2;
            long long emotes_from_p = p * (2 * k - p - 1) / 2;
            
            if (emotes_from_p >= emotes_needed) {
                ans_p = p;      // p 是一个可能的答案,尝试找更小的
                high = p - 1;
            } else {
                low = p + 1;    // 表情还不够,需要更多消息
            }
        }
        // 总消息数 = 上升阶段的 k 条 + 下降阶段的 ans_p 条
        std::cout << k + ans_p << "\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. 二分查找 (Binary Search) 二分查找是一种非常高效的搜索算法,它要求被搜索的数据必须具有单调性(比如有序)。就像我们在这道题里遇到的,消息越多,表情总数也越多。二分查找每次都检查中间元素,然后排除掉一半不可能的范围,所以速度非常快,时间复杂度是 O(log N),对于处理大数据范围的问题超级有用!

  2. 等差数列求和 (Arithmetic Series Sum) 我们在计算表情总数时,反复用到了这个数学小魔法。像 1 + 2 + ... + n 这样的数列就是等差数列。它的求和公式是 (首项 + 末项) * 项数 / 2。记住这个公式,可以帮我们 O(1) 地算出总和,避免了循环累加的麻烦。

  3. 数据范围与 long long 注意到题目中的 kx 都非常大吗?一个普通的 int 类型(通常是32位)最大只能存到 2 * 10^9 左右。在计算 k*k 或者 m*(m+1)/2 时,结果很容易就会超出 int 的范围,导致数据"溢出",变成一个奇怪的负数。为了避免这种错误,我们必须使用 long long 类型,它可以存储大得多的整数(大约到 9 * 10^18),保证计算的正确性。这是写代码时一定要注意的好习惯哦,喵~

好啦,今天的题解就到这里啦!希望主人能喜欢这道题,也喜欢猫娘的讲解。下次再见喵~ >w<

Released under the MIT License.