设恰好 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 <= mx_1 + x_2 + ... + x_n = k- 恰好有
p个人的x_i等于全体最大值
如果能构造,就输出任意一组 x_i 和 m - 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 满足这个区间,那就一定能构造:
- 先让前
p个人都拿t张记号 - 剩下的
k - p * t张记号,往后面n - p个人里塞 - 每个人最多塞到
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;
}复杂度
主程序只做常数次计算和一次线性构造:
- 时间复杂度
- 空间复杂度
总结
这题表面是构造,核心其实是先把“恰好 p 个人并列最大”翻译成一个区间条件。
一旦固定最大值 t,问题就只剩两件事:
- 判断总和
k能不能落进合法区间 - 如果能,就把剩下的记号数贪心塞给非赢家
思路非常短,但条件一定要写完整。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
