CSP-S 提高级课程中心 / 数学进阶 / 第49讲
CSP-S 提高级 · 数学进阶 · s6-08

鸽巢原理 与 二项式定理

一个"多一只鸽子就必然撞巢"的朴素直觉,加上一套把 (a+b)n 彻底拆开的代数工具 —— 组合计数的两块基石。

2 大核心定理 4 条组合数性质 3 个递进案例 8 处交互动画 12 道洛谷原题
本讲 · 第 49/52 讲鸽巢原理与二项式定理
模块 01

情景导航

用一个"看似不可能赢的赌局"引出鸽巢原理,再把二项式定理还原成"选哪几个因子出 b"的计数游戏。

故事生日悖论的赌局

机房里流传着一个赌局:"咱们班 60 个人,我赌至少有两个人同一天生日。谁跟我赌?"

很多同学第一反应是:一年 365 天,60 个人才占 60/365 ≈ 16%,两个人撞生日的概率应该很小吧?于是纷纷应战。结果每次都是那位同学赢。

真相是:60 个人里至少有一对同生日的概率高达 99.4%。因为"同一天生日"不是"某个人和我同生日",而是任意两人之间的两两组合 —— 60 个人有 C(60,2) = 1770 对,而每对撞上的概率是 1/365。

算一下:没有任何两人同生日的概率是 (364/365)×(363/365)×…×(306/365) ≈ 0.006,那么至少一对同生日的概率就是 1 − 0.006 = 0.994。

这就是著名的生日悖论("悖论"只是因为它违反直觉,逻辑上毫无矛盾)。它背后站着的,正是本讲的第一个主角 —— 鸽巢原理(抽屉原理):366 只鸽子放进 365 个巢,必然有一只巢里有两只鸽子。

而当我们试图精确计算 C(60,2) 这类数字时,又会遇到第二个主角:二项式定理 —— 它把 (1+x)n 一次性展开,展开式里第 k 项的系数恰好就是 C(n,k)。

💡 故事背后的算法思想

鸽巢原理是"存在性证明"的万能锤。它不告诉你在哪儿撞,只斩钉截铁地说"一定撞"。竞赛里凡是题目问"是否必然存在""至少有多少"的,第一反应就该是抽屉原理。

二项式定理是"系数计算"的总公式。凡是问"展开式中 xnym 的系数是多少""所有系数之和是多少",答案都藏在 C(n,k) 里。它的证明本身就是一次教科书级的组合双计数:把 n 个 (a+b) 相乘,每个括号里选 a 还是选 b,选 k 次 b 的方案数就是 C(n,k)。

两者一个负责"证明存在",一个负责"算出数目",合起来就构成了组合数学中最常用的两根杠杆。

①生活类比
🕊️

鸽子进巢

6 只鸽子只有 5 个巢,无论怎么分,总有一个巢里挤着至少 2 只鸽子。

→ ⌊(n−1)/k⌋+1
🧦

摸袜子

抽屉里只有黑、白两色袜子,摸出 3 只,必有 2 只同色。3 > 2,抽屉不够分。

→ 颜色 = 巢
📅

生日碰撞

23 个人就有一半概率撞生日,因为比较的是 C(23,2)=253 对而非 23 个人。

→ 对数增长
🎒

余数分抽屉

4 个数按除以 3 的余数放进 3 个抽屉,必有两只挤在一起,它们的差就是 3 的倍数。

→ 构造抽屉
②核心定义

鸽巢原理(抽屉原理):把 n 个物体放进 k 个盒子,若 n > k,则至少有一个盒子里装了 ≥ 2 个物体。

加强形式:把 n 个物体放进 k 个盒子,若 n > k,则至少有一个盒子里装了至少 ⌊(n−1)/k⌋ + 1 个物体。

二项式定理:(a+b)n = Σk=0..n C(n,k)·an−k·bk,其中 C(n,k) 称为二项式系数。

多项式系数:把 n 个物体分成 r 组,第 i 组恰有 ki 个(Σki = n),方案数为 n! / (k1!·k2!···kr!),记作 (n; k1,k2,…,kr)。

📌 本讲核心知识地图

板块核心内容对应大纲
鸽巢基本形式n 物 k 盒,n>k ⇒ 必有 2 物同盒【6】鸽巢原理
鸽巢加强形式必有盒子 ≥ ⌊(n−1)/k⌋+1 个物体【6】鸽巢原理
构造抽屉按余数分组 / 按区间分组 / 按子集分组【6】鸽巢原理
二项式定理(a+b)n = Σ C(n,k)an−kbk【6】二项式定理
组合数性质对称性、帕斯卡恒等式、系数和 2n、奇偶判定【6】二项式定理
杨辉三角递推 C(n,k)=C(n−1,k−1)+C(n−1,k)【6】二项式定理
多项式系数n!/(k1!…kr!),二项系数推广【6】二项式定理
③核心口诀
🔑 鸽巢原理

鸽子多于巢,必有一巢两只;
问"是否必然有",先数物再数盒,物 > 盒就有戏!
要算"最少几个",⌊(n−1)/k⌋ + 1 别忘掉。

🔑 二项式定理

n 个括号相乘,每个括号选 a 或 b;
选 k 次 b 的方案数,就是 C(n,k);
令 a=b=1 得 2n,令 a=1,b=−1 得 奇偶差。

🔑 组合数性质

对称 C(n,k)=C(n,n−k);
递推 C(n,k)=C(n−1,k−1)+C(n−1,k);
系数和 Σ C(n,k) = 2n。

模块 02

知识探究

先驯服鸽巢(会数、会构造、会算下界),再拿下二项式定理(会展开、会用性质、会算系数)。

1鸽巢原理的基本形式 核心

鸽巢原理(Pigeonhole Principle,又叫抽屉原理)是组合数学中最"便宜"却最有力的工具。它的基本形式只有一句话:

1
🕊️
数物体

n = 鸽子的数量

2
🪺
数盒子

k = 巢(抽屉)的数量

3
⚖️
比大小

判断 n 与 k 的关系

4
✅
得结论

n > k ⇒ 必有 2 物同盒

📐 证明其实只有一句"反证法"

假设每个盒子里最多放 1 个物体,那么 k 个盒子最多只能放下 k 个物体。但题目说有 n > k 个物体,矛盾!所以至少有一个盒子里放了 ≥ 2 个物体。证毕。

关键理解:鸽巢原理是存在性定理,它保证"有",但不告诉你是哪一个。想找出具体是哪两个,还得配合算法去枚举或分桶。

反向使用(也是竞赛最爱):如果要求"保证有重复",那么物体的数量必须严格大于盒子的数量。反过来,如果要"设计一种无重复的放法",那就必须 n ≤ k。这一正一反,就是构造题的基本功。

🕊️ 🕊️ 🕊️ 🕊️ 🕊️🕊️ 第 5 个巢被迫装下 2 只鸽子 物体数 n=6 > 盒子数 k=5 ⇒ 必有重复 ⌊(6−1)/5⌋+1 = 2 (加强形式给出下界)
图 1:6 只鸽子 5 个巢,必有一巢至少 2 只
🎬 交互演示:把鸽子依次放进巢

下面一行格子代表 6 只鸽子的"落巢选择"。观察高亮依次扫过 —— 最后一只无巢可选,只能与已有的鸽子同巢。

鸽1→巢1
鸽2→巢2
鸽3→巢3
鸽4→巢4
鸽5→巢5
鸽6→?巢!
已就位 当前鸽子
2鸽巢原理的加强形式 必考公式

基本形式只说"至少 2 只",但竞赛往往追问得更细:到底至少几只?这就需要加强形式。

📐 加强形式(广义鸽巢原理)

把 n 个物体放进 k 个盒子,则至少有一个盒子里装的物体个数 ≥ ⌊(n−1)/k⌋ + 1(等价写法:≥ ⌈n/k⌉)。

证明:反证。若每个盒子最多装 ⌊(n−1)/k⌋ 个,那么总共最多装 k·⌊(n−1)/k⌋ ≤ k·(n−1)/k = n−1 个,与"共 n 个"矛盾。所以必有一个盒子的个数 ≥ ⌊(n−1)/k⌋ + 1。

为什么不用 ⌈n/k⌉?两个式子其实恒等(可以自己验算几组数),但 ⌊(n−1)/k⌋+1 在 C++ 里写成 (n-1)/k + 1,整数除法天然向下取整,一行搞定,不用考虑浮点误差。

物体数 n盒子数 k⌊(n−1)/k⌋+1含义
6526 只鸽子 5 个巢,必有巢 ≥ 2 只
103410 个球 3 个盒,必有盒 ≥ 4 个球
100715100 人分 7 组,必有组 ≥ 15 人
nn1物体不超盒子,无法保证任何重复
n+1n2退回基本形式,必有重复
🎬 交互演示:10 个球放进 3 个盒,最少保证几个?
1物体 n = 10,盒子 k = 3,先套公式 (n-1)/k + 1 = (10−1)/3 + 1 = 3 + 1 = 4
2反证起点:假设"每个盒子都最多装 3 个",那么 3 个盒子最多装 3×3 = 9 个球
3但球有 10 个,9 < 10,装不下 —— 矛盾!所以"每个盒子 ≤ 3"不成立
4一种最"挤"的合法放法:[4, 3, 3] —— 最大的盒子装了 4 个
5注意 [5, 3, 2] 也可以,但下界说的是"必有盒子 ≥ 4",4 就是能保证的最小值
6结论:必有盒子 ≥ ⌊(10−1)/3⌋+1 = 4 个球 —— 与公式完全一致 ✔
💡 记忆:(n-1)/k 是"假设不超标时每个盒子的最大容量",再加 1 就是被迫突破的下界。
🟦 基本形式(够用就好)

问题只问"是否存在重复"时,直接说 n > k ⇒ 有重复。

例:4 个整数里必有两个数奇偶性相同(2 个抽屉:奇 / 偶)。

🟩 加强形式(要精确下界)

问题问"至少有几个"时,必须用 (n-1)/k + 1。

例:100 件衣服分给 7 个柜子,必有一个柜子 ≥ 15 件。

3构造抽屉的三大套路 技巧

鸽巢原理本身不难,难的是"怎么定义盒子"。题目不会告诉你哪是鸽子哪是巢,这一步全靠自己构造。提高组真题里最高频的就是下面三招。

① 按余数分组(模 m 的余数类)
  • 构造:让盒子 = a mod m 的取值,共 m 个盒子
  • 结论:任取 m+1 个整数,必有两个数之差是 m 的倍数
  • 为什么:余数相同 ⇒ 差被 m 整除
  • 这是"倍数差"类证明题的万能钥匙
② 按区间分组(相邻成对 / 长度定长)
  • 构造:把值域切成若干等长(或按相邻两数配对)的盒子
  • 结论:从 [1, 2n] 中取 n+1 个数,必有两个数落在同一对 {2i−1, 2i} 里
  • 妙处:同一对里两数必互质(相邻整数)
  • 经典题:"取 n+1 个数必有两个互质"

③ 按子集 / 状态分组(高阶套路)

最常见的两类:把"取了哪些数"编码成 0/1 状态(共 2m 种,适合状压类问题),或者把前缀和按模 m 的余数分组(共 m 个盒子,n+1 个前缀和 ⇒ 必有同余,从而"存在一段连续区间的和是 m 的倍数")。第三招是鸽巢原理能与算法真正结合的地方,本讲题 3 会完整实现。

