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

信奥赛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 矛盾。

这个原理虽然简单,但在解决"保证存在"类型的问题时,往往能发挥关键作用。解决问题的关键在于识别并构造出合适的"抽屉"和"物体"

二、数学例子

  1. 基本应用 :在长度为 n 的数列中,一定能找到两个数,它们除以 n-1 的余数相同。

    • 解析 :一个数除以 n-1 的余数只能是 0, 1, ..., n-2,共有 n-1 种可能(即 n-1 个抽屉)。数列中有 n 个数(即 n 个物体)。根据定理1,必有两个数落在同一个余数抽屉里。
  2. 加强应用 :在一场至少进行了 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

思路分析

题目大意 :给定两个整数 cn,以及一个由 n 个正整数组成的列表(代表邻居给的糖果数)。需要从这 n 个数中选出连续的一段 ,使得这段数的和能被 c 整除。题目保证 c ≤ n,并且一定有解。要求输出这段连续数的起始编号(从1开始)。

思路分析

  1. 建立数学模型 :设糖果数的数组为 a[1..n]。我们需要找到两个索引 lr (1 ≤ l ≤ r ≤ n),使得 (a[l] + a[l+1] + ... + a[r]) % c == 0

  2. 引入前缀和 :定义前缀和 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 意义下)。

  3. 应用鸽巢原理 :现在我们有 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 中,至少存在两个不同的索引 ij ( i < j ),使得它们的值相等。
    • 一旦找到这样的 ij,根据上面的推导,从第 i+1 个邻居到第 j 个邻居给出的糖果总数就是 c 的倍数,这正是题目要求的解。
  4. 特殊情况 :如果某个 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;
}

功能分析

  1. 数据结构

    • a[N]:存储输入的原始数据。
    • s[N]:存储到当前位置为止的累加和对 c 取模的结果。这是实现鸽巢原理的关键。
    • p[N]:这是一个"桶"或"标记"数组。p[余数值] 记录了该余数第一次 出现时的索引。例如,当 s[3] = 2 时,我们执行 p[2] = 3。如果后续某个 s[j] 也等于2,我们就能通过 p[2] 快速找到对应的起点。
  2. 核心逻辑

    • 代码的核心是 for 循环,它遍历了所有 n+1 个前缀和(包括 s[0])。
    • 每次得到一个新的余数 s[i],程序就检查这个余数是否已经在 p 数组中登记过(即 p[s[i]] 是否不等于 -1)。
    • 如果没有登记过,说明这是该余数第一次出现,我们将当前位置 i 记录到 p[s[i]] 中。
    • 如果登记过,则意味着找到了两个相同余数的前缀和,这正是鸽巢原理保证的结果。此时,p[s[i]] 存储的是之前相同余数出现的位置 i,那么连续段就是从 p[s[i]] + 1i
  3. 正确性保证

    • 由于 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;
}
相关推荐
njidf2 小时前
C++与Qt图形开发
开发语言·c++·算法
qwehjk20082 小时前
代码动态生成技术
开发语言·c++·算法
承渊政道3 小时前
【优选算法】(实战体会位运算的逻辑思维)
数据结构·c++·笔记·学习·算法·leetcode·visual studio
m0_716765233 小时前
C++提高编程--STL常用容器(set/multiset、map/multimap容器)详解
java·开发语言·c++·经验分享·学习·青少年编程·visual studio
承渊政道3 小时前
【优选算法】(实战推演模拟算法的蕴含深意)
数据结构·c++·笔记·学习·算法·leetcode·排序算法
朽棘不雕3 小时前
c++中为什么new[]和delete[]要配对使用
c++
elseif1234 小时前
出题团招人
c++
不想写代码的星星5 小时前
SFINAE 的演进:从替换失败不是错误,到 Concepts 的优雅
c++
2401_878530215 小时前
自定义内存布局控制
开发语言·c++·算法