天选之人

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

设恰好 p 个人抽到最大记号数 t,则总记号数必须落在 [p t, p t + (n-p)(t-1)],找到合法 t 后再贪心构造。

OJ: luogu

题目 ID: P7107

难度:普及+/提高

标签:数学构造思维

日期: 2026-06-20 06:15

题意

要把 k 张有记号的纸团分给 n 个人,每个人正好拿 m 张。

如果第 i 个人拿到的有记号纸团数是 x_i,那么要满足:

  • 0 <= x_i <= m
  • x_1 + x_2 + ... + x_n = k
  • 恰好有 p 个人的 x_i 等于全体最大值

如果能构造,就输出任意一组 x_im - x_i

思路

先看一个直接验证小数据的暴力:

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

i64 n, m, k, p;
i64 x[25];
bool found = false;

void print_answer() {
    cout << "YES\n";
    for (int i = 1; i <= n; i++) {
        cout << x[i] << ' ' << (m - x[i]) << '\n';
    }
}

void dfs(int pos, i64 left) {
    if (found) {
        return;
    }

    if (pos == n + 1) {
        if (left != 0) {
            return;
        }

        i64 mx = -1;
        for (int i = 1; i <= n; i++) {
            mx = max(mx, x[i]);
        }

        int cnt = 0;
        for (int i = 1; i <= n; i++) {
            if (x[i] == mx) {
                cnt++;
            }
        }

        if (cnt == p) {
            found = true;
            print_answer();
        }
        return;
    }

    for (i64 take = 0; take <= m && take <= left; take++) {
        x[pos] = take;
        dfs(pos + 1, left - take);
        if (found) {
            return;
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k >> p;
    dfs(1, k);

    if (!found) {
        cout << "NO\n";
    }

    return 0;
}

暴力版就是直接 DFS 枚举每个人拿到多少张有记号纸团,最后检查有没有恰好 p 个人并列最大。
它只能跑很小的数据,但很适合拿来对拍。

固定最大值 t

设最后恰好有 p 个人抽到了最大值 t

那么这 p 个人一共贡献了:

  • p * t

其余 n - p 个人既然不是最大值,就最多只能拿到 t - 1 张记号。
因此总记号数 k 必须满足:

  • p * t <= k <= p * t + (n - p) * (t - 1)

这就是整题最关键的不等式。

反过来,如果我们找到了某个 t 满足这个区间,那就一定能构造:

  1. 先让前 p 个人都拿 t 张记号
  2. 剩下的 k - p * t 张记号,往后面 n - p 个人里塞
  3. 每个人最多塞到 t - 1 为止

这样做完之后:

  • p 个人都等于 t
  • 其他人都严格小于 t
  • 总和又正好是 k

所以条件也是充分的。

怎么直接判断有没有这样的 t

从不等式直接整理:

  • p * t <= k,得到 t <= floor(k / p)
  • k <= p * t + (n - p) * (t - 1) = n * t - n + p

第二条继续化简得到:

  • t >= ceil((k + n - p) / n)

所以只要看这个区间里有没有整数 t 即可:

  • ceil((k + n - p) / n) <= t <= min(m, floor(k / p))

再注意几个特判:

  • p = 0 一定无解
  • k = 0 时,只有所有人都拿 0 张记号,也就是 p = n 才有解
  • p = n 时,所有人都必须一样,所以需要 k % n = 0

代码

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

i64 n, m, k, p;
i64 x[100005];

i64 ceil_div(i64 a, i64 b) {
    return (a + b - 1) / b;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m >> k >> p;

    if (p == 0) {
        cout << "NO\n";
        return 0;
    }

    if (k == 0) {
        if (p != n) {
            cout << "NO\n";
            return 0;
        }

        cout << "YES\n";
        for (int i = 1; i <= n; i++) {
            cout << 0 << ' ' << m << '\n';
        }
        return 0;
    }

    if (p == n) {
        if (k % n != 0) {
            cout << "NO\n";
            return 0;
        }

        i64 same = k / n;
        if (same > m) {
            cout << "NO\n";
            return 0;
        }

        cout << "YES\n";
        for (int i = 1; i <= n; i++) {
            cout << same << ' ' << (m - same) << '\n';
        }
        return 0;
    }

    // 设恰好 p 个人都抽到最大值 t。
    // 那么:
    // 1) 至少要有 p * t 张记号;
    // 2) 其余 n - p 个人每人至多 t - 1 张记号。
    i64 left = max<i64>(1, ceil_div(k + n - p, n));
    i64 right = min<i64>(m, k / p);

    if (left > right) {
        cout << "NO\n";
        return 0;
    }

    i64 t = left;
    i64 rest = k - p * t;

    for (int i = 1; i <= n; i++) {
        x[i] = 0;
    }
    for (int i = 1; i <= p; i++) {
        x[i] = t;
    }

    for (int i = p + 1; i <= n; i++) {
        i64 give = min(rest, t - 1);
        x[i] = give;
        rest -= give;
    }

    cout << "YES\n";
    for (int i = 1; i <= n; i++) {
        cout << x[i] << ' ' << (m - x[i]) << '\n';
    }

    return 0;
}

复杂度

主程序只做常数次计算和一次线性构造:

  • 时间复杂度 O(n)O(n)
  • 空间复杂度 O(n)O(n)

总结

这题表面是构造,核心其实是先把“恰好 p 个人并列最大”翻译成一个区间条件。

一旦固定最大值 t,问题就只剩两件事:

  1. 判断总和 k 能不能落进合法区间
  2. 如果能,就把剩下的记号数贪心塞给非赢家

思路非常短,但条件一定要写完整。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析