构造抽屉的思考顺序(考场口诀):先问"结论里的'相同'是什么"→ 那个"相同的东西"就是盒子;再数一共有多少种"相同",那就是盒子数 k;最后数物体数 n,比较 n 与 k。

STEP 1读懂结论
STEP 2找相同量
STEP 3数盒子 k
STEP 4数物体 n
STEP 5比较得结论
观察节点依次点亮,每一步的要点见节点提示
4经典结论与生日悖论 经典

🏆 经典结论 1:任取 n+1 个整数,必有两个数之差是 n 的倍数

构造 n 个抽屉:第 r 个抽屉放所有模 n 余 r 的整数(r = 0, 1, …, n−1)。现在有 n+1 个整数,分散到 n 个抽屉中,由鸽巢原理必有两个整数落在同一个抽屉。它们模 n 同余,所以它们的差被 n 整除。

特例(大纲常考的"3 的倍数"版本):任取 4 个整数,必有两个数之差是 3 的倍数 —— 因为 3 个抽屉(余 0、1、2)装了 4 个数。

例:4 个数 7, 4, 9, 2,模 3 的余数分别是 1, 1, 0, 2。7 与 4 都落在"余 1"抽屉里,差 7−4 = 3,正是 3 的倍数。✔

📅 经典结论 2:生日悖论的直觉修正

很多人误以为"生日问题"比较的是 1/365 与 人数/365,其实比较的是 1/365 与 对数 C(n,2)。当 n = 23 时,C(23,2) = 253,而 253/365 ≈ 0.69,此时"至少一对同生日"的概率已经过半(实际 50.7%)。n = 60 时概率达 99.4%。

精确公式:P(至少一对同生日) = 1 − 365!/((365−n)!·365n),当 n > 365 时由鸽巢原理必然为 1。

竞赛意义:这解释了"为什么随机数据下哈希冲突、生日碰撞来得比想象中快得多"——冲突概率随元素个数平方增长,这正是哈希表要选大模数、还是要把冲突概率压到 10−9 量级的原因。

人数 n至少一对同生日的概率直观
1011.7%还看不出什么
2350.7%刚好过半,最反直觉的一点
4190.3%几乎稳了
6099.4%故事里的赌局必胜
366100%鸽巢原理直接给出 1
🎬 交互演示:23 个人为什么就有一半概率撞生日?
1误区:很多人以为比较的是"人数 23 vs 天数 365",觉得 23/365 ≈ 6% 很安全
2真相:比较的是两两配对的对数 C(23,2) = 23×22/2 = 253 对
3第 1 人进教室:不可能撞;第 2 人:1 对比较;第 3 人:又多 2 对……配对数快速增长
4"全不撞"概率 = 365/365 × 364/365 × 363/365 × … × 343/365 ≈ 0.493
5所以"至少一对同生日" = 1 − 0.493 = 50.7%,刚过半!
6对照:n = 10 时概率只有 11.7%,n = 60 时高达 99.4% —— 增长是平方级的
💡 这正是哈希冲突"来得比想象快"的数学原因:冲突概率 ≈ n²/(2M)。
5二项式定理 核心

二项式定理:对任意非负整数 n,有

(a + b)n = C(n,0)an + C(n,1)an−1b + … + C(n,n)bn = Σk=0..n C(n,k)·an−k·bk

🔍 组合证明(比归纳法好懂一百倍)

(a+b)n 就是 n 个括号 (a+b) 相乘。展开时,每一个括号都要"贡献"一项 —— 要么贡献 a,要么贡献 b。于是总共会得到 2n 个乘积项(每项都是 aibn−i 的形式)。

把同类项合并:含有 k 个 b 的那些项一共有多少个?答案是在 n 个括号里"选 k 个来出 b",方案数为 C(n,k)。所以 bk 前面的系数就是 C(n,k)。

一句话记住:系数 C(n,k) 就是"从 n 个括号中挑 k 个出 b"的方案数。

🎬 交互演示:(a+b)3 展开的逐项配对
1写出三个括号:(a+b)(a+b)(a+b),标记为第 1、2、3 个因子
2每个括号有 2 种选择(出 a 或出 b),共 2×2×2 = 8 个乘积项,这正是 ΣC(3,k) = 8
3k = 0(出 0 个 b):只有 aaa 一种,系数 C(3,0) = 1 → 得到 a³
4k = 1(出 1 个 b):第 1 / 第 2 / 第 3 个括号出 b,共 3 种,系数 C(3,1) = 3 → 得到 3a²b
5k = 2(出 2 个 b):选哪两个括号出 b,C(3,2) = 3 → 得到 3ab²
6k = 3(出 3 个 b):只有 bbb 一种,C(3,3) = 1 → 得到 b³
7合并:(a+b)³ = a³ + 3a²b + 3ab² + b³,系数 1 3 3 1 正是杨辉三角第 3 行
💡 把 a = b = 1 代进去:1+3+3+1 = 8 = 2³,这就是"系数和 = 2n"的来历。
C++二项式系数 C(n,k) 的三种求法
// 法一:杨辉三角递推(n ≤ 5000 时首选,O(n²) 预处理 O(1) 查询)
long long C[5005][5005];
void initPascal(int n) {
    C[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        C[i][0] = 1;
        for (int j = 1; j <= i; j++)
            C[i][j] = C[i-1][j-1] + C[i-1][j];   // 帕斯卡恒等式
    }
}

// 法二:阶乘 + 逆元(模素数 p,n ≤ 10^7,单点 O(log p))
const long long P = 1000000007;
long long fac[1000006], ifac[1000006];
long long fpow(long long a, long long b, long long m) {
    long long r = 1; a %= m;
    while (b) { if (b & 1) r = r * a % m; a = a * a % m; b >>= 1; }
    return r;
}
long long binom(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fac[n] * ifac[k] % P * ifac[n-k] % P;
}

// 法三:Lucas 定理(n, m 极大而模数 p 较小时,逐位拆 p 进制)
long long lucas(long long n, long long m, long long p) {
    if (m == 0) return 1;
    return binom(n % p, m % p) * lucas(n / p, m / p, p) % p;
}
运行结果
initPascal(5) 后 C[5][0..5] = 1 5 10 10 5 1
binom(10, 3) = 120      fpow 验证:10!/(3!7!) = 3628800/(6*5040) = 120 ✔
lucas(10, 3, 7):10 = 13₇、3 = 03₇ → C(3,3)·C(1,0) = 1
(验证:C(10,3) = 120,120 mod 7 = 1 ✔)

🔍 代码逐段解析 · C(n,k) 三种求法

法一 initPascal —— 二维数组 C[i][j] 存 C(i,j),先打边界 C[0][0]=C[i][0]=1,内层用帕斯卡恒等式递推。O(n²) 预处理后每次查询 O(1)。易错:数组开 5005×5005 的 long long 约 200MB,必须模意义下使用或改用滚动数组,且全局数组才不爆栈。

法二 binom —— 先线性筛出 fac/ifac,查询时 fac[n] * ifac[k] % P * ifac[n-k] % P。两处取模缺一不可:三个 long long 相乘会溢出。易错:忘判 k < 0 || k > n,k > n 时 ifac 下标为负,直接 RE。

法三 lucas —— 把 n、m 拆成 p 进制逐位相乘:binom(n%p, m%p) * lucas(n/p, m/p, p)。递归出口 m == 0 返回 1。易错:某一位出现 m%p > n%p 时整个结果就是 0(如 C(100,3) mod 7,100 的 7 进制是 202、3 是 003,最高"位"比较 C(2,3) = 0 → 答案 0)。

6组合数四大性质 高频
性质公式组合意义竞赛用法
对称性C(n,k) = C(n,n−k)选 k 个留下 ≡ 选 n−k 个丢掉把 k > n/2 折回小的一半
帕斯卡恒等式C(n,k)=C(n−1,k−1)+C(n−1,k)看最后一个元素:选它或不选它杨辉三角递推、DP 转移式
系数和Σk=0..n C(n,k) = 2nn 个元素的所有子集个数令 (1+1)n,求系数和
交错和Σ(−1)kC(n,k) = 0 (n≥1)奇数大小子集 = 偶数大小子集令 (1−1)n,求奇偶系数差

⚖️ 从二项式定理"一眼看出"所有性质

把 (a+b)n = Σ C(n,k)an−kbk 当成一个"参数接口",代入不同的 a、b 就能刷出一串恒等式:

· 令 a = 1, b = 1 ⇒ (1+1)n = 2n = Σ C(n,k)  系数和

· 令 a = 1, b = −1 ⇒ 0 = Σ (−1)kC(n,k)  交错和

· 令 a = 1, b = x ⇒ (1+x)n = Σ C(n,k)xk  母函数

· 两边对 x 求导再令 x = 1 ⇒ Σ k·C(n,k) = n·2n−1  加权和

· 取 n 为奇数并分离奇偶项 ⇒ 奇数项系数和 = 偶数项系数和 = 2n−1。

🎬 交互演示:把 (a+b)n 当"参数接口"逐个代入
1接口原型:(a+b)n = Σ C(n,k)·an−k·bk,a、b 就是两个"旋钮"
2旋到 a=1, b=1:(1+1)n = Σ C(n,k) = 2n —— 求出"所有系数之和"
3旋到 a=1, b=−1:(1−1)n = 0 —— 奇数项系数和与偶数项系数和恰好抵消
4旋到 a=1, b=2:(1+2)n = 3n —— 得到 Σ C(n,k)·2k,即"每元素三状态"计数
5旋到 a=1, b=x:(1+x)n —— 保留字母,得到母函数,还能对 x 求导得 Σ k·C(n,k) = n·2n−1
6校验 n = 3:(1+x)³ = 1 + 3x + 3x² + x³,代入 x=1 得 8 = 2³ ✔,求导代入 x=1 得 3+6+3 = 12 = 3×4 ✔
💡 考场上凡遇到"Σ C(n,k)·(某数)k"型求和,第一反应就是:这是某个 (a+b)n 的代入结果。
❌ 常见误解:所有系数都是偶数

(1+x)4 = 1 + 4x + 6x² + 4x³ + x⁴,中间三个是偶数,但首尾两个是 1(奇数)。

误以为"n 一大就全偶数",会导致奇偶统计题直接错。

✅ 正确认识:奇数个数 = 2popcount(n)

由 Lucas 定理(模 2),C(n,k) 为奇数 ⟺ k 的二进制每一位都 ≤ n 对应位,即 k 是 n 的子掩码。

这样的 k 有 2popcount(n) 个。n = 5 = 101₂ 时,k ∈ {0,1,4,5},恰好 4 个。

7杨辉三角与奇偶性 图形直觉
📊 杨辉三角前 6 行(每行左右两端恒为 1)
n=01
n=11
1
n=21
2
1
n=31
3
3
1
n=41
4
6
4
1
n=51
5
10
10
5
1
第 5 行:1 5 10 10 5 1,和 = 32 = 2⁵ ✔(每一行都是二项式系数)

🎨 奇偶性:把杨辉三角涂成黑白,会浮现"谢尔宾斯基三角形"

规则很简单:奇数涂黑,偶数留白。第 0~7 行涂出来是这样(■ = 奇,· = 偶):

