Skip to content

喵~ 主人,欢迎来到我的题解小站!今天我们要一起攻克的是一道有点意思的博弈论题目哦,叫作 "Number Game"。别担心,只要跟着我的思路,我们一定能帮 Alice 找到必胜的秘诀,让她打败 Bob 的喵!

题目大意

Alice 和 Bob 又在玩游戏啦。他们有一个包含 nn 个正整数的数组 aa

游戏开始前,Alice 会选择一个整数 k0k \ge 0。游戏将进行 kk 轮,编号从 1 到 kk

在第 ii 轮 (从 i=1i=1i=ki=k),会发生两件事:

  1. Alice 的回合:Alice 必须从数组中移除一个元素,这个元素的值必须小于或等于 ki+1k - i + 1。如果她找不到这样的元素,她就输了。
  2. Bob 的回合:在 Alice 移除了一个元素后,如果数组还不为空,Bob 必须选择数组中任意一个剩下的元素,并给它加上 ki+1k - i + 1

如果 Alice 成功地完成了所有 kk 轮操作而没有输掉,那么她就赢了。

Bob 的目标是让 Alice 输掉,而 Alice 的目标是赢得游戏。他们两个都会采取最优策略。我们的任务就是,找出能让 Alice 获胜的最大的那个 kk 是多少,喵~


解题思路

这道题要求我们找到一个最大kk,满足某个条件(Alice能赢)。一看到“最大化最小值”或者“最小化最大值”这类问题,我们就要像小猫闻到猫薄荷一样,立刻警觉起来——这很可能是二分答案的信号喵!

为什么可以二分答案?

二分答案的前提是答案具有单调性。我们来想一下:如果 Alice 能赢得一个需要进行 kk 轮的游戏,那她能不能赢得一个更简单的、只需要进行 k1k-1 轮的游戏呢?

答案是肯定的喵!如果她有一个能赢 kk 轮的策略,她肯定可以沿用这个策略来应对 k1k-1 轮的挑战。每一轮的要求甚至都变宽松了。所以,“Alice 能否赢得 kk 轮游戏”这个性质,对于 kk 来说是单调递减的。也就是说,存在一个临界点,使得所有小于等于这个值的 kk Alice 都能赢,而所有大于这个值的 kk 她都会输。

既然有了单调性,我们就可以愉快地对 kk 进行二分查找啦!我们可以在 [0, n] 的范围内二分 kk 的值。(kk 不可能超过 nn,因为每轮都要移除一个数,总共就 nn 个数嘛)。

check(k) 函数:模拟最优策略

二分答案的核心在于 check(k) 函数。这个函数需要判断,对于一个给定的 kk,Alice 是否有必胜策略。为此,我们需要模拟整个游戏过程,并假设 Alice 和 Bob 都采取对自己最有利的行动。

1. Alice 的最优策略是什么?

在第 ii 轮,Alice 需要移除一个小于等于 limit = k - i + 1 的数。她有很多选择,应该选哪个呢?为了让未来的自己有更多选择,她应该尽可能地保留那些“好用”的牌。哪些牌好用呢?当然是数值小的牌!因为越到后面的回合,limit 的值会越小,对牌面大小的要求也越严格。

所以,Alice 的最优策略是:在当前所有满足 a[j] <= limit 的牌中,移除数值最大的那一张。这样,她就把最接近 limit、最“危险”的牌用掉了,同时为未来的自己保留了更小、更灵活的牌。这是一种贪心策略哦,喵~

2. Bob 的最优策略是什么?

Bob 是个坏心眼的家伙,他想让 Alice 输掉。他会把 limit 加到剩下的某个数上。怎么加才能让 Alice 最难受呢?

他应该把 limit 加到当前数组中最小的那个元素上。为什么呢?因为这样会创造出一个非常非常大的数,这个数在接下来的回合里基本不可能满足 a[j] <= k - i' + 1 的条件了(因为 k - i' + 1 只会越来越小)。这相当于 Bob 帮 Alice “废掉”了她一张本来可能很有用的小牌,让她未来的选择更少。真是太坏了,哼!

3. 如何高效地模拟?

在模拟过程中,我们需要不断地:

  • 查找小于等于 limit 的最大元素。
  • 移除一个元素。
  • 查找最小的元素。
  • 添加一个元素。

对于这种需要维护一个有序集合并频繁增删查改的操作,std::multiset (C++) 就是我们的不二之选!它是一个基于红黑树的有序集合,可以存储重复元素,并且所有操作的时间复杂度都是对数级别的(O(logN)),非常高效。

