纸牌游戏

GitHub跳转原题关系图返回列表

通过分析游戏无限进行的稳态条件,转化为降序排序后寻找最大满足 a[i] >= i 的人数。

OJ: roj

题目 ID: 10005

难度:普及+/提高-

标签:贪心排序思维

创建: 2026-10-09 23:32

更新: 2026-10-09 23:32

形式化题目

有 nn 个人进行游戏,初始每人有 nn 张牌。 每轮从第一个人开始,第 ii 个人需要从存活的玩家中选择 min⁡(k−1,ai)\min(k - 1, a_i) 个不同的人,分别从他们那里拿走一张牌(其中 kk 为当前存活人数)。 任何时刻如果某人手中没有牌,立刻出局。 游戏无限进行。每个人的目标优先级依次为:

  1. 自己不出局。
  2. 在保证自己不出局的前提下,希望出局的人数尽可能多。 求在所有人都采取最优策略下,最终游戏最少剩下的存活人数。

正解

思路

本题需要考虑玩家在游戏无限进行时的存活条件。 假设当前有 kk 个玩家存活。每个人在一轮内,最多从其他人手中拿走 min⁡(k−1,ai)\min(k-1, a_i) 张牌。 由于每个人拿别人一张牌会导致被拿牌者牌数减 1,自己牌数加 1。在一轮游戏中,每个人最多可能被剩下的 k−1k-1 人每人拿走一张牌,也就是单轮最多失去 k−1k-1 张牌。

  1. 绝对安全条件:如果某个玩家的 ai≥k−1a_i \ge k-1,那么他每轮能够拿到 k−1k-1 张牌。即使他在这一轮中被剩下的所有人“集火”针对,他最多也就是失去 k−1k-1 张牌。由于他的收支永远保证 ≥0\ge 0,而初始有 nn 张牌(n≥kn \ge k 足够多,且在一轮内最多失去 n−1n-1 张牌,不会导致首轮暴毙),所以该玩家的牌数永远不会减少,也就绝对不可能出局。
  2. 必然出局条件:如果某个玩家的 ai<k−1a_i < k-1,那么他单轮最多只能获取 aia_i 张牌。如果其他玩家为了“让尽可能多的人出局”而联合起来集火他,他每轮会失去 k−1k-1 张牌。这样他每轮净亏损至少 k−1−ai>0k-1-a_i > 0 张牌。在无限进行的游戏中,该玩家的牌数迟早会扣到 0,从而被淘汰。

综上所述,当剩余 kk 个人时,所有 ai<k−1a_i < k-1 的人都是可以被其他玩家集火淘汰的。由于大家都希望淘汰更多的人,所以必定会优先集火他们。 游戏人数会不断减少,每淘汰一人,kk 随之减小 1,安全门槛也会随之降低。直到剩下的所有人均满足 ai≥k−1a_i \ge k-1 为止,此时没有任何人能被集火淘汰,游戏进入最终稳态。

由于能够存活的人必定是 aia_i 较大的那批人,我们可以将所有的 aia_i 按降序排列,此时只需寻找最大的 kk,使得前 kk 个人的 aia_i 都大于等于 k−1k-1。 因为序列是降序的,只要第 kk 个人满足 ak≥k−1a_k \ge k-1,前 kk 个人必然都满足条件。同时因为若存在 m>km > k 且 am≥m−1a_m \ge m-1,则必然也有 am≥k−1a_m \ge k-1,与“最大的 kk”矛盾。 因此,对降序排列后的数组(设为 0-indexed),即寻找最大的 ii 使得 ai≥ia_i \ge i,此时存活人数就是 i+1i+1。

代码

python
#!/usr/bin/env python3
# Author: 2026-10-09 22:30

import sys

def solve() -> None:
    input_data = sys.stdin.read().split()
    if not input_data:
        return
    n = int(input_data[0])
    a = []
    for i in range(1, n + 1):
        a.append(int(input_data[i]))
    
    a.sort(reverse=True)
    
    ans = 0
    for i in range(n):
        if a[i] >= i:
            ans = i + 1
        else:
            break
            
    print(ans)