■
■ ■
■ · ■
■ ■ ■ ■
■ · · · ■
■ ■ · · ■ ■
■ · ■ · ■ · ■
■ ■ ■ ■ ■ ■ ■ ■

这不是巧合 —— 它是分形。原因就是前面说的:第 n 行有 2popcount(n) 个奇数,而这一行共有 n+1 个数。当 n = 2m−1 时 popcount(n) = m,奇数个数 = 2m = n+1,整行全奇(图中第 1、3、7 行全是 ■ 的原因)。

竞赛价值:凡是问"C(n,k) 是不是奇数""第 n 行有多少个奇数系数",答案都在 popcount 里,一行 __builtin_popcountll(n) 就能 O(1) 求出,不用递推!

C++杨辉三角奇偶行 O(n) 生成 + 位运算 O(1) 判定
#include <bits/stdc++.h>
using namespace std;
int main() {
    long long n; cin >> n;        // 输出第 n 行 C(n,k) 的奇偶性
    // 法一:位运算判定 —— C(n,k) 为奇数 ⟺ (k & (n-k)) == 0
    for (long long k = 0; k <= n; k++)
        cout << ((k & (n - k)) == 0 ? 1 : 0) << " ";
    cout << "\n";
    // 奇数系数总个数 = 2^popcount(n)
    cout << (1LL << __builtin_popcountll(n)) << "\n";
    return 0;
}
// 等价判据(Lucas 模 2):(k & n) == k,即 k 是 n 的二进制子掩码
运行结果
输入: 5
输出: 1 1 0 0 1 1
      4
说明: C(5,k)=1,5,10,10,5,1 → 奇数 4 个;popcount(5)=popcount(101₂)=2,2²=4 ✔
🎬 交互演示:第 6 行奇偶逐位判定((k & n) == k)
1n = 6 = 110₂,popcount = 2,预言奇数个数 = 2² = 4 个。第 6 行系数:1, 6, 15, 20, 15, 6, 1
2k = 0 = 000₂:0 & 6 = 0 = k,是子掩码 → C(6,0) = 1,奇 ✔
3k = 1 = 001₂:1 & 6 = 0 ≠ 1(k 的最低位是 1,n 的最低位是 0),不是子掩码 → C(6,1) = 6,偶 ✔
4k = 2 = 010₂:2 & 6 = 2 = k → C(6,2) = 15,奇 ✔;k = 3 = 011₂:3 & 6 = 2 ≠ 3 → C(6,3) = 20,偶 ✔
5k = 4 = 100₂:4 & 6 = 4 = k → C(6,4) = 15,奇 ✔;k = 5 = 101₂:5 & 6 = 4 ≠ 5 → 6,偶 ✔
6k = 6 = 110₂:6 & 6 = 6 = k → C(6,6) = 1,奇 ✔。奇数下标 {0,2,4,6} 共 4 个,与预言一致,整行奇偶 = 1 0 1 0 1 0 1 ✔
💡 子掩码判据一行代码 (k & n) == k 免去递推整个杨辉三角 —— n = 106 也只需 O(n)。

🔍 代码逐段解析 · 杨辉三角奇偶行

第 1~4 行(读入) —— long long n 接住输入,题目 n ≤ 106 虽 int 够用,但后面 n - k、位运算统一用 long long 更稳。

第 6~9 行(逐位判定循环) —— k 从 0 扫到 n,判据 (k & (n - k)) == 0:k 与 n−k 没有公共的 1 位(二进制加法 k + (n−k) = n 无进位,即 Kummer 定理的进位次数为 0)⟺ C(n,k) 为奇数。输出 1 或 0,空格分隔。

第 11 行(附加统计) —— 1LL << __builtin_popcountll(n) 直接给出第 n 行奇数总个数 2popcount(n);注意必须写 1LL,否则 1 << 40 这类会按 int 溢出。

易错点 1 —— 判据写反:有人写 (k & n) != 0,那是"有公共位"而不是"子掩码",两者完全不同。

易错点 2 —— 用 __builtin_popcount(32 位版)统计 long long 的 n,高位全被截断。

8多项式系数(二项式定理的推广) 进阶

二项式定理处理两个变量,多项式定理处理 r 个变量。套路完全一样,只是"选哪几个括号出 b"变成了"把 n 个位置分成 r 组"。

📐 多项式定理

(x1 + x2 + … + xr)n = Σ (n; k1,k2,…,kr) · x1k1x2k2…xrkr,求和跑遍所有 k1+…+kr = n 的非负整数解。

其中多项式系数:(n; k1,…,kr) = n! / (k1!·k2!···kr!)

组合意义("分堆模型"):把 n 个不同的小球分给 r 个不同的盒子,第 i 个盒子恰放 ki 个,方案数就是它。计算时按"从 n 个里选 k1 个,再从剩下的里选 k2 个…"连乘,化简后得到上面的阶乘形式。

与二项式系数的关系:取 r = 2 就退化回 C(n,k1) = n!/(k1!k2!),完全一致。

C++多项式系数模板(模素数 1e9+7)
const long long MOD = 1000000007;
long long fac[2000006], ifac[2000006];

long long fpow(long long a, long long b) {
    long long r = 1; a %= MOD;
    while (b) { if (b & 1) r = r * a % MOD; a = a * a % MOD; b >>= 1; }
    return r;
}
void init(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i-1] * i % MOD;
    ifac[n] = fpow(fac[n], MOD - 2);              // 费马小定理求逆元
    for (int i = n; i >= 1; i--) ifac[i-1] = ifac[i] * i % MOD;
}
// 多项式系数 (n; k1,k2,...,kr)
long long multinom(vector<int>& k) {
    int n = 0; for (int x : k) n += x;
    long long res = fac[n];
    for (int x : k) res = res * ifac[x] % MOD;
    return res;
}
运行结果
multinom({2,2}) = 4!/(2!·2!) = 24/4 = 6      ← 即 C(4,2)
multinom({1,1,1}) = 3!/(1!1!1!) = 6           ← (x+y+z)³ 中 xyz 的系数
multinom({2,1,0}) = 3!/(2!1!0!) = 3           ← (x+y+z)³ 中 x²y 的系数
🎬 交互演示:(x+y+z)³ 中 xyz 的系数是怎么"分堆"出来的
1目标:求 (x+y+z)³ 中 xyz 项的系数,即把 3 个括号分成 x 组 / y 组 / z 组各 1 个
2第 1 步:从 3 个括号里选 1 个出 x —— C(3,1) = 3 种
3第 2 步:从剩下 2 个括号里选 1 个出 y —— C(2,1) = 2 种
4第 3 步:最后 1 个括号只能出 z —— C(1,1) = 1 种
5连乘:3 × 2 × 1 = 6,正是 multinom({1,1,1}) = 3!/(1!·1!·1!) = 6 ✔
6换 x²y:选 2 个出 x(C(3,2)=3)、1 个出 y(C(1,1)=1)→ 3,与 multinom({2,1,0}) 一致 ✔
💡 "从 n 个里选 k₁ 个,再选 k₂ 个…"连乘化简后就是 n!/(k₁!·k₂!···kᵣ!) —— 分堆公式。

🔍 代码逐段解析 · 多项式系数模板

fpow —— 标准快速幂,每步 % MOD 防溢出,用于费马小定理求单个逆元。

init —— 正推 fac[i] = fac[i-1] * i 建阶乘表;逆元表从后往前:ifac[n] = fpow(fac[n], MOD-2),再由 ifac[i-1] = ifac[i] * i 倒推(因为 1/i! = 1/(i!) × i = 1/(i−1)!)。一次 O(n) 预处理,之后每次查询 O(r)。

multinom —— 先累加求 n = Σkᵢ,再 fac[n] 乘上每个 ifac[x]。返回值即 (n; k₁,…,kᵣ) mod 1e9+7。

易错点 1 —— fac/ifac 数组大小要按"最大可能的 n"开(代码开了 2×106+6),n 超界同样 RE。

易错点 2 —— 模数必须是素数才能用费马小定理;MOD 是合数时改用扩展欧几里得或 exLucas。

案例案例1:余数分组抽屉证明题 构造抽屉

题目:给定一个整数 n 和 n+1 个整数 a1…an+1。由鸽巢原理可知,必存在两个数之差是 n 的倍数。请用程序找出任意一对满足条件的下标。

🔍 详细分析过程

第1步:识别模型 —— 结论里的"相同量"是模 n 的余数,所以盒子 = 余数取值,共 n 个盒子(余 0, 1, …, n−1)。

第2步:数物体 —— 有 n+1 个整数,n+1 > n,鸽巢原理生效,必有两个数落在同一个余数盒子里。

第3步:为什么同余 ⟹ 差是倍数 —— 若 ai ≡ aj (mod n),则 ai − aj ≡ 0 (mod n),即差被 n 整除。

第4步:算法实现 —— 开一个大小为 n 的桶 first[r] 记录"余数 r 第一次出现的位置",遍历时若发现桶已被占用,直接输出两个下标。

第5步:复杂度 —— 时间 O(n),空间 O(n)。注意输入本身就有 n+1 个数,所以已经是理论最优。

⚠️ 负数取模坑:C++ 中 -7 % 3 == -1(不是 2)。必须写成 ((x % n) + n) % n 才能得到正确的非负余数。

C++余数分组抽屉 — 完整代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int n; cin >> n;                    // 一共输入 n+1 个数

    vector<int> firstIdx(n, -1);        // 余数 r 首次出现的下标
    vector<long long> firstVal(n, 0);    // 余数 r 首次出现的数值

    for (int i = 1; i <= n + 1; i++) {
        long long x; cin >> x;
        int r = ((x % n) + n) % n;        // 处理负数:转成非负余数
        if (firstIdx[r] != -1) {          // 该抽屉已被占过 → 找到答案
            cout << firstIdx[r] << " " << i << "\n";
            cout << "a[i]=" << firstVal[r] << ", a[j]=" << x
                 << ", 差=" << x - firstVal[r] << ", 除以 n 余 "
                 << ((x - firstVal[r]) % n + n) % n << "\n";
            return 0;
        }
        firstIdx[r] = i; firstVal[r] = x;
    }
    return 0;
}
运行结果
输入: 3
      7 4 9 2
输出: 1 2
      a[i]=7, a[j]=4, 差=3, 除以 n 余 0

说明: 7%3=1 存入抽屉1;4%3=1 与抽屉1冲突 → 输出下标 1 和 2。
      n+1=4 个数放进 3 个抽屉,必然冲突 ✔
🔍 代码逐段解析 · 余数分组抽屉
1读入 n:注意接下来要读的是 n+1 个数,循环写成 i <= n+1 —— 这是本题最容易被看漏的一行
2firstIdx / firstVal:两个大小为 n 的桶,分别记"余数 r 第一次出现的下标"和"数值"。下标初始化 −1 表示空抽屉
3int r = ((x % n) + n) % n:非负余数。直接 x % n 对负数会得负值(−7%3=−1),当数组下标必越界
4if (firstIdx[r] != -1):抽屉已被占 → 立刻输出这一对下标并 return,O(1) 提前结束
5firstIdx[r] = i; firstVal[r] = x;:没冲突就把当前数登记进抽屉,继续扫描
6正确性:鸽巢原理保证 n+1 个数对 n 个余数必冲突,循环最多 n+1 轮必然命中 return,不会走到末尾
7复杂度:时间 O(n)、空间 O(n),已是最优 —— 输入本身就有 n+1 个数
💡 易错点:① 循环上界写成 i <= n(少读一个数);② 忘记负数取模修正。