check(k) 的模拟流程就清晰啦:

  1. 把初始数组 a 的所有元素放入一个 multiset s
  2. 循环 kk 轮,从 i = 1k: a. 计算当前轮的限制 limit = k - i + 1。 b. 在 s 中查找小于等于 limit 的最大元素。这可以通过 s.upper_bound(limit) 找到第一个大于 limit 的元素,然后将迭代器向前移动一位得到。 c. 如果找不到这样的元素(即 upper_bound 返回的是 s.begin()),说明 Alice 没牌可出了,她输了。check(k) 返回 false。 d. 如果找到了,就从 s 中移除这个元素。 e. 如果 s 不为空,取出 s 中最小的元素(*s.begin()),移除它,然后将 最小元素 + limit 的新值再插入回 s 中。
  3. 如果循环能顺利跑完 kk 轮,说明 Alice 赢了!check(k) 返回 true

最后,我们在 [0, n] 的区间上对 k 进行二分,每次用 check(mid) 来判断,不断缩小范围,就能找到最大的可行 k 啦!


题解代码

这是参考的 C++ 代码实现,我已经加上了可爱的注释,方便主人理解哦~

cpp
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>

// 这个函数用来检查对于给定的k,Alice是否能赢喵
// 我们会模拟双方都采取最优策略的整个游戏过程
bool check(int k, int n, const std::vector<int>& a) {
    if (k == 0) {
        return true; // k=0意味着不玩游戏,Alice当然算赢啦
    }
    if (k > n) {
        return false; // 牌都不够出k轮,肯定不行喵
    }

    // 用 multiset 来高效地管理数组里的数字
    // 它可以自动排序,方便我们找最大最小的数
    std.multiset<long long> s;
    for (int x : a) {
        s.insert(x);
    }

    // 模拟从第1轮到第k轮的游戏
    for (int i = 1; i <= k; ++i) {
        long long limit = k - i + 1; // 当前轮次,Alice能移除的数的上限

        // Alice的回合:移除小于等于 limit 的最大的数
        // s.upper_bound(limit) 找到第一个严格大于 limit 的数
        auto it = s.upper_bound(limit);
        
        if (it == s.begin()) {
            // 如果迭代器在最开始,说明所有数都比 limit 大
            // Alice 找不到可以出的牌,她输了喵 >_<
            return false;
        }

        // 把迭代器往前挪一位,就找到了小于等于 limit 的最大元素
        --it;
        s.erase(it);

        // Bob的回合:把 limit 加到剩下的最小的数上
        if (!s.empty()) {
            long long smallest = *s.begin(); // multiset 的第一个元素就是最小的
            s.erase(s.begin());
            s.insert(smallest + limit); // 把"被污染"的数再放回去
        }
    }

    // 如果 Alice 成功撑过了所有 k 轮,她就赢了!
    return true;
}

void solve() {
    int n;
    std::cin >> n;
    std::vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> a[i];
    }

    // 在 [0, n] 的范围内二分查找最大的 k
    int low = 0, high = n, ans = 0;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (check(mid, n, a)) {
            // 如果 k=mid 可行,那它就是一个潜在的答案,我们尝试更大的 k
            ans = mid;
            low = mid + 1;
        } else {
            // 如果 k=mid 不行,说明 k 太大了,我们得试试小一点的 k
            high = mid - 1;
        }
    }
    std::cout << ans << std::endl;
}

int main() {
    // 加速一下输入输出,跑得快一点喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}

知识点小课堂

这道题用到了几个非常重要的算法思想和工具,我们来总结一下吧,喵~

  1. 博弈论 (Game Theory)

    • 这道题是典型的二人零和博弈。解决这类问题的关键是分析双方的最优策略。你需要站在 Alice 的角度思考如何最大化自己的赢面,也要站在 Bob 的角度思考如何让 Alice 的处境最糟。
  2. 二分答案 (Binary Search on the Answer)

    • 当问题的答案具有单调性时,就可以使用二分法来快速定位答案。对于求解“最大值最小”或“最小值最大”的问题,这是一种非常经典且高效的技巧。
  3. 贪心算法 (Greedy Algorithm)

    • 在分析最优策略时,我们得出了 Alice 和 Bob 的行动方式:每一步都做出当前看起来最好的选择。Alice 贪心地移除最“危险”的牌,Bob 贪心地“污染”最“有用”的牌。正确的贪心策略是解决本题模拟部分的核心。
  4. 数据结构:std::multiset

    • 选择合适的数据结构能让代码实现事半功倍。multiset 能够自动维护一个有序的、可重复的元素集合,并支持高效的插入、删除和查找操作。在本题这种需要动态维护有序数据的场景下,它简直是完美的选择喵!

好啦,这道题的解析就到这里结束啦!希望我的讲解对主人有所帮助。通过分析双方策略,结合二分答案和 multiset,我们就轻松地解决了这个问题。下次再一起玩耍吧,喵~ (ฅ'ω'ฅ)

Released under the MIT License.