if __name__ == '__main__':
    solve()
cpp
/*
 * Author: 2026-10-09 22:30
 */
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long ll;

const int MAXN = 100005;

ll a[MAXN];
int n;

bool cmp(ll x, ll y) {
    return x > y;
}

void solve() {
    if (!(cin >> n)) return;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }
    
    sort(a, a + n, cmp);
    
    int ans = 0;
    for (int i = 0; i < n; ++i) {
        if (a[i] >= i) {
            ans = i + 1;
        } else {
            break;
        }
    }
    
    cout << ans << "\n";
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    solve();
    return 0;
}

复杂度

  • 时间复杂度:排序的时间复杂度为 O(nlog⁡n)\mathcal{O}(n \log n),随后只需遍历一次数组,时间复杂度为 O(n)\mathcal{O}(n)。总体时间复杂度为 O(nlog⁡n)\mathcal{O}(n \log n),可以轻松通过 1000ms 的时限。
  • 空间复杂度:保存所有人参数的数组空间为 O(n)\mathcal{O}(n),空间消耗远低于 128MB 的内存限制。

总结

这道题的绝妙之处在于“无限轮”下的收益平衡,并且题目给出了一个精妙的条件——初始每人有 nn 张牌。

1. 算法与完整推导、复杂度: 核心算法为:将 aa 数组降序排列,寻找最大的 kk 使得前 kk 个人全部满足 ak≥k−1a_k \ge k-1。详细推导见上文【思路】。 时间复杂度为 O(nlog⁡n)\mathcal{O}(n \log n)(瓶颈在排序),空间复杂度为 O(n)\mathcal{O}(n)。

2. 数据范围:

  • n≤105n \le 10^5
  • ai≤109a_i \le 10^9
  • 时限:1000ms
  • 内存:128MB

3. 输出格式: 一个整数,直接输出最终剩下的最少人数。最后带有换行,无任何多余空格。

4. T9 五项排查:

  • 多组数据:无。
  • 边界情况:n=1n=1 时直接输出 1;ai=0a_i=0 的极端情况均可处理。
  • 顺序与初始值:题目特意规定初始每人有 nn 张牌,这保证了在第一轮中(即使大家轮流拿牌),每人最多被拿走 n−1n-1 张牌,不会出现刚开局还未出手就被淘汰的“冤死”情况。这就将复杂的先后手博弈转化为了纯粹的轮均收益比较!
  • 数据类型:ai≤109a_i \le 10^9,使用 long long 避免溢出。
  • 格式:无行末空格。

5. 逐点数据表:

数据点 输入特征 输出 结果说明
2.in 2 个人,1 1 2 初始各自有一牌的拿牌能力,都不会出局。
4.in 3 个人,2 5 2 3 aia_i 降序为 5 2 2,全部 ≥2\ge 2,全部存活。
5.in n=100000n=100000 50012 大数据,有效验证 O(nlog⁡n)O(n \log n) 的算法效率。

6. 声明: 本题有官方数据,验证基准是与 .out 逐字节比对。

验证记录(R2 补充)

本题有官方数据 ⇒ 证据等级 ★★★★★(与官方 .out 逐字节比对)。

结论:真实数据 10/10 逐字节通过

★ 算法裁定:「官方数据站在 greedy(贪心)一边」

本题存在两个候选算法:greedy(贪心)与 M3(另一候选)。 验证方式是在全部 10 个数据点上逐点对比两者的输出:

项 结论
数据立场 官方数据选了 greedy
判据 greedy vs M3 的 10 点逐点对比

★ 结论分层的必要声明(T14): 这是数据驱动的裁定,不是数学证明。 ⇒ 报告中必须写「官方数据选择了 greedy」, 而不能写「greedy 是唯一正确答案」。

证据 | 声明

text
✅ 真实数据 10/10 逐字节(★★★★★)
✅ greedy 与 M3 在 10 个点上对比 ⇒ 数据选 greedy
⚠ 「greedy 正确」是数据裁定,不是普适证明(诚实边界)