💡 如果题目改成"差是 n 的倍数且差最大"?

鸽巢原理只能保证"存在",找最优解就得换思路:把每个抽屉里所有数排序,最大差就在每个抽屉的最大值 − 最小值里取最大。这也是一个 O(n log n) 的分组后处理技巧。

案例案例2:P1313 计算系数 NOIP 真题

题目(NOIP2011 提高组):给定多项式 (ax + by)k,求展开式中 xnym 项的系数,结果对 10007 取模。
输入:a b k n m(保证 n + m = k,0 ≤ n, m ≤ k ≤ 1000,1 ≤ a, b ≤ 106)。
输出:一个整数,表示系数 mod 10007。
输入样例:1 1 3 1 2   输出样例:3

🔍 详细分析过程

第1步:套用二项式定理 —— (ax + by)k = Σt=0..k C(k,t)·(ax)k−t·(by)t = Σ C(k,t)·ak−tbt·xk−tyt。

第2步:对齐指数 —— 题目要 xnym,所以 k−t = n,即 t = m(这也是为什么题目保证 n+m = k)。

第3步:写出答案 —— 系数 = C(k, m) × an × bm mod 10007。

第4步:三块零件怎么算

· C(k, m):k ≤ 1000,直接杨辉三角 O(k²) 预处理即可,不用逆元。

· an、bm:指数最大 1000,但底数达 106,先取模再用快速幂 O(log n);也可以直接循环乘 1000 次。

第5步:取模时机 —— 乘法前先 a %= 10007、b %= 10007,避免溢出。

复杂度:O(k²) 预处理 + O(log k) 快速幂,k ≤ 1000 时毫秒级通过。

C++P1313 计算系数 — 完整代码
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 10007;
long long C[1005][1005];

long long qpow(long long a, long long b) {   // 快速幂
    long long res = 1; a %= MOD;
    while (b > 0) {
        if (b & 1) res = res * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return res;
}

int main() {
    long long a, b;
    int k, n, m;
    cin >> a >> b >> k >> n >> m;

    // 杨辉三角预处理组合数模 10007(帕斯卡恒等式)
    C[0][0] = 1;
    for (int i = 1; i <= k; i++) {
        C[i][0] = 1;
        for (int j = 1; j <= i; j++)
            C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD;
    }

    long long ans = C[k][m];            // C(k, m):也可写 C[k][n],对称性
    ans = ans * qpow(a, n) % MOD;       // 乘上 a^n
    ans = ans * qpow(b, m) % MOD;       // 乘上 b^m
    cout << ans << "\n";
    return 0;
}
运行结果
输入: 1 1 3 1 2
输出: 3
      (x+y)³ 中 x¹y² 的系数 = C(3,2)·1¹·1² = 3 ✔
      (展开式 1x³ + 3x²y + 3xy² + y³)

输入: 2 3 3 2 1
输出: 36
      C(3,2)·2²·3¹ = 3 × 4 × 3 = 36 ✔

输入: 1000000 1000000 1000 500 500
输出: 8028(乘数先取模,不会溢出;实际运行验证)
🔍 代码逐段解析 · P1313 计算系数
1qpow:标准快速幂。入口 a %= MOD 先把底数压进模域 —— 底数 a 可达 106,不先取模中间乘积也不会溢 long long,但取模更稳
2杨辉三角预处理:C[0][0]=1 起手,外层 i 递增、内层 j 用帕斯卡恒等式递推并 % MOD。k ≤ 1000,1000×1000 的 long long 约 8MB,全局数组可承受
3ans = C[k][m]:对齐指数 —— (ax+by)k 中 xnym 对应 t = m,所以组合数取 C(k,m) 而不是 C(k,n)(由对称性两者数值相同,但推导必须写对)
4ans = ans * qpow(a, n) % MOD:乘 an;再乘 bm。每乘一次就取一次模,链式乘法防溢出
5样例走查:输入 1 1 3 1 2 → C(3,2)=3,a¹=1,b²=1 → 输出 3 ✔;输入 2 3 3 2 1 → C(3,1)×2²×3¹ = 36 ✔
6复杂度:O(k²) 预处理 + O(log k) 快速幂,k ≤ 1000 毫秒级通过
💡 易错点:① 把系数写成 C(k,n)·am·bn(幂次与组合数配反);② 底数不取模直接 qpow,a=106、指数 1000 时 a*a 阶段即溢出。

💡 如果 k 达到 1018 怎么办?

杨辉三角 O(k²) 就废了。此时要用Lucas 定理把 C(k,m) mod 10007 拆成 10007 进制逐位相乘(因为 10007 是素数),时间复杂度 O(10007 + log10007k)。这也是 P3807 模板题、以及后续数论专题的核心。

案例案例3:系数和与奇偶统计 性质应用

题目:给定 n(n ≤ 1018),求 (1+x)n 展开式中:① 所有系数之和;② 系数为奇数的项的个数。
输入样例:5   输出样例:32 4

🔍 详细分析过程

问题①:系数之和 —— 直接把 x = 1 代入:(1+1)n = 2n = Σ C(n,k)。答案是 2n,用快速幂 O(log n) 算即可。

问题②:奇数系数个数 —— 需要判定"哪些 k 使 C(n,k) 为奇数"。由 Lucas 定理取模 2:

C(n,k) ≡ C(n0,k0)·C(n1,k1)·… (mod 2),其中 ni、ki 是二进制各位

而 C(0,0)=1、C(0,1)=0、C(1,0)=1、C(1,1)=1,所以每个二进制位都要求 ki ≤ ni。换句话说:k 的二进制必须是 n 的二进制"子掩码"(n 的每个 1 位可以选填 0 或 1,n 的 0 位只能填 0)。

于是 k 的可选个数 = 2popcount(n)。用 __builtin_popcountll(n) 一行搞定。

两种实现对比:

方法一 暴力递推杨辉三角求每项奇偶:O(n²),n ≤ 5000 还行。

方法二 位运算:O(log n),n = 1018 也瞬间出答案。推荐。

C++方法一:杨辉三角暴力统计(适合小 n,用于对拍验证)
#include <bits/stdc++.h>
using namespace std;
int main() {
    int n; cin >> n;
    vector<vector<int>> C(n + 1, vector<int>(n + 1, 0));
    C[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        C[i][0] = 1;
        for (int j = 1; j <= i; j++)
            C[i][j] = (C[i-1][j-1] + C[i-1][j]) % 2;  // 只存奇偶
    }
    int odd = 0;
    for (int k = 0; k <= n; k++) odd += C[n][k];
    cout << odd << "\n";
}

🔍 代码逐段解析 · 方法一(暴力对拍版)

第 1~3 行 —— 只读一个 n,开 (n+1)×(n+1) 的二维 vector 存组合数的奇偶(每步 % 2,数值只有 0/1,不担心溢出)。

递推循环 —— 标准帕斯卡递推,唯一差别是模数取 2:C[i][j] = (C[i-1][j-1] + C[i-1][j]) % 2。

统计输出 —— odd += C[n][k] 对第 n 行求和,和值就是奇数系数个数。

易错点 —— 这是 O(n²),只适合 n ≤ 5000 的对拍验证;拿它交 n = 1018 的原题必 TLE。它的价值是与方法二互相验证正确性。

C++方法二:位运算 O(log n) —— 推荐
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 1000000007;

long long qpow(long long a, long long b) {
    long long r = 1; a %= MOD;
    while (b) { if (b & 1) r = r * a % MOD; a = a * a % MOD; b >>= 1; }
    return r;
}

int main() {
    long long n; cin >> n;

    // ① 系数之和 = 2^n mod 1e9+7
    long long sum = qpow(2, n);

    // ② 奇数系数个数 = 2^popcount(n)
    int pc = __builtin_popcountll(n);
    long long odd = qpow(2, pc);      // n=10^18 时 pc ≤ 60

    cout << sum % MOD << " " << odd % MOD << "\n";

    // 附:输出每个奇数项的下标(n 不太大时)
    for (long long k = 0; k <= n; k++)
        if ((k & n) == k) cout << k << " ";   // k 是 n 的子掩码
    cout << "\n";
    return 0;
}
运行结果
输入: 5
输出: 32 4
      0 1 4 5

说明: (1+x)^5 = 1 + 5x + 10x² + 10x³ + 5x⁴ + x⁵
      · 系数和 1+5+10+10+5+1 = 32 = 2⁵ ✔
      · 奇数项是 1、5、5、1,共 4 个 ✔
      · popcount(5) = popcount(101₂) = 2,2² = 4 ✔
      · 奇数下标 {0,1,4,5}:0&5=0 ✔ 1&5=1 ✔ 4&5=4 ✔ 5&5=5 ✔
🔍 代码逐段解析 · 方法二(位运算)
1qpow:快速幂取模 1e9+7,供问题①使用 —— 系数之和 2n mod p,O(log n)
2sum = qpow(2, n):把 x=1 代入 (1+x)n,一行得到所有系数之和
3pc = __builtin_popcountll(n):数出 n 二进制中 1 的个数。n = 1018 时 pc ≤ 60,必须用 ll 后缀版本
4odd = qpow(2, pc):奇数系数个数 = 2pc —— 依据是 Lucas 模 2:C(n,k) 为奇 ⟺ k 是 n 的子掩码,每个 1 位独立选 0/1
5附加循环:(k & n) == k 逐个枚举子掩码下标,把每个奇数项的位置打出来(n 很大时这段本身是 O(n),注释已声明"n 不太大时")
6样例走查:n=5 → sum = 32,pc(101₂)=2 → odd = 4,子掩码 {0,1,4,5} 全部命中 ✔
💡 易错点:① popcount 用 32 位版;② 附加枚举循环忘了它本身是 O(n),大数据下要删掉。

💡 从"C(n,k) 奇偶"到"谢尔宾斯基三角形"

把每一行的奇偶画成黑白,就得到了分形图案。这条"组合数 → 二进制 → 分形"的链条,本质上是 Lucas 定理(模 2) 的几何呈现,也是 Kummer 定理(C(n,k) 中因子 p 的个数 = 作 p 进制加法时的进位次数)的直观版本。竞赛中遇到"计数 + 质因子"类问题,Kummer 定理往往能一步到位。

模块 03

实践应用

三道题分别练"鸽巢计数""杨辉三角性质""前缀和抽屉",每题都给两种解法对比。

题1倍数差计数 鸽巢 + 计数
B编程题

题目描述

给定 n 个整数 a1..n 和一个正整数 m,求有多少对下标 (i, j) 满足 i < j 且 aj − ai 能被 m 整除。

输入格式:第一行两个整数 n, m。第二行 n 个整数 ai。

输出格式:一个整数,表示满足条件的下标对数。

输入样例:
5 3
1 4 7 10 5

输出样例:6

数据范围:1 ≤ n ≤ 2×105,1 ≤ m ≤ 109,|ai| ≤ 109,时间限制 1s,空间 256MB。

方法一双重循环枚举:对每个 i、j 判断 (aj−ai) mod m 是否为 0。时间复杂度 O(n²),n = 2×105 时约 4×1010 次运算,严重超时。

方法二按余数分桶(鸽巢思想):注意到 aj − ai ≡ 0 (mod m) ⟺ ai ≡ aj (mod m)。所以"每个余数就是一个抽屉",桶内任意两数都满足条件。

计数公式:若余数 r 的桶里装了 cntr 个数,则它们两两配对贡献 C(cntr, 2) = cntr·(cntr−1)/2 对。答案 = Σr C(cntr, 2)。

复杂度:时间 O(n),空间 O(不同余数个数)。因为 m 可达 109 无法开桶数组,要用 unordered_map 或"排序 + 扫描"。

⚠️ 计数类型:n = 2×105 时最大可能是 C(n,2) ≈ 2×1010,必须开 long long。

C++倍数差计数 — 完整代码(方法二)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int n; long long m;
    cin >> n >> m;

    unordered_map<long long, long long> cnt;   // 余数 → 出现次数
    for (int i = 0; i < n; i++) {
        long long x; cin >> x;
        long long r = ((x % m) + m) % m;           // 非负余数
        cnt[r]++;
    }

    long long ans = 0;
    for (auto& p : cnt) {
        long long c = p.second;
        ans += c * (c - 1) / 2;                    // C(c, 2) 组合数
    }
    cout << ans << "\n";
    return 0;
}
运行结果
输入: 5 3
      1 4 7 10 5
