信奥赛C++提高组csp-s之组合数学专题课:鸽巢原理详解及案例实践

鸽巢原理是组合数学中一个看似简单但极具威力的工具,它在信奥赛提高组中常用于解决存在性证明和构造问题。下面我们将结合数学原理、实例以及编程案例实践进行详细讲解。
一、数学原理
鸽巢原理,又称抽屉原理,由德国数学家狄利克雷首先明确提出 。其核心思想非常直观,可以用以下两个定理来表述:
-
定理 1(基本形式) :如果将
n + 1个物体放入n个抽屉中,那么无论怎么放,至少有一个抽屉里含有至少两个物体 。- 证明 :反证法。假设每个抽屉最多放一个物体,那么
n个抽屉最多能放n个物体,这与有n + 1个物体矛盾。因此假设不成立,原命题为真 。
- 证明 :反证法。假设每个抽屉最多放一个物体,那么
-
定理 2(推广形式) :如果将多于
k × n + 1个物体放入n个抽屉中,那么至少有一个抽屉里含有至少k + 1个物体 。更一般地,若将m个物体放入n个抽屉,则至少有一个抽屉里含有不少于⌈m / n⌉个物体 。- 证明 :同样可用反证法证明。如果每个抽屉里的物体都不超过
k个,那么物体总数最多为k × n,与总数大于k × n矛盾。
- 证明 :同样可用反证法证明。如果每个抽屉里的物体都不超过
这个原理虽然简单,但在解决"保证存在"类型的问题时,往往能发挥关键作用。解决问题的关键在于识别并构造出合适的"抽屉"和"物体"。
二、数学例子
-
基本应用 :在长度为
n的数列中,一定能找到两个数,它们除以n-1的余数相同。- 解析 :一个数除以
n-1的余数只能是0, 1, ..., n-2,共有n-1种可能(即n-1个抽屉)。数列中有n个数(即n个物体)。根据定理1,必有两个数落在同一个余数抽屉里。
- 解析 :一个数除以
-
加强应用 :在一场至少进行了
n+1轮的比赛中,一定存在某个选手连续若干轮的得分之和为n的倍数。- 解析 :设前
i轮的得分总和为Sᵢ(i=1...T),并考虑S₀ = 0。现在看T+1个前缀和S₀, S₁, ..., Sₜ。根据定理1,这T+1个数除以n的余数只有n种可能,因此必有两个前缀和Sₐ和Sₑ(a < b) 除以n的余数相同。那么从第a+1轮到第b轮的得分之和Sₑ - Sₐ就是n的倍数。
- 解析 :设前
三、编程案例: Halloween treats
题目描述
每年万圣节都会遇到同样的问题:每个邻居只愿意在当天给出一定数量的糖果,无论有多少孩子上门,因此如果去得太晚,孩子可能什么也得不到。为了避免冲突,孩子们决定把所有糖果放在一起,然后平均分配。根据去年的万圣节经验,他们知道从每个邻居那里能得到多少糖果。由于他们更关心公平而不是糖果的数量,他们想选择一组邻居去拜访,这样在分配时每个孩子都能得到相同数量的糖果。如果剩下任何无法分配的糖果,他们不会满意。
你的工作是帮助孩子们并给出一个解决方案。
输入格式
输入包含多个测试用例。
每个测试用例的第一行包含两个整数 c 和 n ( 1 ≤ c ≤ n ≤ 100000 1 \leq c \leq n \leq 100000 1≤c≤n≤100000),分别代表孩子数量和邻居数量。下一行包含 n 个空格分隔的整数 a 1 , ... , a n ( 1 ≤ a i ≤ 100000 ) a_1, \ldots, a_n ( 1 \leq a_i \leq 100000 ) a1,...,an(1≤ai≤100000),其中 a i a_i ai 表示孩子们拜访邻居 i 时能得到的糖果数量。
最后一个测试用例后跟着两个零。
输出格式
对于每个测试用例,输出一行包含孩子们应该选择的邻居的索引(这里索引 i对应第 i个邻居,他们提供 a i a_i ai 颗糖果)。如果不存在使每个孩子至少得到一颗糖的解,则输出 "no sweets"。请注意,如果有多个解能使每个孩子至少得到一颗糖,你可以输出其中任意一个。
输入输出样例 1
输入 1
4 5
1 2 3 7 5
3 6
7 11 2 5 13 17
0 0
输出 1
3 5
2 3 4
思路分析
题目大意 :给定两个整数 c 和 n,以及一个由 n 个正整数组成的列表(代表邻居给的糖果数)。需要从这 n 个数中选出连续的一段 ,使得这段数的和能被 c 整除。题目保证 c ≤ n,并且一定有解。要求输出这段连续数的起始编号(从1开始)。
思路分析:
-
建立数学模型 :设糖果数的数组为
a[1..n]。我们需要找到两个索引l和r(1 ≤ l ≤ r ≤ n),使得(a[l] + a[l+1] + ... + a[r]) % c == 0。 -
引入前缀和 :定义前缀和
sum[i] = (a[1] + a[2] + ... + a[i]) % c,并定义sum[0] = 0。那么,连续子段[l, r]的和能被c整除的条件就等价于:
(sum[r] - sum[l-1]) % c == 0,即sum[r] == sum[l-1](模c意义下)。 -
应用鸽巢原理 :现在我们有
n+1个前缀和sum[0], sum[1], ..., sum[n]。每个sum[i]是模c后的结果,因此它的取值范围是{0, 1, ..., c-1},共有c种可能的余数(即c个抽屉)。因为c ≤ n,所以n+1 > c,即物体数量大于抽屉数量。- 根据鸽巢原理(定理1),这
n+1个前缀和sum中,至少存在两个不同的索引i和j(i < j),使得它们的值相等。 - 一旦找到这样的
i和j,根据上面的推导,从第i+1个邻居到第j个邻居给出的糖果总数就是c的倍数,这正是题目要求的解。
- 根据鸽巢原理(定理1),这
-
特殊情况 :如果某个
sum[j] == 0,那么从第1个到第j个邻居的和本身就是c的倍数,同样满足条件。
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100005; // 定义数组大小
int c, n; // c: 需要整除的数, n: 邻居数量
int a[N]; // a[]: 存放每个邻居给的糖果数
int s[N]; // s[]: 存放前缀和模c的结果
int p[N]; // p[]: 用于记录某个余数第一次出现的索引 (p[余数] = 索引)
int main() {
while (scanf("%d%d", &c, &n) && c && n) { // 循环读入,直到c=n=0
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
// 计算当前前缀和模c:前一个前缀和加上当前值,再取模
s[i] = (s[i-1] + a[i]) % c;
}
// 初始化标记数组,-1表示该余数还没出现过
// 注意:余数的范围是 [0, c-1]
memset(p, -1, sizeof(p));
int l = 0, r = 0; // l和r用于记录找到的连续段的起始和结束位置
for (int i = 0; i <= n; ++i) { // 从0到n遍历所有前缀和
if (p[s[i]] != -1) {
// 如果当前余数s[i]之前出现过,说明找到了解
// 之前出现的位置是p[s[i]],注意p[s[i]]存的是索引,这个索引对应的前缀和是s[p[s[i]]]
// 那么解就是从索引 p[s[i]]+1 到 i
l = p[s[i]] + 1; // 起始位置是之前出现位置的下一个
r = i; // 结束位置是当前位置
break;
} else {
// 如果当前余数是第一次出现,记录下它出现的位置
p[s[i]] = i;
}
}
// 输出结果
if (l != 0) { // 确保找到了解
for (int i = l; i <= r; ++i) {
printf("%d ", i); // 题目要求输出编号,从1开始
}
printf("\n");
}
}
return 0;
}
功能分析
-
数据结构:
a[N]:存储输入的原始数据。s[N]:存储到当前位置为止的累加和对c取模的结果。这是实现鸽巢原理的关键。p[N]:这是一个"桶"或"标记"数组。p[余数值]记录了该余数第一次 出现时的索引。例如,当s[3] = 2时,我们执行p[2] = 3。如果后续某个s[j]也等于2,我们就能通过p[2]快速找到对应的起点。
-
核心逻辑:
- 代码的核心是
for循环,它遍历了所有n+1个前缀和(包括s[0])。 - 每次得到一个新的余数
s[i],程序就检查这个余数是否已经在p数组中登记过(即p[s[i]]是否不等于 -1)。 - 如果没有登记过,说明这是该余数第一次出现,我们将当前位置
i记录到p[s[i]]中。 - 如果登记过,则意味着找到了两个相同余数的前缀和,这正是鸽巢原理保证的结果。此时,
p[s[i]]存储的是之前相同余数出现的位置i,那么连续段就是从p[s[i]] + 1到i。
- 代码的核心是
-
正确性保证:
- 由于
c ≤ n,所以有n+1个物体和c个抽屉,满足鸽巢原理的使用条件,保证了相同余数必然存在。 - 算法的时间复杂度是 O(n),空间复杂度是 O(n),可以高效处理题目范围内的数据。
- 由于
更多系列知识,请查看专栏:《信奥赛C++提高组csp-s知识详解及案例实践》:
https://blog.csdn.net/weixin_66461496/category_13113932.html
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新)
https://blog.csdn.net/weixin_66461496/category_13125089.html
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、csp/信奥赛C++,完整信奥赛系列课程(永久学习):
https://edu.csdn.net/lecturer/7901 点击跳转
· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}