输出: 6

过程: 1%3=1, 4%3=1, 7%3=1, 10%3=1, 5%3=2
      cnt[1] = 4 → C(4,2) = 6;cnt[2] = 1 → C(1,2) = 0
      合计 6 ✔
验证: 差为 3 的倍数的数对 (1,4)(1,7)(1,10)(4,7)(4,10)(7,10) 共 6 对 ✔

🔍 代码逐段解析 · 倍数差计数

第 1~4 行(读入) —— n 用 int、m 用 long long(m ≤ 109,恰好压线 int,但后续取模运算统一 long long 更稳)。

第 6~9 行(分桶) —— r = ((x % m) + m) % m 得非负余数,unordered_map 计数。m 最大 109 不能开数组,map 的 key 是余数、value 是出现次数。

第 11~13 行(累加) —— 对每个桶贡献 c * (c - 1) / 2 即 C(c,2);c 是 long long,先乘后除不会丢精度也不会溢出。

易错点 1 —— ans 或 c 用 int:n = 2×105 全同余时答案 C(n,2) ≈ 2×1010,int 直接变负数。

易错点 2 —— 负数取模没修正:|ai| ≤ 109 允许负数,x % m 可能为负,两桶错分。

💡 鸽巢原理在这里做了什么?

它没直接给出答案,但给出了"按什么分组"的关键提示 —— 把问题里的"差是 m 的倍数"翻译成"模 m 同余",分组方式自然浮现。这正是鸽巢原理在算法题中最常见的用法:它提供的是分组的视角,而不是结论本身。

题2杨辉三角奇偶行 位运算
B编程题

题目描述

给定 n,输出杨辉三角第 n 行(即 C(n,0), C(n,1), …, C(n,n))的奇偶性:系数为奇数输出 1,偶数输出 0。

输入格式:一行一个整数 n。

输出格式:一行 n+1 个 0 或 1,用空格隔开。

输入样例:6

输出样例:1 0 1 0 1 0 1

数据范围:1 ≤ n ≤ 106,时间限制 1s,空间 256MB。

方法一递推杨辉三角(只存奇偶):C(n,k) = C(n−1,k−1) + C(n−1,k),每步 % 2。需要 O(n²) 时间与 O(n) 空间。n = 106 时 1012 次运算,超时,只适合 n ≤ 5000 的对拍。

方法二位运算判定:由 Lucas 定理(模 2),C(n,k) 为奇数 ⟺ k 的二进制是 n 的子掩码,等价写法有三种:

· (k & n) == k  (k 的每个 1 位,n 对应位也是 1)

· (k & (n − k)) == 0  (k 与 n−k 没有公共的 1 位)

· 由 Kummer 定理也可推出同样结论(二进制加法无进位)

复杂度:O(n) 时间(每个 k 判一次),O(1) 额外空间。完美通过。

C++杨辉三角奇偶行 — 完整代码(方法二)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    long long n;
    cin >> n;

    for (long long k = 0; k <= n; k++) {
        // C(n,k) 为奇数 ⟺ k 是 n 的二进制子掩码
        int isOdd = ((k & n) == k) ? 1 : 0;
        cout << isOdd;
        if (k < n) cout << ' ';
    }
    cout << "\n";
    return 0;
}
运行结果
输入: 6
输出: 1 0 1 0 1 0 1

说明: C(6,k) = 1, 6, 15, 20, 15, 6, 1 → 奇数的是 1, 15, 15, 1(下标 0,2,4,6)共 4 个 ✔
      n = 6 = 110₂,popcount = 2,2² = 4 ✔

输入: 7
输出: 1 1 1 1 1 1 1 1     ← n = 7 = 111₂ 全 1,整行 8 个系数全是奇数

🔍 代码逐段解析 · 杨辉三角奇偶行

第 1~3 行(读入) —— n ≤ 106,用 long long 接住,后面位运算统一位数。

第 5~9 行(主循环) —— k 从 0 到 n 逐个判定:isOdd = ((k & n) == k),即 k 是否为 n 的二进制子掩码。输出用 if (k < n) cout << ' ' 控制末尾无多余空格。

统计拓展 —— 若需要奇数总个数,1LL << __builtin_popcountll(n) 一行即可算出 2popcount(n),不用再扫一遍(原理见模块二的"奇数个数 = 2^popcount(n)")。

复杂度 —— O(n) 时间、O(1) 空间,n = 106 轻松通过。

易错点 1 —— 输出格式:题目要求空格分隔,写成 cout << isOdd << " " 末尾多一个空格可能被判 PE(本代码用条件空格规避)。

易错点 2 —— 把判据写成 (k & n) != 0(有公共位)而非 == k(子掩码),结果完全不同。

💡 为什么 n = 2m − 1 时整行全奇?

n = 2m−1 的二进制是全 1,任何 k ≤ n 都是它的子掩码,所以整行系数全是奇数。这也解释了杨辉三角图中为什么第 1、3、7、15… 行是"实心"的。反过来,n = 2m 时 popcount(n) = 1,只有 k = 0 和 k = n 两项是奇数 —— 这就是图中"稀疏行"的成因。

题3前缀和抽屉 鸽巢 + 前缀和
C编程题

题目描述

给定 n 个整数构成的序列 a1..n。由鸽巢原理(n+1 个前缀和放进 n 个余数抽屉)必存在一段连续子序列,其元素和是 n 的倍数。请输出这样一段的起止下标(1-indexed,任意一组即可)。

输入格式:第一行一个整数 n。第二行 n 个整数 ai。

输出格式:一行两个整数 l, r,表示区间 [l, r] 的元素和是 n 的倍数。

输入样例:
5
1 2 3 4 5

输出样例:2 3(因为 a₂+a₃ = 2+3 = 5,是 5 的倍数;本题输出任意一组合法解即可)

数据范围:1 ≤ n ≤ 105,|ai| ≤ 109,保证存在答案,时间 1s,空间 256MB。

方法一枚举左右端点:求出前缀和后枚举所有区间判断模 n 是否为 0,O(n²)。n = 105 时超时。

方法二前缀和 + 余数抽屉(鸽巢定理的算法化):

第1步:令 s0 = 0,si = a1 + … + ai。共有 n+1 个前缀和。

第2步:把每个 si 按 mod n 的余数放进 n 个抽屉里。

第3步:n+1 个对象放进 n 个抽屉,由鸽巢原理必有两个 si、sj(i < j)同余。

第4步:sj − si ≡ 0 (mod n),而 sj − si 恰好等于 ai+1 + … + aj,所以区间 [i+1, j] 就是答案。

复杂度:O(n) 时间、O(n) 空间。而且是确定性构造,不需要随机化。

⚠️ 细节:s 数组要开 long long(105 个 109 相加会到 1014);负数取模必须写 ((s % n) + n) % n;初始把 pos[0] = 0 放进去,才能正确处理"从第 1 个元素开始"的区间。

C++前缀和抽屉 — 完整代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int n; cin >> n;

    vector<long long> s(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        long long x; cin >> x;
        s[i] = s[i-1] + x;               // 构建前缀和,共 n+1 个
    }

    vector<int> pos(n, -1);             // 抽屉:余数 r 首次出现的下标
    for (int i = 0; i <= n; i++) {
        int r = ((s[i] % n) + n) % n;      // 非负余数(处理负数)
        if (pos[r] != -1) {              // 抽屉已被占 → 找到同余的一对
            cout << pos[r] + 1 << " " << i << "\n";
            return 0;
        }
        pos[r] = i;
    }
    return 0;
}
运行结果
输入: 5
      1 2 3 4 5
输出: 2 3

过程: s = [0, 1, 3, 6, 10, 15]
      s mod 5 = [0, 1, 3, 1, 0, 0]
      i=0 余 0 → pos[0]=0;i=1 余 1;i=2 余 3;i=3 余 1 冲突!
      pos[1]=1,当前 i=3 → 输出 "2 3",区间和 = s[3]-s[1] = 6-1 = 5 ✔
      (若想输出 [1,4],把判据改成"余数为 0 时立即输出",两种都是一组合法解)
🔍 代码逐段解析 · 前缀和抽屉
1s 数组:大小 n+1、long long —— 105 个 109 相加可达 1014,int 必溢出;s[0] = 0 天生就在抽屉 0 里
2pos 数组:n 个抽屉,记每个余数"首次出现的前缀和下标",−1 表示空
3循环 i = 0..n:必须从 i = 0 开始,把 s[0]=0 登记进去,才能处理"区间从第 1 个元素开始"的情况
4r = ((s[i] % n) + n) % n:ai 可为负,s[i] 也可为负,必须修正成非负余数再当下标
5命中输出:pos[r]+1, i —— 同余对 (s[pos[r]], s[i]) 对应区间 [pos[r]+1, i],其和 = s[i] − s[pos[r]] 是 n 的倍数
6正确性:n+1 个前缀和放进 n 个抽屉,鸽巢原理保证第 n+1 轮之前必命中 return
7复杂度:O(n) 时间、O(n) 空间,确定性构造,无需任何随机化
💡 易错点:① 循环从 i=1 开始漏掉 s[0];② 输出写成 pos[r] 而不是 pos[r]+1(区间左端差一)。

💡 为什么这道题"必然有解"?

因为 n+1 > n,鸽巢原理保证了冲突一定发生。这也是本讲最漂亮的一点:算法不需要"尝试",它只需要"扫描一遍"。凡是证明里出现"必存在",往往就能像这样把存在性证明直接翻译成一个 O(n) 的构造算法。

模块 04

总结提升

①核心要点与万能模板
知识板块必须记住的核心复杂度
鸽巢基本形式n 物 k 盒,n > k ⇒ 必有 2 物同盒O(1)
鸽巢加强形式必有盒子 ≥ ⌊(n−1)/k⌋+1,代码写 (n-1)/k + 1O(1)
余数抽屉n+1 个数必有 2 数之差是 n 的倍数O(n) 验证
前缀和抽屉n+1 个前缀和 ⇒ 存在区间和是 n 的倍数O(n)
二项式定理(a+b)n = Σ C(n,k)an−kbk,系数 = "选哪 k 个括号出 b"O(n)
组合数性质对称、帕斯卡、Σ=2n、交错和=0O(n²)/O(1)
系数奇偶C(n,k) 奇 ⟺ k 是 n 的子掩码 ⇒ 个数 2popcount(n)O(log n)
多项式系数(n;k1..kr) = n!/Πki!O(n+r)
C++本讲万能模板:组合数 + 快速幂 + 鸽巢分桶
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;

/* ---------- ① 快速幂 ---------- */
ll qpow(ll a, ll b, ll mod = MOD) {
    ll r = 1; a %= mod; if (a < 0) a += mod;
    while (b) { if (b & 1) r = r * a % mod; a = a * a % mod; b >>= 1; }
    return r;
}

/* ---------- ② 组合数:杨辉三角(n ≤ 5000) ---------- */
const int MAXN = 5005;
ll C[MAXN][MAXN];
void initComb() {
    C[0][0] = 1;
    for (int i = 1; i < MAXN; i++) {
        C[i][0] = 1;
        for (int j = 1; j <= i; j++)
            C[i][j] = (C[i-1][j-1] + C[i-1][j]) % MOD;
    }
}

/* ---------- ③ 组合数:阶乘 + 逆元(n ≤ 10^7,模素数) ---------- */
ll fac[1000006], ifac[1000006];
void initFac(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i-1] * i % MOD;
    ifac[n] = qpow(fac[n], MOD - 2);
    for (int i = n; i >= 1; i--) ifac[i-1] = ifac[i] * i % MOD;
}
ll binom(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fac[n] * ifac[k] % MOD * ifac[n-k] % MOD;
}

/* ---------- ④ Lucas:n 巨大、模数素数且不大 ---------- */
ll lucas(ll n, ll m, ll p) {           // 注意 p 需为素数
    if (m == 0) return 1;
    return binom(n % p, m % p) * lucas(n / p, m / p, p) % p;
}

/* ---------- ⑤ 鸽巢:余数分桶 ---------- */
// 任取 m+1 个整数,必有两个之差是 m 的倍数
// 桶 = 模 m 的余数(共 m 个),first[r] 记录首次出现位置
// 负数取模:r = ((x % m) + m) % m

/* ---------- ⑥ 鸽巢:前缀和抽屉 ---------- */
// n+1 个前缀和 s[0..n] 放进 n 个余数抽屉
// 必有 i < j 使 s[i] ≡ s[j] (mod n) → 区间 (i, j] 的和是 n 的倍数

/* ---------- ⑦ 系数奇偶 O(1) ---------- */
// C(n,k) 为奇数 ⟺ (k & n) == k   (k 是 n 的二进制子掩码)
// 第 n 行奇数个数 = 2^popcount(n) = 1LL << __builtin_popcountll(n)

🔍 代码逐段解析 · 本讲万能模板

① qpow —— 快速幂模板。额外一行 if (a < 0) a += mod 处理负底数,比裸模板更健壮。

② initComb —— 杨辉三角 O(n²) 建表,n ≤ 5000 适用。注意 C[5005][5005] 的 long long 约 200MB,实际使用时要么降维滚动、要么压缩模数类型,此模板给出的是结构骨架。

③ initFac + binom —— 阶乘逆元法。倒推 ifac[i-1] = ifac[i] * i 是核心技巧;binom 入口的 k < 0 || k > n 防御别删。

④ lucas —— 递归拆 p 进制,要求 p 为素数且 p 不太大(逆元表按 p 建)。

⑤⑥ 鸽巢两个套路 —— 注释即模板:余数分桶记住"负数取模修正"三连 ((x % m) + m) % m;前缀和抽屉记住"s[0]=0 也要进抽屉"。

⑦ 系数奇偶 —— 两行口诀:(k & n) == k 判奇、1LL << popcount(n) 计数,考场上 O(1) 出答案。

易错点 —— ②与③按数据范围二选一,别同时全开(②的数组非常吃内存);④的 p 必须素数,否则逆元不存在。

②拓展案例(难度高于正文)

拓展 1:子集和是 n 的倍数(鸽巢 + 构造输出方案)

题目:给定 n 和 n 个整数,证明并构造一个非空子集,使其元素和是 n 的倍数。

思路:考察前缀和 s1, s2, …, sn,共 n 个。若某个 si ≡ 0 (mod n),那么前 i 个数就是答案;否则这 n 个前缀和的余数都在 {1,…,n−1} 这 n−1 个抽屉里,由鸽巢原理必有两个同余 si ≡ sj,则 {ai+1,…,aj} 是答案。

说明:与正文题 3 的区别是本题只需要"子集"(可以跳着选),而这里构造出来的是连续段,属于子集的特例 —— 结论更强。

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n; cin >> n;
    vector<long long> s(n + 1, 0);
    for (int i = 1; i <= n; i++) { long long x; cin >> x; s[i] = s[i-1] + x; }
    vector<int> pos(n, -1);
    pos[0] = 0;                          // s[0]=0 已在抽屉 0 中
    for (int i = 1; i <= n; i++) {
        int r = ((s[i] % n) + n) % n;
        if (pos[r] != -1) { cout << pos[r] + 1 << " " << i << "\n"; return 0; }
        pos[r] = i;
    }
}

运行:输入 5 / 3 1 4 1 5,前缀和 0, 3, 4, 8, 9, 14,mod 5 = 0, 3, 4, 3, 4, 4 → 冲突在 i=3(pos[3]=1)→ 输出 2 3,区间和 1+4 = 5 ✔

拓展 2:二项式定理求常数项(分数指数)

题目:求 (x + 1/x)n 展开式中的常数项(即 x 的指数为 0 的项)。

思路:展开 = Σ C(n,k)·xn−k·x−k = Σ C(n,k)·xn−2k。常数项要求 n − 2k = 0,即 k = n/2。所以:n 为奇数时无常数项(答案 0);n 为偶数时常数项 = C(n, n/2)。这是"指数配平"的典型应用。

// (x + 1/x)^n 的常数项
long long constantTerm(int n) {
    if (n % 2 != 0) return 0;      // 奇数次数 → 无常数项
    return C[n][n / 2];               // 杨辉三角查表
}
// n=4 → C(4,2) = 6;验证 (x+1/x)^4 = x⁴+4x²+6+4/x²+1/x⁴ ✔
// n=5 → 0(无 x⁰ 项)

拓展 3:Kummer 定理 —— 数清楚质因子个数

题目:求 C(n, k) 中含有多少个因子 2(即 2 的指数)。

Kummer 定理:C(n,k) 中质数 p 的指数 = 在 p 进制下做 k 与 n−k 的加法时产生的进位次数。当 p = 2 时,就是二进制加法的进位次数。

快速写法:2 的指数 = popcount(k) + popcount(n−k) − popcount(n)(因为每次进位使二进制中 1 的个数减少一个)。

int expOfTwo(long long n, long long k) {
    return __builtin_popcountll(k) + __builtin_popcountll(n - k)
         - __builtin_popcountll(n);
}
// n=6, k=2 → C(6,2)=15,pc(2)+pc(4)-pc(6) = 1+1-2 = 0 ✔(15 是奇数)
// n=10,k=2 → C(10,2)=45,1+1-2 = 0 ✔
// n=10,k=3 → C(10,3)=120=8×15,pc(3)+pc(7)-pc(10) = 2+3-2 = 3 ✔(2³=8 整除)

用途:当题目要求 C(n,k) 的奇偶性、或求阶乘/组合数的质因子分布时,Kummer 定理能把 O(n) 的统计降为 O(1) 位运算。

③思考与启发

💡 思考 1:鸽巢原理为什么只能"证明存在",不能"给出位置"?

它的证明方式是反证 —— 假设"每个盒子都不超载"会导致物体总数不够。反证法天然是非构造性的。启发:想要给出位置,就必须把"抽屉"具体化成数据结构(数组桶、map),扫描时逐个检查冲突。本讲的案例 1、题 1、题 3 都是这个思路。

查看参考要点

思路:区分"存在性证明"与"构造性算法"。

考点:鸽巢原理 + 分桶数据结构。

易错:误以为"必然存在"就意味着"能一眼看出来",考场上找不到构造方案就慌。实际上只要把抽屉实现成数组或 map,顺序扫描一遍即可。

延伸:如果要求"最小长度"或"字典序最小"的那一对,就必须枚举所有冲突并取最优,复杂度可能上升到 O(n log n)。

💡 思考 2:把 a = b = 1 代入二项式定理得到 2n,那代入 a = 1, b = 2 得到什么?

得到 Σ C(n,k)·2k = 3n,即"每个元素有三种状态"的总数。启发:二项式定理本质上是一个"参数化的计数接口",代入不同的 a、b 就是在回答不同的组合计数问题。这种"用母函数统一处理计数"的思想,就是组合数学的核心方法论。

查看参考要点

思路:(1+2)n = 3n,即 Σ C(n,k)·2k = 3n。

考点:二项式定理的参数化应用、母函数思想。

易错:把 a、b 的指数搞混。由对称性,Σ C(n,k)·2n−k 与 Σ C(n,k)·2k 恰好相等;但换成 a=1, b=3 时就未必,必须看清哪一项带 k 次方。

💡 思考 3:为什么"随机化"算法在竞赛中很受欢迎?

因为生日悖论告诉我们:冲突概率随规模平方增长。要让冲突概率低于 10−9,只需让值域达到 ~1018 量级,用两个 109 级模数组合即可。这正是字符串哈希用双模数、随机化 Treap 用随机优先级的原因。

查看参考要点

思路:生日悖论 → 冲突概率 ≈ n²/(2M),取 M ~ 1018 即可把概率压到极低。

考点:概率分析 + 哈希冲突处理。

易错:用单个 109 级模数存 105 个元素时,冲突概率约 (105)²/(2×109) ≈ 5,几乎必定被卡 —— 这就是"哈希被 hack"的数学原因。

模块 05

学习检测

先找错,再背口诀 —— 把易错点消灭在考试之前。

⚡高频错误诊断
易错1

负数取模直接写 x % m

❌ 错误
直接 r = x % m 然后用 pos[r]。当 x = −7、m = 3 时 −7 % 3 == −1(C++ 规定余数符号跟随被除数),得到负下标,数组越界 / RE。
✅ 正确
统一写成 r = ((x % m) + m) % m,保证结果落在 [0, m−1]。这是所有"余数分桶"代码的第一条铁律。
口诀:余数非负才当桶,加个 m 再取模,负下标永远不出现!
易错2

桶数组开成 m 大小(m 可达 109)

❌ 错误
int cnt[1000000000] —— 局部变量栈溢出(MLE / RE),全局变量也会超 256MB 内存限制。
✅ 正确
当 m 很大时用 unordered_map<long long,int> 只存出现过的余数;或者把余数算出来后排序再扫描统计连续段长度。空间 O(实际不同余数个数)。
口诀:桶的大小时看 m,m 太大就换 map,排序扫描也能数!
易错3

计数用 int 导致溢出

❌ 错误
int ans 累加对数。n = 2×105 时最多有 C(n,2) ≈ 2×1010 对,远超 int 上限 2.1×109,答案变成负数。
✅ 正确
计数一律用 long long;组合数算式写成 c * (c - 1) / 2 并确保 c 是 long long(否则 c*(c−1) 先按 int 溢出)。
口诀:数对数量用 long long,先乘后除防溢出!
易错4

把 ⌊(n−1)/k⌋+1 写成 n/k

❌ 错误
问"100 人分 7 组至少多少",写成 100/7 = 14。但 14×7 = 98 < 100,装不下!正确下界是 15。
✅ 正确
加强形式:(n-1)/k + 1 = (100−1)/7 + 1 = 14 + 1 = 15。也可以写成上取整 (n + k - 1)/k,两者结果相同。
口诀:最少几个别用整除,减一除一再加一!
易错5

二项式系数算错组合数下标

❌ 错误
求 (ax+by)k 中 xnym 的系数时写成 C(k, n) * pow(a, m) * pow(b, n) —— 组合数与幂次的参数配反了。
✅ 正确
(ax)k−t(by)t 对应 xk−tyt。要 xnym 则 t = m、k−t = n,所以系数 = C(k, m)·an·bm。记住幂次跟变量走,组合数用另一侧的指数。
口诀:a 配 n、b 配 m,组合数取 C(k,m),对齐指数不迷路!
📋速查表
公式 / 结论表达式适用场景复杂度
鸽巢基本形式n > k ⇒ 必有盒子 ≥ 2存在性判断O(1)
鸽巢加强形式(n−1)/k + 1求"至少有几个"O(1)
余数抽屉n+1 个数 ⇒ 存在两数差为 n 的倍数倍数差证明 / 分桶计数O(n)
前缀和抽屉n+1 个前缀和 ⇒ 存在区间和是 n 的倍数区间和被整除O(n)
二项式定理(a+b)n = Σ C(n,k)an−kbk展开式系数O(n)
对称性C(n,k) = C(n,n−k)折半优化O(1)
帕斯卡恒等式C(n,k)=C(n−1,k−1)+C(n−1,k)杨辉三角递推O(n²) 建表
系数和Σ C(n,k) = 2n(1+1)nO(log n)
交错和Σ (−1)kC(n,k) = 0 (n≥1)(1−1)nO(1)
奇数个数2popcount(n)杨辉三角奇偶统计O(1)
子掩码判据(k & n) == kC(n,k) 是否为奇数O(1)
多项式系数n!/(k1!…kr!)多元展开 / 分堆O(n+r)
Lucas 定理C(n,m) ≡ Π C(ni,mi) (mod p)n 巨大、p 素数O(p + logpn)
Kummer 定理v2(C(n,k)) = pc(k)+pc(n−k)−pc(n)质因子个数O(1)
📌笔记卡
🧠 必记概念
  • 鸽巢原理是存在性定理,不是构造性算法
  • 加强形式 (n-1)/k + 1 给出精确下界
  • 构造抽屉三招:余数、区间、子集/前缀和
  • 二项式系数 = "从 n 个括号里选 k 个出 b"
  • C(n,k) 为奇数 ⟺ k 是 n 的二进制子掩码
💻 必背模板
  • 快速幂 qpow(a, b, mod)
  • 杨辉三角 C[i][j] = C[i-1][j-1] + C[i-1][j]
  • 阶乘逆元 ifac[n] = pow(fac[n], MOD-2)
  • 余数分桶 r = ((x % m) + m) % m
  • 奇偶判定 (k & n) == k
⚠️ 考场提醒
  • 负数取模必须先修正,否则 RE
  • m 很大时桶要换 unordered_map
  • 对数计数一律 long long
  • 前缀和数组同样要 long long
  • P1313 类题注意幂次与组合数的参数对齐
课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 12 道洛谷原题。

🟢 A组 · 基础(4题)

A1概念题

把 13 个苹果放进 4 个篮子,说明必有一个篮子至少装几个苹果?写出计算过程。

查看参考答案

答案:至少 4 个。

过程:n = 13(物体),k = 4(盒子)。加强形式给出下界 = ⌊(13−1)/4⌋ + 1 = ⌊12/4⌋ + 1 = 3 + 1 = 4。

验证:若每个篮子最多 3 个,4 个篮子最多装 12 个,装不下 13 个 → 矛盾。所以必有一个 ≥ 4。

写法:C++ 中写 (13-1)/4 + 1,整数除法自动向下取整。

易错:误答 13/4 = 3(向下取整漏掉"+1")。

A2编程题

输入 n 和 m,输出"把 n 个物体放进 m 个盒子,至少有一个盒子装多少个"。

查看参考答案
#include <iostream>
using namespace std;
int main() {
    long long n, m;
    cin >> n >> m;
    if (n <= m) { cout << 1 << endl; return 0; }
    cout << (n - 1) / m + 1 << endl;   // 加强形式
    return 0;
}

输入:13 4   输出:4

思路:直接套 ⌊(n−1)/m⌋+1;当 n ≤ m 时鸽巢原理不保证重复,答案退化为 1(也可以理解为"至少 1 个的盒子一定存在")。

易错:写成 n/m(漏 "+1");或用 int 存 n 时 n 很大导致溢出(这里用 long long 更稳)。

🔍 代码逐段解析

读入 —— n、m 都开 long long,防大数溢出。

特判分支 —— n <= m 时输出 1:物体不超过盒子数,只能保证"每盒至少可以有 1 个",无重复下界。

主公式 —— (n - 1) / m + 1:整数除法天然向下取整,一行实现 ⌊(n−1)/m⌋+1。

易错点 —— ① 漏写 "+1" 得 13/4 = 3;② 用浮点 ceil(n*1.0/m) 有精度风险,纯整数写法最稳。

A3编程题

输入 n 和 k,输出组合数 C(n,k)(保证结果在 long long 范围内,n ≤ 60)。

查看参考答案
#include <iostream>
using namespace std;
long long C[65][65];
int main() {
    int n, k; cin >> n >> k;
    C[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        C[i][0] = 1;
        for (int j = 1; j <= i; j++)
            C[i][j] = C[i-1][j-1] + C[i-1][j];
    }
    cout << C[n][k] << endl;
    return 0;
}

输入:10 3   输出:120

思路:帕斯卡恒等式递推杨辉三角,O(n²) 预处理后 O(1) 查表。n ≤ 60 时 C(60,30) 约 1.18×1017,long long 放得下。

易错:数组只开到 60×60 而循环写 i <= n 时下标 n 越界;或忘记 C[i][0] = 1。

🔍 代码逐段解析

数组声明 —— C[65][65] 全局数组,比 n ≤ 60 多开 5 格余量;不取模因为题目保证结果在 long long 内(C(60,30) ≈ 1.18×1017)。

递推双层循环 —— 外层 i 从 1 到 n;内层 j 从 1 到 i。入口先置 C[i][0] = 1(每行行首),j = i 时 C[i-1][i] 越界读?不:j ≤ i,当 j = i 时读 C[i-1][i-1] 和 C[i-1][i],后者是上一行"行尾之后"的 0 —— 全局数组默认 0,恰好正确。

易错点 —— 若把数组改成本地 vector 且未初始化,C[i-1][i] 就是随机值 —— 全局数组默认清零是这段代码能偷懒的原因。

A4编程题

输入 n,输出 (x + 1)n 展开式的所有系数(n ≤ 30)。

查看参考答案

思路:(x+1)n = Σ C(n,k)xk,系数就是杨辉三角第 n 行。直接打印即可。

#include <iostream>
using namespace std;
long long C[35][35];
int main() {
    int n; cin >> n;
    C[0][0] = 1;
    for (int i = 1; i <= n; i++) {
        C[i][0] = 1;
        for (int j = 1; j <= i; j++) C[i][j] = C[i-1][j-1] + C[i-1][j];
    }
    for (int k = 0; k <= n; k++) cout << C[n][k] << " \n"[k == n];
    return 0;
}

输入:5   输出:1 5 10 10 5 1

易错:忘记输出换行导致格式错误;或把 (x+1)n 的系数当成 (1+x)n 的倒序(其实对称性保证两者相同,是巧合而非通例)。

🔍 代码逐段解析

建表部分 —— 与 A3 相同的帕斯卡递推,n ≤ 30 时 C(30,15) ≈ 1.55×108,long long 绰绰有余。

输出行 —— cout << C[n][k] << " \n"[k == n]:字符串 " \n" 的下标 0 是空格、下标 1 是换行 —— 不是最后一个数就打空格,是就打换行,一行代码搞定分隔。

易错点 —— " \n"[k == n] 这种写法简洁但易读性差,考场上写不惯就用普通 if,别为了炫技写错。

🟡 B组 · 提高(4题)

B1鸽巢计数

输入 n 个整数和 m,输出有多少对 (i,j)(i<j)满足 a[i] 与 a[j] 之差被 m 整除。

查看参考答案

分析:差被 m 整除 ⟺ 两数模 m 同余 → 按余数分桶,桶内两两配对贡献 C(cnt,2) = cnt(cnt−1)/2。

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(0);
    int n; long long m; cin >> n >> m;
    unordered_map<long long, long long> cnt;
    for (int i = 0; i < n; i++) {
        long long x; cin >> x;
        cnt[((x % m) + m) % m]++;
    }
    long long ans = 0;
    for (auto& p : cnt) { long long c = p.second; ans += c * (c - 1) / 2; }
    cout << ans << endl;
    return 0;
}

思路:O(n) 时间、O(不同余数) 空间。

易错:负数取模没修正;忘记用 long long 存答案(C(n,2) 可达 2×1010);m 很大时开了巨大的桶数组导致 MLE。

🔍 代码逐段解析

读入与分桶 —— 与正文题 1 同构:cnt[((x % m) + m) % m]++ 一行完成"取非负余数 + 计数"。

统计累加 —— 遍历 map,每桶贡献 c * (c - 1) / 2;c 声明为 long long 保证先乘不溢。

易错点 —— 若写成 int c = p.second,c*(c−1) 按 int 乘法溢出后再转 long long 已经晚了。

B2二项式系数

输入 n、k 和一个素数 p(p ≤ 1000,n ≤ 1018),输出 C(n,k) mod p。

查看参考答案

思路:n 太大,用 Lucas 定理:C(n,k) ≡ C(n mod p, k mod p) · C(n/p, k/p) (mod p),递归到底即可。每一位的组合数用杨辉三角或阶乘逆元预处理。

#include <bits/stdc++.h>
using namespace std;
long long p, fac[1005], ifac[1005];
long long qp(long long a, long long b) {
    long long r = 1; a %= p;
    while (b) { if (b & 1) r = r * a % p; a = a * a % p; b >>= 1; }
    return r;
}
long long cb(long long n, long long m) {
    if (m < 0 || m > n) return 0;
    return fac[n] * ifac[m] % p * ifac[n-m] % p;
}
long long lucas(long long n, long long m) {
    if (m == 0) return 1;
    return cb(n % p, m % p) * lucas(n / p, m / p) % p;
}
int main() {
    long long n, k; cin >> n >> k >> p;
    fac[0] = 1;
    for (int i = 1; i < p; i++) fac[i] = fac[i-1] * i % p;
    ifac[p-1] = qp(fac[p-1], p - 2);
    for (int i = p - 1; i >= 1; i--) ifac[i-1] = ifac[i] * i % p;
    cout << lucas(n, k) << endl;
    return 0;
}

输入:10 3 7   输出:1(C(10,3)=120,120 mod 7 = 1;Lucas 分解:10 = 13₇、3 = 03₇ → C(3,3)·C(1,0) = 1)

易错:忘记 p 必须是素数;或递归时写成 lucas(n/p, m/p) 之前没先乘 cb(n%p, m%p);数组只用开到 p 却开到 1005 后对 p > 1005 越界。

🔍 代码逐段解析

fac / ifac 预处理 —— 表只建到 p−1(Lucas 每一位的组合数参数都 < p);ifac 倒推与正文模板一致。

cb —— 单位 combining function,带 m < 0 || m > n 返回 0 的防御:某一位 k 的数字大于 n 的数字时整体即 0。

lucas 递归 —— cb(n%p, m%p) * lucas(n/p, m/p) % p:当前位乘上更高位的递归结果,出口 m == 0 返回 1。

易错点 —— 递归深度 O(logpn),n = 1018、p = 2 时也只有 60 层,安全;但 p = 1 不是素数会死循环/除零,读入时依题意保证。

B3构造证明

从 1 ~ 2n 中任取 n+1 个不同的整数。证明其中必有两个数互质,并写出算法找出这一对。

查看参考答案

证明:把 1~2n 分成 n 组:{1,2}, {3,4}, …, {2n−1, 2n}(每组两个相邻整数)。这 n 组就是 n 个抽屉。取出的 n+1 个数由鸽巢原理必有两个落在同一组里。而同一组里的两个数形如 {2i−1, 2i},是相邻整数,gcd(2i−1, 2i) = 1,所以它们互质。∎

算法:把每个数映射到组号 g = (x + 1) / 2,用桶记录每组出现过的数,第二次出现时即可输出这一对。

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n; cin >> n;
    vector<int> first(n + 1, -1);     // 每组首次出现的数
    for (int i = 0; i < n + 1; i++) {
        int x; cin >> x;
        int g = (x + 1) / 2;              // 组号 1..n
        if (first[g] != -1) {
            cout << first[g] << " " << x << "\n";
            cout << "gcd = " << __gcd(first[g], x) << "\n";
            return 0;
        }
        first[g] = x;
    }
    return 0;
}

输入:4 / 1 4 6 8 7   输出:8 7 / gcd = 1(7、8 同属 {7,8} 组,程序先输出组内首现的 8)

考点:抽屉的构造方式(按相邻整数配对,而不是按余数)。

易错:把抽屉设计成"奇偶各一组"(那样只能证明同奇偶,推不出互质);组号公式写成 x / 2(会导致 2 和 3 分到不同组)。

🔍 代码逐段解析

first 数组 —— 大小 n+1,下标即组号 1..n,存该组首个出现的数,−1 表示空组。

组号映射 —— g = (x + 1) / 2:x=1,2 → g=1;x=3,4 → g=2……恰好把 {2i−1, 2i} 归为一组,组内两数相邻必互质。

命中输出 —— 第二次遇到同组号即输出 first[g], x 与 __gcd 验证;n+1 个数对 n 个组,鸽巢原理保证必命中。

易错点 —— 组号若写成 x / 2:1 落进 0 号组、2 与 3 同组、4 与 5 同组……组数变成 n+1 个,鸽巢原理的"物体多于盒子"不再成立,可能扫完都找不到冲突。

B4鸽巢 + 前缀和

输入 n 和 n 个整数,输出一段连续区间 [l, r] 使区间和能被 n 整除。

查看参考答案

分析:n+1 个前缀和 s[0..n] 按 mod n 余数放进 n 个抽屉,必有同余的一对 s[i] ≡ s[j],则区间 (i, j] 的和是 n 的倍数。

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios::sync_with_stdio(0);
    int n; cin >> n;
    vector<long long> s(n + 1, 0);
    for (int i = 1; i <= n; i++) { long long x; cin >> x; s[i] = s[i-1] + x; }
    vector<int> pos(n, -1);
    for (int i = 0; i <= n; i++) {
        int r = ((s[i] % n) + n) % n;
        if (pos[r] != -1) { cout << pos[r] + 1 << " " << i << endl; return 0; }
        pos[r] = i;
    }
    return 0;
}

思路:O(n)。注意 n = 1 时任何区间都满足(任何数都是 1 的倍数),程序会输出 1 1。

易错:前缀和用 int 存导致溢出;把输出写成 pos[r] 而不是 pos[r]+1(差一个下标)。

🔍 代码逐段解析

前缀和构建 —— s[0]=0 起手,循环读入并累加;long long 防 1014 量级溢出。

扫描抽屉 —— i 从 0 到 n(含 s[0]),r = ((s[i] % n) + n) % n 非负余数;命中即输出 pos[r]+1, i。

易错点 —— 本代码没有预先 pos[0] = 0,而是靠"i 从 0 开始扫"把 s[0]=0 登记 —— 两种写法等价,但只能取其一:若循环从 i=1 开始又不预置 pos[0],"前缀本身就是 n 的倍数"的情况会漏解。

🔴 C组 · 挑战(4题)

C1初赛真题

把 100 个球放进 7 个盒子,下列哪个结论必然成立?
A. 每个盒子至少 14 个   B. 至少一个盒子 ≥ 15 个   C. 至少一个盒子 = 15 个   D. 每个盒子至多 15 个

查看参考答案

答案:B

解析:加强形式 ⌊(100−1)/7⌋ + 1 = ⌊99/7⌋ + 1 = 14 + 1 = 15,即至少有一个盒子装了 ≥ 15 个球。

逐项分析:A 错(可以有一个盒子只放 1 个);C 错(可以是 16 个,不一定是"恰好等于 15");D 错(可以有一个盒子放 94 个)。

考点:加强形式的取整计算,"至少 ≥"与"恰好 ="的区别。

易错:选 C —— 把"下界"误当成"可以取到等号"。鸽巢原理只保证 ≥,不保证 =。

C2综合分析

求 Σk=0..n k·C(n,k) 的闭式结果(n ≥ 1)。

查看参考答案

答案:n·2n−1。

方法一(求导):由 (1+x)n = Σ C(n,k)xk,两边对 x 求导得 n(1+x)n−1 = Σ k·C(n,k)xk−1。令 x = 1 得 n·2n−1 = Σ k·C(n,k)。∎

方法二(组合双计数):Σ k·C(n,k) 表示"对所有子集统计其大小之和"。换个数法:每个元素出现在 2n−1 个子集中(其余 n−1 个元素任选),共 n 个元素,所以总和 = n·2n−1。

验证:n=3 时 Σ k·C(3,k) = 0+3+6+3 = 12,n·2n−1 = 3×4 = 12 ✔

易错:把答案记成 n·2n(多乘了一个 2);或忘记 k 从 0 开始(第一项是 0,不影响结果)。

C3综合编程

输入 n(n ≤ 105)和长度为 n 的数组 a,输出杨辉三角第 a[i] 行中奇数系数的个数(对每个 i)。

查看参考答案

思路:对每个 a[i],答案 = 2popcount(a[i])。用 __builtin_popcountll O(1) 求出 popcount,再快速幂取模。总复杂度 O(n log n)——n = 105 时飞快。

#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;
long long qp(long long a, long long b) {
    long long r = 1; a %= MOD;
    while (b) { if (b & 1) r = r * a % MOD; a = a * a % MOD; b >>= 1; }
    return r;
}
int main() {
    ios::sync_with_stdio(0);
    int n; cin >> n;
    for (int i = 0; i < n; i++) {
        long long x; cin >> x;
        int pc = __builtin_popcountll(x);
        cout << qp(2, pc) << " \n"[i == n - 1];
    }
    return 0;
}

输入:4 / 5 6 7 8   输出:4 4 8 2

说明:popcount(5)=2→4;popcount(6)=2→4;popcount(7)=3→8;popcount(8)=1→2。

易错:用 __builtin_popcount(32 位版)而不是 __builtin_popcountll(64 位版),导致 x > 231 时结果错误。

🔍 代码逐段解析

qp —— 快速幂,用于 2pc mod 1e9+7(pc ≤ 60,其实不取模也行,取模是防题目进一步加码)。

主循环 —— 每读一个 a[i],pc = __builtin_popcountll(x) 后输出 qp(2, pc);" \n"[i == n-1] 控制最后一个数后换行。

易错点 —— 单点 O(log) 但整体是逐个查询,总复杂度 O(n log MOD);若 n 高达 107 可离线预处理 2 的幂表。

C4构造挑战

给定 n 个互不相同的正整数 a1..n(每个 ≤ 109)。证明必存在一个非空子集,其元素和是 n 的倍数,并输出这个子集(输出元素个数与下标)。

查看参考答案

证明:考虑前 i 项和 si(i = 1…n),共 n 个。若存在 si ≡ 0 (mod n),则前 i 项就是答案。否则这 n 个前缀和的余数都在 {1,…,n−1} 内,共 n−1 种取值,而对象有 n 个 —— 由鸽巢原理必有两个前缀和 si ≡ sj(i<j),则 {ai+1,…,aj} 的和 = sj−si 是 n 的倍数。∎

算法:把 pos[0] = 0 也放进抽屉(对应 s0 = 0),扫描前缀和时若余数已出现即输出 [pos[r]+1, i]。

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n; cin >> n;
    vector<long long> s(n + 1, 0);
    for (int i = 1; i <= n; i++) { long long x; cin >> x; s[i] = s[i-1] + x; }
    vector<int> pos(n, -1);
    for (int i = 0; i <= n; i++) {
        int r = ((s[i] % n) + n) % n;
        if (pos[r] != -1) {
            cout << i - pos[r] << "\n";         // 元素个数
            for (int j = pos[r] + 1; j <= i; j++) cout << j << " ";
            cout << "\n和 = " << s[i] - s[pos[r]] << "\n";
            return 0;
        }
        pos[r] = i;
    }
}

输入:5 / 3 1 4 1 5   输出:2 / 2 3 / 和 = 5

易错:遍历时从 i = 1 开始(漏掉 s0 = 0),导致"前缀本身就是 n 的倍数"的情况漏解;或忘记取非负余数。

🔍 代码逐段解析

与 B4 的差别 —— 本题要求输出方案:命中后先打元素个数 i - pos[r],再用 for 从 pos[r]+1 到 i 逐个打下标,最后附区间和验证。

正确性依据 —— s0..n 共 n+1 个前缀和进 n 个抽屉必冲突;冲突对 (pos[r], i] 之和 = s[i] − s[pos[r]] ≡ 0 (mod n)。

易错点 —— 输出的是元素个数在前、下标在后,与"输出 l r"的题面格式区分清楚;打完要 return,否则继续扫可能再次输出多组。

🏆 洛谷原题精选(12道)

以下 12 道题与鸽巢原理、二项式定理、组合数高度相关,按难度从低到高排列。

1
P2181
对角线(组合数 C(n,4))入门
2
P1036
[NOIP2002] 选数(组合枚举)入门
3
P1157
组合的输出(杨辉三角/组合)入门
4
P1706
全排列问题(阶乘计数)入门
5
P1067
多项式输出(多项式表示)普及-
6
P1866
编号(组合计数 + 排序)普及-
7
P1226
【模板】快速幂普及-
8
P1313
[NOIP2011] 计算系数(二项式定理)提高
9
P2822
[NOIP2016] 组合数问题(杨辉三角+前缀和)提高
10
P3811
【模板】乘法逆元提高
11
P3807
【模板】卢卡斯定理(Lucas)提高
12
P4071
[SDOI2016] 排列计数(错排 + 组合数)提高