【数学1】基础数学问题

【数学1】基础数学问题

24 道题 | 难度 1-5 | 约 180 分钟 | J 组普及/提高


上节课回顾

📝 本课是「基础篇」数学模块的第一讲。前置知识:C++ 基础语法(循环、数组、函数)。

在正式开始之前,请先完成课前自测,检验以下基础知识:

知识点 自检问题
素数判定 判断 \(n\) 是否为素数,试除法的时间复杂度是多少?
欧几里得算法 gcd(a,b) 的核心递推公式是什么?
快速幂 计算 \(a^b \bmod p\) 的快速幂时间复杂度是多少?
进制 十进制 \(255\) 的十六进制表示是什么?
组合数 \(C(n,m) = C(n-1,m-1) + C(n-1,m)\) 这个公式叫什么?

如果上述问题有不确定的,本课将逐一讲解。


课程导入

数学是信息学竞赛的基石。从普及组的进制转换、GCD/LCM,到提高组的组合数取模、素数筛法,数学基础贯穿了整个竞赛学习路径。

本课覆盖 三大模块

  1. 位运算与进制转换:理解计算机底层的数据表示,掌握异或、位移等位运算技巧。
  2. 计数原理与排列组合:从加法/乘法原理出发,建立组合数学的基本框架。
  3. 整除理论:素数筛法、约数与倍数、GCD/LCM、质因数分解——这是数论最核心的工具箱。

本课学习路线: 进制与位运算 → 计数原理 → 整除理论(素数 → 约数 → GCD/LCM → 质因数分解)


补充知识点:计算机数据表示

⚠️ 以下知识点在思维导图中提及,但没有直接对应的编程题目。在正式讲例题之前,用几分钟把它们讲清楚。

1. 原码、反码、补码

计算机中,整数以补码形式存储。理解补码是理解位运算和负数表示的基础。

flowchart TD subgraph 原码["原码 (最高位=符号位)"] O1["+5 = 0 0000101"] O2["-5 = 1 0000101"] end subgraph 反码["反码 (负数: 符号位不变, 其余取反)"] F1["+5 = 0 0000101 (不变)"] F2["-5 = 1 1111010"] end subgraph 补码["补码 (负数: 反码+1)"] T1["+5 = 0 0000101 (不变)"] T2["-5 = 1 1111011"] end 原码 --> 反码 --> 补码 style T2 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
表示法 规则 示例(8位,\(+5\)\(-5\)
原码 最高位为符号位(0正1负),其余位表示绝对值 \(+5 = 00000101\)\(-5 = 10000101\)
反码 正数反码 = 原码;负数反码 = 符号位不变,其余取反 \(+5 = 00000101\)\(-5 = 11111010\)
补码 正数补码 = 原码;负数补码 = 反码 + 1 \(+5 = 00000101\)\(-5 = 11111011\)

为什么用补码? 补码让加减法统一——减法可以转换为加法(\(a - b = a + (-b)\) 的补码)。而且补码只有一个零(00000000),不会像原码那样出现 +0-0

速算技巧:对一个数 \(x\)~x + 1 = -x(取反加一得相反数)。例如 \(x=5\)~5 = -6-6 + 1 = -5。在 C++ 中 int x = 5; cout << -x; 等价于 cout << (~x + 1);

2. 十六进制(Hexadecimal)

十六进制用 16 个符号表示:0-9A-F(或 a-f)。

flowchart LR subgraph 十六进制["十六进制 0x2F3A"] H1["2"] --> H2["F"] --> H3["3"] --> H4["A"] end subgraph 二进制["二进制 (每4位一组)"] B1["0010"] --> B2["1111"] --> B3["0011"] --> B4["1010"] end subgraph 十进制["十进制"] D1["0x2F3A = 2×16³ + 15×16² + 3×16 + 10 = 12090"] end H1 -.->|"2→0010"| B1 H2 -.->|"F→1111"| B2 H3 -.->|"3→0011"| B3 H4 -.->|"A→1010"| B4 B1 --> D1 style H1 fill:#e3f2fd,stroke:#1565c0 style H2 fill:#e3f2fd,stroke:#1565c0 style H3 fill:#e3f2fd,stroke:#1565c0 style H4 fill:#e3f2fd,stroke:#1565c0
十进制 0 1 ... 9 10 11 12 13 14 15
十六进制 0 1 ... 9 A B C D E F
二进制 0000 0001 ... 1001 1010 1011 1100 1101 1110 1111
  • 为什么用十六进制? 1 个十六进制位恰好对应 4 个二进制位(\(16 = 2^4\))。0xFF = 1111 1111,一眼就能看出是 8 位全 1。
  • C++ 中的表示0x 前缀表示十六进制字面量,如 int x = 0xFF; // x = 255
  • 常见用途:颜色值(#FF5733)、内存地址、位掩码(如 0xFFFF 表示低 16 位)。

3. 二进制小数转换

十进制小数转二进制采用乘 2 取整法

flowchart LR S["0.625 × 2 = 1.25"] -->|"取整数 1"| A["0.25 × 2 = 0.5"] A -->|"取整数 0"| B["0.5 × 2 = 1.0"] B -->|"取整数 1"| R["0.625₁₀ = 0.101₂ ✓"] style S fill:#e3f2fd,stroke:#1565c0 style R fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

\(0.625\) 为例:

步骤 操作 整数部分 小数部分
1 \(0.625 \times 2 = 1.25\) 1 0.25
2 \(0.25 \times 2 = 0.5\) 0 0.5
3 \(0.5 \times 2 = 1.0\) 1 0(结束)

结果:\(0.625_{10} = 0.101_2\)

注意:并非所有有限十进制小数都能用有限二进制小数表示。例如 \(0.1_{10}\) 在二进制中是无限循环小数 \(0.0001100110011..._2\)——这正是 float/double 产生精度误差的根源。

4. 数据量单位转换(bit / Byte)

单位 换算 常见场景
1 Byte (B) = 8 bit 一个 char 占 1 Byte
1 KB = \(2^{10}\) B = 1024 B 一页文本约 2-4 KB
1 MB = \(2^{20}\) B = 1024 KB 一首 MP3 约 3-5 MB
1 GB = \(2^{30}\) B = 1024 MB 一部高清电影约 1-3 GB

竞赛相关:洛谷评测机的内存限制通常为 125MB ~ 512MB。一个 int 占 4 Byte,long long 占 8 Byte。开一个 \(10^7\) 大小的 int 数组约占用 40MB——在 125MB 限制下开 \(3\times 10^7\) 就危险了。


本课知识地图

每个知识点均依托例题展开,不再单独罗列概念定义。

知识模块 核心概念 对应例题
位运算与进制转换 进制互转 · 异或性质 · 位移操作 例1 P1143 → 例2 P1469 → 例3 P1100
计数原理与排列组合 乘法原理 · 排列数 · 杨辉三角 · 二维前缀和 例4 P1866 → 例5 P1246 → 例6 P2822
整除理论 — 素数 埃氏筛 · 线性筛 · 区间筛 例7 P3383 → 例8 P1835
整除理论 — 约数与倍数 倍数枚举法 · 除数函数公式 例9 P2926 → 例10 P1403
整除理论 — GCD/LCM 核心公式 · 暴力枚举 · 质因数分解综合 例11 P1029 → 例12 P1072
整除理论 — 质因数分解 指数比较法 · 综合应用 例13 P1069

例题精讲

模块一:位运算与进制转换


例题 1:P1143 进制转换

知识点: 进制互转 — 其他进制 → 十进制 → 目标进制

题意

给定 \(n\) 进制数 \(s\),将其转为 \(m\) 进制输出。\(n, m \le 36\),数字用 0-9A-Z 表示。

样例解释

输入 输出 解释
n=16, s=FF, m=10 255 十六进制 FF = \(15\times 16 + 15 = 255\)
n=10, s=255, m=16 FF 十进制 255 转回十六进制

思路详解

进制转换的两步走:

flowchart LR subgraph 输入 S["n 进制数 s"] end subgraph Step1["Step 1: 按权展开 (Horner 法)"] D["十进制 ans"] end subgraph Step2["Step 2: 除基取余"] O["m 进制字符串"] end S -->|"ans = ans × n + digit"| D D -->|"t = ans % m; ans /= m"| O O -->|"逆序输出"| R["结果"] style S fill:#e3f2fd,stroke:#1565c0 style D fill:#fff3e0,stroke:#e65100 style O fill:#e8f5e9,stroke:#2e7d32 style R fill:#c8e6c9,stroke:#2e7d32

十进制作为中转站:任何进制转换都经过十进制,分两步走。

Step 1:任意进制 → 十进制(按权展开)

\[\text{result} = \sum_{i=0}^{len-1} \text{digit}_i \times n^{len-1-i} \]

代码中用 Horner 法(秦九韶算法)逐位累加,一次循环完成。

十六进制 A2F → 十进制 为例:

步骤 当前字符 转换值 ans = ans × 16 + val 累计 ans
初始 0
1 A \(10\) 0 × 16 + 10 10
2 2 \(2\) 10 × 16 + 2 162
3 F \(15\) 162 × 16 + 15 2607

\(A2F_{16} = 10 \times 16^2 + 2 \times 16 + 15 = 2560 + 32 + 15 = 2607_{10}\)

for (auto c : s) {
    if ('0' <= c && c <= '9') ans = ans * n + (c - '0');
    else                       ans = ans * n + (c - 'A' + 10);
}

Step 2:十进制 → 任意进制(除基取余,逆序输出)

反复对 \(m\) 取余得到每一位,最后逆序输出。

十进制 2607 → 十六进制 为例:

步骤 ans ans % 16 余数对应字符 更新 ans = ans / 16
1 2607 2607 % 16 = 15 F 162
2 162 162 % 16 = 2 2 10
3 10 10 % 16 = 10 A 0(结束)

字符记录顺序:F2A,逆序输出 → A2F

while (ans) {
    int t = ans % m;
    if (t < 10) a += t + '0';
    else        a += t - 10 + 'A';
    ans /= m;
}
// 逆序输出 a

注意:字母 A 表示 10,所以 c - 'A' + 10 将字母转为数值,t - 10 + 'A' 将数值转为字母。除基取余的顺序是低位 → 高位,所以最后必须逆序。

AC 代码

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n, m;
    string s;
    cin >> n >> s >> m;
    // Step 1: n 进制 → 十进制
    int ans = 0;
    for (auto c : s)
    {
        if ('0' <= c && c <= '9') ans = ans * n + c - '0';
        else                       ans = ans * n + c - 'A' + 10;
    }
    // Step 2: 十进制 → m 进制
    string a;
    while (ans)
    {
        int t = ans % m;
        if (t < 10) a += t + '0';
        else        a += t - 10 + 'A';
        ans /= m;
    }
    for (int i = a.size() - 1; i >= 0; i--) cout << a[i];
    return 0;
}

复杂度\(O(\log_m \text{ans} + |s|)\)。进制转换的循环次数与数值的对数成正比。

总结与拓展

  • 进制转换的核心是十进制作为中转站:任意进制 → 十进制(乘基累加)→ 目标进制(除基取余)。
  • \(n=10\)\(m=10\) 时可以把对应步骤省掉,但保留两步走逻辑更清晰。
  • 拓展:P1017 引入了负进制的概念,取模运算在负数情况下需要特殊处理。

例题 2:P1469 找筷子

知识点: 位运算 — 异或(XOR)的经典应用

题意

给定 \(n\) 个整数,其中恰好一个数出现了奇数次,其余数都出现了偶数次。找出这个数。

思路详解

本题是异或运算最经典的应用场景。核心性质:

\(a \oplus a = 0\)(一个数与自身异或,结果为 0)

\(a \oplus 0 = a\)(任何数与 0 异或,结果不变)

异或满足交换律和结合律

因此,将所有数全部异或起来:成对出现的数两两抵消归零,最终剩下的就是那个落单的数。

int ans = 0;
for (int i = 1; i <= n; i++) {
    cin >> x;
    ans ^= x;    // ans = ans ^ x
}
// ans 即为答案

教学推演:以 \(n=5\),筷子长度为 [2, 3, 2, 4, 3] 为例:

flowchart LR subgraph 初始 A0["ans = 0"] end subgraph step1["⊕ 2"] A1["ans = 2"] end subgraph step2["⊕ 3"] A2["ans = 2⊕3 = 1"] end subgraph step3["⊕ 2 ← 抵消!"] A3["ans = 1⊕2 = 3"] end subgraph step4["⊕ 4"] A4["ans = 3⊕4 = 7"] end subgraph step5["⊕ 3 ← 抵消!"] A5["ans = 7⊕3 = 4 ✓"] end A0 --> A1 --> A2 --> A3 --> A4 --> A5 style A0 fill:#f5f5f5,stroke:#999 style A5 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
步骤 操作 当前 ans(二进制) 说明
初始 000 \(0 \oplus a = a\)
1 ans ^= 2 010 2 进入
2 ans ^= 3 010 ^ 011 = 001 2 和 3 混在一起
3 ans ^= 2 001 ^ 010 = 011 = 3 🔥 第 2 个 2 把第 1 个 2 抵消了!
4 ans ^= 4 011 ^ 100 = 111 = 7 4 进入
5 ans ^= 3 111 ^ 011 = 100 = 4 🔥 第 2 个 3 把第 1 个 3 抵消了!

关键直觉:每个成对出现的数,第一次异或时"激活",第二次异或时"抵消"——因为 \(a \oplus a = 0\)。最终的 ans 就是那个只出现奇数次的落单数 4。

AC 代码

#include<bits/stdc++.h>
using namespace std;
int main()
{
    ios::sync_with_stdio(0);
    int ans = 0, n, x;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> x, ans ^= x;
    cout << ans;
    return 0;
}

复杂度\(O(n)\) 时间,\(O(1)\) 额外空间。不需要排序、不需要哈希表,仅用一个 int 变量。

总结与拓展

  • 异或的"自反性"(\(a \oplus a = 0\))是本题的核心,也是很多位运算技巧的基础。
  • 拓展场景:找出数组中两个落单的数(其余成对)→ 先全部异或得到 \(x \oplus y\),再按最低不同位分组分别异或。
  • 异或还常用于:交换两个变量(a^=b; b^=a; a^=b;)、简单加密(明文 \(\oplus\) 密钥 = 密文,密文 \(\oplus\) 密钥 = 明文)。

例题 3:P1100 高低位交换

知识点: 位运算 — 位移(>><<)与位掩码

题意

给定一个 32 位无符号整数 \(n\),将其高 16 位与低 16 位交换后输出。

思路详解

这是位移操作的经典练习题。32 位整数可以看作两段 16 位的数据拼接在一起:

Step 1:分离高低位

x = n & 0xFFFF0000;   // 用掩码取出高 16 位(低 16 位清零)
y = n & 0x0000FFFF;   // 用掩码取出低 16 位(高 16 位清零)

0xFFFF0000 的二进制是 11111111 11111111 00000000 00000000& 操作让高 16 位保留、低 16 位清零。

Step 2:移位交换

x >>= 16;    // 高 16 位移到低位
y <<= 16;    // 低 16 位移到高位

Step 3:合并

x | y        // 按位或,将两个错开的部分拼在一起

教学推演:以 \(n = 131073 = 2 \times 2^{16} + 1\) 为例(高 16 位 = 2,低 16 位 = 1):

flowchart TD subgraph 原始["原始 32 位整数 n = 0x00020001"] B0["00000000 00000010 | 00000000 00000001"] end subgraph 分离["Step 1: 位掩码分离"] B1["x = n & 0xFFFF0000<br/>00000000 00000010 | 00000000 00000000"] B2["y = n & 0x0000FFFF<br/>00000000 00000000 | 00000000 00000001"] end subgraph 移位["Step 2: 移位"] B3["x >>= 16<br/>00000000 00000000 | 00000000 00000010"] B4["y <<= 16<br/>00000000 00000001 | 00000000 00000000"] end subgraph 合并["Step 3: 按位或合并"] B5["x | y<br/>00000000 00000001 | 00000000 00000010 = 0x00010002 = 65538"] end B0 --> B1 B0 --> B2 B1 --> B3 B2 --> B4 B3 --> B5 B4 --> B5 style B0 fill:#e3f2fd,stroke:#1565c0 style B5 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px
步骤 二进制表示 十进制 说明
原始 \(n\) 00000000 00000010 00000000 00000001 131073 高 16 位 = 2,低 16 位 = 1
x = n & 0xFFFF0000 00000000 00000010 00000000 00000000 131072 掩码保留高 16 位,低 16 位归零
y = n & 0x0000FFFF 00000000 00000000 00000000 00000001 1 掩码保留低 16 位,高 16 位归零
x >>= 16 00000000 00000000 00000000 00000010 2 高位右移到低位
y <<= 16 00000000 00000001 00000000 00000000 65536 低位左移到高位
x | y 00000000 00000001 00000000 00000010 65538 合并:原低位移到高位,原高位移到低位 ✓

验证:\(65538 = 1 \times 2^{16} + 2\)——原低 16 位的 1 移到了高位,原高 16 位的 2 移到了低位。交换完成!

AC 代码

#include<bits/stdc++.h>
using namespace std;
int main()
{
    unsigned int n, x, y;
    cin >> n;
    x = n & 0xFFFF0000;    // 掩码取高 16 位
    y = n & 0x0000FFFF;    // 掩码取低 16 位
    x >>= 16;               // 高位移到低位
    y <<= 16;               // 低位移到高位
    cout << (x | y) << endl;
    return 0;
}

复杂度\(O(1)\)。纯粹的位运算,常数时间。

总结与拓展

  • 位掩码(Bit Mask):用 & 配合掩码可以提取任意位置的位。0xFFFF 是 16 位全 1 的掩码。
  • unsigned int vs int:右移时,unsigned int 高位补 0(逻辑右移),int 高位补符号位(算术右移)。处理位运算时建议使用 unsigned 类型避免符号位干扰。
  • 拓展:n = (n >> 16) | (n << 16) 一行代码即可完成交换(利用 unsigned 移位自动补 0 的特性),但分步写更容易理解。

模块二:计数原理与排列组合


例题 4:P1866 编号

知识点: 乘法原理 — 阶梯式乘法

题意

\(n\) 个变量,第 \(i\) 个变量的取值范围是 \([1, a_i]\),且所有变量的取值必须互不相同。求方案数对 \(10^9+7\) 取模。

思路详解

这是乘法原理的直接应用:每个位置可选的数量是"当前剩余可用的编号数"。

Step 1:排序

\(a_i\) 从小到大排序。排序后,第 \(i\) 个变量可选的编号数为 \(a_i - (i-1)\)(前面的 \(i-1\) 个变量已经用掉了 \(i-1\) 个编号)。

Step 2:累乘

\[\text{ans} = \prod_{i=1}^{n} (a_i - (i-1)) \]

如果某个 \(a_i - (i-1) \le 0\),说明无法分配(可选数量不足),答案为 0。

教学推演:以 \(n=3, a=[4, 2, 5]\) 为例:

flowchart LR subgraph 排序["Step 1: 排序"] S1["[4, 2, 5]"] -->|"sort"| S2["[2, 4, 5]"] end subgraph 逐位["Step 2: 逐位可选数"] T1["i=1: a₁=2<br/>可选: {1,2}<br/>2 种"] T2["i=2: a₂=4<br/>可选: 前面没用过的 {1,3,4}<br/>3 种"] T3["i=3: a₃=5<br/>可选: 前面没用过的 {3,4,5}<br/>3 种"] end S2 --> T1 --> T2 --> T3 T3 --> R["ans = 2×3×3 = 18"] style S1 fill:#e3f2fd,stroke:#1565c0 style R fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
步骤 \(a_i\)(排序后) \(i-1\)(已用) 可选数 累乘
\(i=1\) 2 0 \(2-0 = 2\) 2
\(i=2\) 4 1 \(4-1 = 3\) 6
\(i=3\) 5 2 \(5-2 = 3\) 18

答案 = 18 种方案。关键理解:排序保证了小范围的先选,避免了大范围的数"抢占"小范围数的名额。

AC 代码

#include<bits/stdc++.h>
using namespace std;
long long a[55], n, p = 1e9 + 7;
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + n + 1);
    long long ans = 1;
    for (int i = 1; i <= n; i++)
        ans = ans * (a[i] - (i - 1)) % p;
    cout << ans << endl;
    return 0;
}

复杂度\(O(n \log n)\),排序是瓶颈。

总结与拓展

  • 乘法原理的关键是排序后减去已用数量。排序保证了 \(a_i\) 单调递增,使得"前面用掉 \(i-1\) 个"成为有效的下界。
  • 拓展:如果不要求互不相同,则每个位置独立,答案为 \(\prod a_i\)(无排序需要)。

例题 5:P1246 [ECNA 1995] 编码

知识点: 排列数(组合序)— 字典序编码与组合数学

题意

按字典序给所有长度不超过 6 的严格递增字母串(字母从 az)编号。给定一个合法串,求其编号。

思路详解

本题的核心是:计算比当前串小的所有合法串的数量

方法一(DFS 打表):数据范围极小(最多 \(C_{26}^1 + C_{26}^2 + \cdots + C_{26}^6 \approx 313911\) 个串),可以直接 DFS 枚举所有合法串并编号。适合初学者理解题意。

void dfs(int u, string s) {
    if (u == m) { mp[s] = ++tot; return; }
    char i = (u == 0 ? 'a' : s[u-1] + 1);
    for (; i <= 'z'; i++) dfs(u + 1, s + i);
}

方法二(组合数公式):对于长度为 \(len\) 的串,可以用组合数直接算:

  • 所有长度 \(< len\) 的合法串数量 = \(\sum_{k=1}^{len-1} C_{26}^k\)
  • 对于长度等于 \(len\) 且字典序小于当前串的串,逐位枚举比当前位小的可用字母,累加 \(C_{剩余字母数}^{剩余位置数}\)

方法二适合拔高训练,让学生理解组合序的概念——与"下一个排列"计数异曲同工。

AC 代码(DFS 打表法)

#include<bits/stdc++.h>
using namespace std;
int n, m, tot;
map<string, int> mp;
void dfs(int u, string s)
{
    if (u == m)
    {
        mp[s] = ++tot;
        return;
    }
    char i;
    if (u == 0) i = 'a';
    else i = s[u-1] + 1;          // 严格递增:下一位必须比前一位大
    for (; i <= 'z'; i++)
        dfs(u + 1, s + i);
}
int main()
{
    string s;
    cin >> s;
    for (int i = 1; i <= 6; i++)  // 枚举所有长度
    {
        m = i;
        dfs(0, "");
    }
    cout << mp[s] << endl;
    return 0;
}

复杂度\(O(\sum_{k=1}^6 C_{26}^k)\) ≈ 31 万次 DFS,轻松通过。

总结与拓展

  • 字典序编号 = 计数"比它小的"有多少个——这是排列组合中常见的思路。
  • 严格递增 = 组合(与顺序无关,一旦选定字母集合,只有一种排列方式符合递增)。
  • 拓展:如果字母串不要求严格递增,则问题退化为 26 进制计数。

例题 6:P2822 [NOIP 2016 提高组] 组合数问题

知识点: 杨辉三角递推 + 取模 + 二维前缀和

题意

给定 \(t\) 组询问,每组询问 \(n, m\),求 \(C_i^j\)\(0 \le i \le n, 0 \le j \le \min(i, m)\))中有多少个是 \(k\) 的倍数。

思路详解

本题是非常典型的预处理 + 查询模式,三步走:

Step 1:杨辉三角递推求组合数(模 \(k\)

\[C[i][j] = (C[i-1][j-1] + C[i-1][j]) \bmod k \]

初始化:\(C[i][0] = C[i][i] = 1 \bmod k\)

为什么直接模 \(k\) 我们只关心 \(C[i][j]\) 是否为 \(k\) 的倍数(即模 \(k\) 余 0),不关心具体值。递推过程中全程模 \(k\) 避免了高精度。

Step 2:二维前缀和统计

\(k=2\) 为例,杨辉三角的前 5 行模 2:

flowchart TD subgraph 杨辉三角模2["杨辉三角 C[i][j] mod 2"] R1["i=0: 1"] R2["i=1: 1 1"] R3["i=2: 1 0 1"] R4["i=3: 1 1 1 1"] R5["i=4: 1 0 0 0 1"] end subgraph 标记k的倍数["C[i][j]==0 的位置 (k=2 的倍数)"] M3["i=2,j=1: 0 ✓"] M5["i=4,j=1: 0 ✓<br/>i=4,j=2: 0 ✓<br/>i=4,j=3: 0 ✓"] end subgraph 前缀和["二维前缀和 s[i][j]"] SR["s[4][4] = 被框住的 0 的个数"] end R1 --> R2 --> R3 --> R4 --> R5 M3 -.-> SR M5 -.-> SR style M3 fill:#ffcdd2,stroke:#c62828 style M5 fill:#ffcdd2,stroke:#c62828

\[s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + (C[i][j] == 0) \]

\(s[i][j]\) 表示所有满足 \(0 \le x \le i, 0 \le y \le j\)\(C[x][y]\)\(k\) 的倍数的个数。

flowchart LR subgraph 前缀和递推["二维前缀和递推公式图解"] direction LR A["s[i-1][j-1]"] --> B["s[i-1][j]"] A --> C["s[i][j-1]"] B --> D["s[i][j] = <br/>s[i-1][j] + s[i][j-1]<br/>- s[i-1][j-1]<br/>+ (C[i][j]==0)"] C --> D end style D fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

直观理解\(s[i-1][j]\) 覆盖了左上矩形,\(s[i][j-1]\) 覆盖了左下矩形,\(s[i-1][j-1]\) 被加了两次所以要减掉一次。

Step 3:O(1) 回答询问

对于询问 \((n, m)\),如果 \(m > n\) 则令 \(m = n\)(因为 \(j\) 最大为 \(i\)\(n\)),输出 \(s[n][m]\)

AC 代码

#include<bits/stdc++.h>
using namespace std;
const int N = 2e3 + 5;
int t, k, n, m, c[N][N], s[N][N];
int main()
{
    cin >> t >> k;
    memset(c, -1, sizeof(c));
    c[1][1] = 1; c[1][0] = 1;
    // Step 1: 杨辉三角递推(模 k)
    for (int i = 2; i < N; i++)
        for (int j = 0; j <= i; j++)
        {
            if (j == 0 || j == i) c[i][j] = 1;
            else c[i][j] = (c[i-1][j-1] + c[i-1][j]) % k;
        }
    // Step 2: 二维前缀和
    for (int i = 1; i < N; i++)
        for (int j = 1; j < N; j++)
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + (c[i][j] == 0);
    // Step 3: O(1) 回答
    while (t--)
    {
        cin >> n >> m;
        if (m > n) m = n;
        cout << s[n][m] << endl;
    }
    return 0;
}

复杂度\(O(N^2)\) 预处理 + \(O(t)\) 查询。\(N=2000\)\(N^2=4\times 10^6\),在可接受范围内。

总结与拓展

  • 帕斯卡公式\(C(n,m) = C(n-1,m-1) + C(n-1,m)\),即杨辉三角的递推关系。
  • 模运算与递推兼容:在递推过程中直接模 \(k\),因为加减乘的模运算可交换序。
  • 前缀和套路:将"区间内满足某条件的个数"转化为前缀和相减。二维前缀和公式:\(s[x_2][y_2] - s[x_1-1][y_2] - s[x_2][y_1-1] + s[x_1-1][y_1-1]\)

模块三:整除理论 — 质数与素数筛法


例题 7:P3383 【模板】线性筛素数

知识点: 素数筛法 — 埃氏筛 vs 线性筛

题意

给定 \(n, q\),输出第 \(k\) 小的素数(\(q\) 次询问)。

思路详解

方法一:埃拉托斯特尼筛法(埃氏筛)

基本思想:从 2 开始,遇到一个未标记的数就将其加入素数列表,并将其所有倍数标记为合数。

flowchart TD subgraph 埃氏筛过程["埃氏筛 n=10 全过程"] S2["i=2: 素数 ✓ → 筛掉 4,6,8,10"] S3["i=3: 素数 ✓ → 筛掉 6,9"] S4["i=4: 已筛掉 ✗"] S5["i=5: 素数 ✓ → 筛掉 10"] end S2 --> S3 --> S4 --> S5 style S2 fill:#c8e6c9,stroke:#2e7d32 style S3 fill:#c8e6c9,stroke:#2e7d32 style S4 fill:#ffcdd2,stroke:#c62828 style S5 fill:#c8e6c9,stroke:#2e7d32

问题所在:6 被 \(i=2\)\(i=3\) 筛了两次,10 被 \(i=2\)\(i=5\) 筛了两次。每个合数被其所有质因子重复筛——这是 \(O(n \log \log n)\) 而非 \(O(n)\) 的原因。

for (int i = 2; i <= n; i++) {
    if (!vis[i]) {
        prime[++cnt] = i;
        for (int j = i * 2; j <= n; j += i)  // 标记 i 的所有倍数
            vis[j] = 1;
    }
}

时间复杂度\(O(n \log \log n)\)。每个合数被其所有质因子各筛一次。对于 \(n=10^8\) 勉强可过但常数较大。

方法二:欧拉线性筛

核心优化:每个合数只被它的最小质因子筛掉一次,做到 \(O(n)\)

flowchart TD subgraph 线性筛过程["线性筛 n=10 全过程"] L2["i=2: 素数 ✓ → prime=[2]<br/>2×2=4 筛掉, 2%2=0 break"] L3["i=3: 素数 ✓ → prime=[2,3]<br/>3×2=6 筛掉, 3%2≠0 继续<br/>3×3=9 筛掉, 3%3=0 break"] L4["i=4: 合数 →<br/>4×2=8 筛掉, 4%2=0 break<br/>⚠️ 不会用 3 筛 12!"] end L2 --> L3 --> L4 style L2 fill:#c8e6c9,stroke:#2e7d32 style L3 fill:#c8e6c9,stroke:#2e7d32 style L4 fill:#fff9c4,stroke:#f9a825

关键区别:6 只被 \(i=3\)\(prime[1]=2\) 筛了一次(最小质因子是 2)。而埃氏筛中 6 被筛了两次。每个合数有唯一的最小质因子,线性筛利用这一点做到 \(O(n)\)

for (int i = 2; i <= n; i++) {
    if (!vis[i]) prime[++cnt] = i;
    for (int j = 1; j <= cnt && i * prime[j] <= n; j++) {
        vis[i * prime[j]] = 1;
        if (i % prime[j] == 0) break;   // ← 关键!保证 prime[j] 是最小质因子
    }
}

if (i % prime[j] == 0) break; 为什么是关键?

以下用 \(n=20\) 演示线性筛的完整执行过程:

线性筛执行追踪(\(n=20\)
循环 \(i\) !vis[i]? prime 列表 内层: j / p=prime[j] i×p vis[i×p]=1 i%p==0? break?
2 ✅ 素数 [2] j=1, p=2 4 vis[4]=1 ✅ 2%2=0 break
3 ✅ 素数 [2,3] j=1, p=2 6 vis[6]=1 ❌ 3%2≠0 继续
j=2, p=3 9 vis[9]=1 ✅ 3%3=0 break
4 ❌ 合数 [2,3] j=1, p=2 8 vis[8]=1 ✅ 4%2=0 break
5 ✅ 素数 [2,3,5] j=1, p=2 10 vis[10]=1 继续
j=2, p=3 15 vis[15]=1 继续
j=3, p=5 25>20 越界停止

关键观察

  • \(i=4\) 时,\(4 \% 2 = 0\) 立即 break。如果继续用 \(p=3\)\(4 \times 3 = 12\),但 12 的最小质因子是 2(不是 3),12 会在后面 \(i=6\) 时被 \(p=2\) 正确筛掉。若这里不 break,12 就被重复筛了两次!
  • \(i=3\) 时,\(3 \% 2 \neq 0\) 不 break,继续用 \(p=3\)\(3 \times 3 = 9\)。因为 9 的最小质因子确实是 3,在这里筛掉是正确且唯一的时机。

每个合数恰好被它的最小质因子筛掉一次——这是 \(O(n)\) 的根本保障。

每个合数被筛的时刻一览
合数 最小质因子 \(i=?\) 时被筛 筛它的 \(p\)
4 2 \(i=2\) \(p=2\)
6 2 \(i=3\) \(p=2\)
8 2 \(i=4\) \(p=2\)
9 3 \(i=3\) \(p=3\)
10 2 \(i=5\) \(p=2\)
12 2 \(i=6\) \(p=2\) (不是 \(i=4,p=3\)!)
14 2 \(i=7\) \(p=2\)
15 3 \(i=5\) \(p=3\)
16 2 \(i=8\) \(p=2\)
18 2 \(i=9\) \(p=2\)
20 2 \(i=10\) \(p=2\)

每个合数恰好出现一次——这就是线性复杂度的本质。

\(i\) \(prime[j]\) \(i \times prime[j]\) \(i \% prime[j] == 0\)? 是否 break
2 2 4 break
3 2 6 继续
3 3 9 break
4 2 8 break(不会用 3 筛 12)
5 2 10 继续
5 3 15 继续
5 5 25 break

埃氏筛 vs 线性筛对比

埃氏筛 线性筛
时间复杂度 \(O(n \log \log n)\) \(O(n)\)
空间复杂度 \(O(n)\) \(O(n)\)
代码复杂度 简单 略复杂
能顺便求最小质因子吗
\(n=10^7\) 实测 ~0.3s ~0.15s

AC 代码(线性筛)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e8 + 5, M = 6e6 + 5;
bool vis[N];
int a[M], cnt;
void f()
{
    vis[1] = 1;
    for (int i = 2; i < N; i++)        // 保证了每个数是被最小的质数筛掉的
    {
        if (!vis[i]) a[++cnt] = i;
        for (int j = 1; j <= cnt && i * a[j] < N; j++)
        {
            vis[i * a[j]] = 1;
            if (i % a[j] == 0) break;  // 保证最小质因子
        }
    }
}
int main()
{
    f();
    int n, k, x;
    cin >> n >> k;
    while (k--) cin >> x, cout << a[x] << endl;
    return 0;
}

复杂度\(O(n)\) 预处理 + \(O(1)\) 查询。

总结与拓展

  • 线性筛的核心:if (i % prime[j] == 0) break; 保证了每个合数只被其最小质因子标记。
  • 线性筛不仅可以筛素数,还能顺便求出每个数的最小质因子(记录 minp[i]),这是埃氏筛做不到的。
  • 拓展:杜教筛、Min_25 筛等可以在亚线性时间内求前缀素数个数(NOI 级内容)。

例题 8:P1835 素数密度

知识点: 区间筛法 — 用小区间素数筛大区间合数

题意

给定区间 \([L, R]\)\(R-L \le 10^6\)\(L, R \le 2^{31}-1\)),求区间内的素数个数。

思路详解

问题分析\(R\) 可能高达 \(2\times 10^9\),不能直接开数组到 \(R\)。但区间长度 \(R-L \le 10^6\),可利用区间筛法

核心思想

  1. 任何一个合数 \(x\) 必定有一个 \(\le \sqrt{x}\) 的质因子。
  2. \(\sqrt{2^{31}} \approx 46340\),只需先生成 \([2, 50000]\) 内的所有素数(埃氏筛即可)。
  3. 用这些素数去标记 \([L, R]\) 中的合数:对于每个素数 \(p\),从 \(L\) 开始的第一个 \(p\) 的倍数开始,每次加 \(p\) 标记。

区间映射:开一个大小为 \(R-L+1\) 的数组 vis[0..R-L]vis[i] 对应 \(L+i\) 是否为合数。

flowchart TD subgraph 小区间["Step 1: 生成小素数 (≤ sqrt(R))"] P["[2, 50000] 埃氏筛 →<br/>prime = {2, 3, 5, 7, 11, ...}"] end subgraph 大区间映射["Step 2: 区间映射"] M["数组 vis[0 .. R-L]<br/>vis[i] 代表数字 (L + i)"] end subgraph 筛法过程["Step 3: 用小区间素数筛大区间"] S["对每个素数 p:<br/>找到第一个 ≥L 的 p 的倍数<br/>每次 +p 标记 vis 数组"] end P -->|"tot 个小素数"| S M --> S S --> R2["统计: vis[i]==0 的个数<br/>= 区间素数个数"] style P fill:#e3f2fd,stroke:#1565c0 style M fill:#fff3e0,stroke:#e65100 style S fill:#e8f5e9,stroke:#2e7d32 style R2 fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px

具体示例\(L=100, R=120\),用素数 \(p=3\) 筛:

下标 i 0 1 2 3 ... 20
实际数值 \(L+0=100\) 101 102 103 ... 120
筛 3 的倍数 vis[i] ✅ (102=3×34) ... ❌ (120=3×40 但已过)

102 是 ≥100 的第一个 3 的倍数 → vis[2] = 1。继续 +3:105 (vis[5]=1)、108、111、114、117、120 依次标记。

for (int i = 1; i <= tot; i++) {            // 遍历每个小素数 p
    int p = prime[i];
    for (int j = R / p; j >= 2; j--) {      // 从大到小找第一个 ≥ L 的 p 的倍数
        if (j * p < L) break;
        vis[j * p - L] = 1;                 // 映射到区间下标
    }
}

细节:如果 \(L=1\),1 不是素数也不是合数,需要从 \(L=2\) 开始。

AC 代码

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e6 + 5;
int f[N], p[N], tot, vis[N];
void init()
{
    for (int i = 2; i <= 50000; i++)       // 只需要到 sqrt(R_max)
    {
        if (f[i]) continue;
        p[++tot] = i;
        for (int j = 2 * i; j <= 50000; j += i) f[j] = 1;
    }
}
void solve()
{
    int a, b;
    cin >> a >> b;
    if (a == 1) a = 2;                     // 1 不是素数
    init();
    for (int i = 1; i <= tot; i++)
    {
        for (int j = b / p[i]; j >= 2; j--)
        {
            if (j * p[i] < a) break;
            vis[j * p[i] - a] = 1;         // 标记为合数
        }
    }
    int ans = 0;
    for (int i = 0; i <= b - a; i++)
        if (!vis[i]) ans++;
    cout << ans << endl;
}
int main()
{
    solve();
    return 0;
}

复杂度\(O(\sqrt{R} \log \log \sqrt{R} + (R-L) \log \log R)\)\(\sqrt{R} \approx 5\times 10^4\) 部分可忽略,主复杂度 \(O((R-L) \log \log R)\)

总结与拓展

  • 区间筛的核心:大范围 → 小范围映射,用 \(\sqrt{R}\) 内的素数去筛 \([L, R]\)
  • 区间长度 \(10^6\) 是关键限制——使其成为可行的 \(O((R-L) \log \log R)\) 算法。
  • 拓展:区间筛也可用于求区间内的欧拉函数值、约数个数等积性函数。

模块三:整除理论 — 约数、倍数与因数


例题 9:P2926 [USACO08DEC] Patting Heads S

知识点: 倍数枚举法 — 类似于埃氏筛的逆过程

题意

给定 \(n\) 个数 \(a_1 \dots a_n\),对于每个 \(i\),求 \(a_1 \dots a_n\) 中(除自身外)能被 \(a_i\) 整除的数的个数。

思路详解

朴素做法:对于每个 \(a_i\),遍历所有 \(a_j\) 判断整除关系 → \(O(n^2)\)\(n \le 10^5\) 会超时。

倍数枚举法:换一个角度——统计每个数出现的次数,然后对于每个数 \(x\),枚举它的所有倍数 \(x, 2x, 3x, \dots\),将 \(cnt[x]\) 加到 \(ans[倍数]\) 上。

flowchart TD subgraph 统计["Step 1: 统计频次 cnt[x]"] C1["cnt[2]=2, cnt[3]=1, cnt[4]=1"] end subgraph 枚举["Step 2: 倍数枚举"] E1["x=2 (出现2次):<br/>ans[2]+=2, ans[4]+=2, ans[6]+=2, ..."] E2["x=3 (出现1次):<br/>ans[3]+=1, ans[6]+=1, ans[9]+=1, ..."] E3["x=4 (出现1次):<br/>ans[4]+=1, ans[8]+=1, ..."] end C1 --> E1 C1 --> E2 C1 --> E3 style C1 fill:#e3f2fd,stroke:#1565c0 style E1 fill:#fff9c4,stroke:#f9a825 style E2 fill:#fff9c4,stroke:#f9a825 style E3 fill:#fff9c4,stroke:#f9a825

关键视角转换:不是枚举 \((i,j)\) 对判整除(\(O(n^2)\)),而是枚举每个值的所有倍数(\(O(N \log N)\))。换个方向,效率天壤之别。

for (int i = 1; i < N; i++) {
    if (!cnt[i]) continue;
    for (int j = i; j < N; j += i)   // 枚举 i 的所有倍数
        ans[j] += cnt[i];            // i 的倍数可以被 i 整除
}

直觉:埃氏筛是"遇到素数,标记所有倍数",这里是"对于每个出现过的数,把它的贡献加到所有倍数上"。方向恰好相反。

复杂度分析:内层循环次数 = \(\sum_{i=1}^{N} \frac{N}{i} = N \cdot H_N \approx N \ln N\)(调和级数)。\(N=10^6\) 时约 \(1.4 \times 10^7\) 次,完全可以接受。

AC 代码

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N = 1e6 + 5;
int cnt[N], a[N], n, m, ans[N];
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i], cnt[a[i]]++;
    for (int i = 1; i < N; i++)
    {
        if (!cnt[i]) continue;
        for (int j = i; j < N; j += i) ans[j] += cnt[i];
    }
    for (int i = 1; i <= n; i++) cout << ans[a[i]] - 1 << endl;  // 减掉自身
    return 0;
}

复杂度\(O(N \log N)\)\(N\) 为值域大小。

总结与拓展

  • 倍数枚举法是处理"整除关系统计"的通用技巧。当值域不大(\(\le 10^6\))时可用。
  • 思路转换:不枚举"每对 \((i,j)\) 判整除",而枚举"每个数的所有倍数"——复杂度从 \(O(n^2)\) 降为 \(O(N \log N)\)
  • 拓展:埃氏筛、倍数枚举、莫比乌斯反演都利用了类似的"调和级数"复杂度结构。

例题 10:P1403 [AHOI2005] 约数研究

知识点: 除数函数 — \(\sum_{i=1}^n d(i)\) 的公式求法

题意

\(\sum_{i=1}^{n} d(i)\),其中 \(d(i)\) 表示 \(i\) 的约数个数。\(n \le 10^6\)

思路详解

方法一(倍数枚举法):用上一题的技巧,枚举每个数并为其倍数贡献约数计数 → \(O(n \log n)\),可以通过。

方法二(公式法):核心观察——

\(1 \sim n\) 中,约数 \(i\) 出现了多少次?答:\(i\) 的倍数 \(i, 2i, 3i, \dots\) 都包含 \(i\) 这个约数。所以约数 \(i\) 出现的次数 = \(\lfloor n/i \rfloor\)

flowchart TD subgraph 视角1["❌ 旧视角: 统计每个数的约数个数"] V1["d(1)=1, d(2)=2, d(3)=2, d(4)=3, ...<br/>逐个分解,费时费力"] end subgraph 视角2["✅ 新视角: 统计每个约数出现了几次"] V2["1 出现了 ⌊n/1⌋ 次 (1,2,3,4,5,6 都含因子1)<br/>2 出现了 ⌊n/2⌋ 次 (2,4,6 含因子2)<br/>3 出现了 ⌊n/3⌋ 次 (3,6 含因子3)<br/>..."] end 视角1 -.->|"换个角度"| 视角2 style V1 fill:#ffcdd2,stroke:#c62828 style V2 fill:#c8e6c9,stroke:#2e7d32

两种视角统计的是同一个东西,但新视角只需一行代码 sum += n/i,把 \(O(n \log n)\) 的分解降为 \(O(n)\) 的除法。

因此:

\[\sum_{i=1}^{n} d(i) = \sum_{i=1}^{n} \left\lfloor \frac{n}{i} \right\rfloor \]

直接 \(O(n)\) 求和即可。如果想进一步优化到 \(O(\sqrt{n})\),可用整除分块\(\lfloor n/i \rfloor\) 仅有 \(O(\sqrt{n})\) 个不同取值)。

教学推演:以 \(n=6\) 为例:

\(i\) \(\lfloor 6/i \rfloor\) 含义
1 6 1 是 1,2,3,4,5,6 的约数 → 出现 6 次
2 3 2 是 2,4,6 的约数 → 出现 3 次
3 2 3 是 3,6 的约数 → 出现 2 次
4 1 4 是 4 的约数 → 出现 1 次
5 1 5 是 5 的约数 → 出现 1 次
6 1 6 是 6 的约数 → 出现 1 次

总和 = \(6+3+2+1+1+1 = 14\)。手动验证:\(d(1)=1, d(2)=2, d(3)=2, d(4)=3, d(5)=2, d(6)=4\),总和 = \(1+2+2+3+2+4 = 14\)

AC 代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
    ll n, b = 0;
    cin >> n;
    for (int i = 1; i <= n; i++)
        b += n / i;
    cout << b << endl;
    return 0;
}

复杂度\(O(n)\)\(n=10^6\) 秒过。若 \(n\) 达到 \(10^{12}\) 级别,可用整除分块做到 \(O(\sqrt{n})\)

总结与拓展

  • 除数函数前缀和公式\(\sum_{i=1}^n d(i) = \sum_{i=1}^n \lfloor n/i \rfloor\)。这是数论中的经典结论。
  • 直觉:换个角度统计——不统计"每个数有多少约数",而统计"每个约数出现了多少次"。
  • 拓展:整除分块可以在 \(O(\sqrt{n})\) 内计算 \(\sum \lfloor n/i \rfloor\)\(\sum \lfloor n/i \rfloor \times f(i)\) 等和式。

模块三:整除理论 — 最大公约数与最小公倍数(GCD/LCM)


例题 11:P1029 [NOIP 2001 普及组] 最大公约数和最小公倍数问题

知识点: GCD 与 LCM 的核心公式 \(a \times b = \gcd(a,b) \times \operatorname{lcm}(a,b)\)

题意

给定两个自然数 \(x_0\)(GCD)和 \(y_0\)(LCM),求满足 \(\gcd(P, Q) = x_0\)\(\operatorname{lcm}(P, Q) = y_0\) 的有序对 \((P, Q)\) 的数量。

思路详解

核心公式

\[P \times Q = \gcd(P, Q) \times \operatorname{lcm}(P, Q) = x_0 \times y_0 \]

flowchart LR subgraph 分解["质因数分解视角"] F1["P = 2² × 3¹"] F2["Q = 2¹ × 3³"] end subgraph GCD["GCD = 取 min 指数"] G1["gcd = 2¹ × 3¹ = 6"] end subgraph LCM["LCM = 取 max 指数"] L1["lcm = 2² × 3³ = 108"] end subgraph 乘积["P × Q = GCD × LCM"] M1["P×Q = 2³ × 3⁴ = 648<br/>GCD×LCM = 2¹×3¹ × 2²×3³<br/>= 2³ × 3⁴ = 648 ✓"] end F1 --> G1 F2 --> G1 F1 --> L1 F2 --> L1 G1 --> M1 L1 --> M1 style G1 fill:#e3f2fd,stroke:#1565c0 style L1 fill:#fff3e0,stroke:#e65100 style M1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

直觉\(\min(a,b) + \max(a,b) = a + b\)。每个质因子的指数也是这样——GCD 取 min,LCM 取 max,两者之和恰好是原数指数之和。乘起来就得到 \(P \times Q = \gcd \times \operatorname{lcm}\)

有了这个公式,可以枚举 \(P\)\(P\) 必须是 \(x_0 \times y_0\) 的约数),\(Q = (x_0 \times y_0) / P\),然后验证 \(\gcd(P, Q) = x_0\)\(\operatorname{lcm}(P, Q) = y_0\) 是否成立。

优化枚举\(P\) 只需枚举到 \(\sqrt{x_0 \times y_0}\),对于每个 \(P\) 可以同时得到 \((P, Q)\)\((Q, P)\) 两对(当 \(P \ne Q\) 时)。

教学推演:以 \(x_0=3, y_0=60\) 为例:

\(P \times Q = 3 \times 60 = 180\)。枚举 180 的约数作为 \(P\)

\(P\) \(Q = 180/P\) \(\gcd(P,Q) = 3\)? \(\operatorname{lcm}(P,Q) = 60\)? 合法?
3 60 \((3,60)\)
12 15 \((12,15)\)
15 12 \((15,12)\)
60 3 \((60,3)\)

答案 = 4。有序对意味着 \((3,60)\)\((60,3)\) 算两对。

AC 代码

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<int> g;
int main()
{
    int x0, y0;
    cin >> x0 >> y0;
    // 枚举 y0 的所有约数(也是 x0*y0 的约数)
    for (int i = 1; i * i <= y0; i++)
        if (y0 % i == 0)
        {
            g.push_back(i);
            if (i * i == y0) continue;
            g.push_back(y0 / i);
        }
    int ans = 0;
    for (int i = 0; i < g.size(); i++)
        for (int j = 0; j < g.size(); j++)
        {
            int x = g[i], y = g[j];
            if (__gcd(x, y) == x0 && x * y / __gcd(x, y) == y0) ans++;
        }
    cout << ans << endl;
    return 0;
}

复杂度\(O(d(y_0)^2 \log \max(x,y))\),其中 \(d(y_0)\)\(y_0\) 的约数个数。\(y_0 \le 10^5\) 时约数最多约 128 个,平方后仍可接受。

总结与拓展

  • 核心公式\(a \times b = \gcd(a,b) \times \operatorname{lcm}(a,b)\)。证明思路:分解质因数后,\(\gcd\)\(\min\) 指数,\(\operatorname{lcm}\)\(\max\) 指数,两者相乘刚好是原数指数之和。
  • C++17 起 <algorithm> 提供了 std::gcdstd::lcm。GCC 内置的 __gcd 也可用(注意两个下划线)。
  • 拓展:若有多个数,\(\gcd(a,b,c) = \gcd(\gcd(a,b), c)\),但 \(\operatorname{lcm}\) 无法用 \(\prod / \gcd\) 推广到三个以上。

例题 12:P1072 [NOIP 2009 提高组] Hankson 的趣味题

知识点: GCD/LCM 综合 — 暴力枚举约数 + 质因数分解

题意

给定 \(a_0, a_1, b_0, b_1\),求满足 \(\gcd(x, a_0) = a_1\)\(\operatorname{lcm}(x, b_0) = b_1\)\(x\) 的个数。

思路详解

这是数论综合性较强的经典题。两条约束可以分开利用:

flowchart TD subgraph 约束1["约束 1: gcd(x, a₀) = a₁"] C1A["x 必须是 a₁ 的倍数"] C1B["且 gcd(x/a₁, a₀/a₁) = 1<br/>(除去 a₁ 后互质)"] end subgraph 约束2["约束 2: lcm(x, b₀) = b₁"] C2A["x 必须是 b₁ 的约数<br/>(lcm(x,b₀)=b₁ ⇒ x∣b₁)"] end subgraph 枚举["枚举候选"] E["枚举 b₁ 的所有约数作为 x"] end subgraph 验证["验证两条约束"] V["gcd(x, a₀) == a₁ ?<br/>b₀/gcd(x,b₀) == b₁/x ?"] end 约束1 --> E 约束2 --> E E --> V V -->|"✅ 满足"| ANS["ans++"] V -->|"❌ 不满足"| SKIP["跳过"] style C1A fill:#e3f2fd,stroke:#1565c0 style C2A fill:#fff3e0,stroke:#e65100 style E fill:#e8f5e9,stroke:#2e7d32 style V fill:#f3e5f5,stroke:#7b1fa2 style ANS fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

约束 1\(\gcd(x, a_0) = a_1\)\(x\)\(a_1\) 的倍数,且 \(\gcd(x/a_1, a_0/a_1) = 1\)

约束 2\(\operatorname{lcm}(x, b_0) = b_1\)\(x\)\(b_1\) 的约数(因为 \(\operatorname{lcm}(x, b_0) = b_1\) 说明 \(x \mid b_1\))。

结合:\(x\) 同时是 \(a_1\) 的倍数和 \(b_1\) 的约数 → \(x\)\(b_1\) 的约数。

算法

  1. 枚举 \(b_1\) 的所有约数作为候选 \(x\)
  2. 验证两个条件:\(\gcd(x, a_0) = a_1\)\(b_0 / \gcd(x, b_0) = b_1 / x\)(等价于 \(\operatorname{lcm}(x, b_0) = b_1\))。

为什么用 \(b_0 / \gcd(x, b_0) = b_1 / x\) 而不是直接算 LCM? 避免溢出——\(x \times b_0\) 可能超过 int 范围,而这种形式只需除法。

AC 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
vector<int> g;
int main()
{
    int n, a0, a1, b0, b1;
    cin >> n;
    while (n--)                               // 最多 2×10³ 组
    {
        cin >> a0 >> a1 >> b0 >> b1;
        g.clear();
        for (int i = 1; i * i <= b1; i++)     // 枚举 b1 的约数
            if (b1 % i == 0)
            {
                g.push_back(i);
                if (i * i == b1) continue;
                g.push_back(b1 / i);
            }
        int ans = 0;
        for (int i = 0; i < g.size(); i++)
        {
            int x = g[i];
            if (__gcd(x, a0) == a1 && b0 / __gcd(x, b0) == b1 / x)
                ans++;
        }
        cout << ans << endl;
    }
    return 0;
}

复杂度\(O(n \times d(b_1) \times \log V)\)\(b_1 \le 2\times 10^9\) 时约数最多约 1536 个,总计算量在可接受范围内。

总结与拓展

  • \(\operatorname{lcm}\) 约束可以转化为约数关系 \(x \mid b_1\),缩小枚举范围。
  • 避免溢出技巧:\(\operatorname{lcm}(x, b_0) = b_1 \iff b_0 / \gcd(x, b_0) = b_1 / x\)(两边都是整除)。
  • 本题也可以从质因数分解角度做:分别考虑每个质因子的指数约束区间。

模块三:整除理论 — 算术基本定理(质因数分解)


例题 13:P1069 [NOIP 2009 普及组] 细胞分裂

知识点: 质因数分解 — 指数比较法

题意

\(n\) 种细胞,第 \(i\) 种每秒分裂成 \(a_i\) 倍。试管中需达到 \(m_1^{m_2}\) 个细胞(均匀分装)。求最早在第几秒可以开始实验(即 \(\exists t\) 使得 \(a_i^t\)\(m_1^{m_2}\) 的倍数)。若所有细胞都无法满足,输出 \(-1\)

思路详解

\(a_i^t \mid m_1^{m_2}\) 转化为质因数指数的比较。

flowchart TD subgraph 目标["目标: m₁ᵐ²"] T1["m₁ = 2³ × 3¹<br/>m₁ᵐ² = 2^(3×m₂) × 3^(1×m₂)"] end subgraph 细胞["细胞 aᵢ"] C1["aᵢ = 2² × 3¹<br/>✅ 包含所有必要质因子"] C2["aᵢ = 2² × 5¹<br/>❌ 缺少质因子 3"] end subgraph 指数比较["指数比较: 求最小 t"] E1["质因子 2: 2×t ≥ 3×m₂ → t ≥ ⌈3m₂/2⌉<br/>质因子 3: 1×t ≥ 1×m₂ → t ≥ m₂<br/>取 max → t = max(⌈3m₂/2⌉, m₂)"] E2["直接不可行 → f=0"] end T1 --> C1 T1 --> C2 C1 --> E1 C2 --> E2 style T1 fill:#e3f2fd,stroke:#1565c0 style E1 fill:#c8e6c9,stroke:#2e7d32 style E2 fill:#ffcdd2,stroke:#c62828

核心转化:整除关系 \(A \mid B\) ≈ 每个质因子的指数 \(A\) 都不超过 \(B\)。把"大数能否整除"变成了"指数的不等式比较",清晰且高效。

Step 1:对 \(m_1\) 做质因数分解

\[m_1 = \prod_{j=1}^{k} p_j^{c_j} \quad \Rightarrow \quad m_1^{m_2} = \prod_{j=1}^{k} p_j^{c_j \times m_2} \]

Step 2:对于每种细胞 \(a_i\),检查每个质因子 \(p_j\)

  • \(a_i\) 不包含 \(p_j\) → 该细胞永远无法满足要求(\(f=0\))。
  • \(a_i\) 包含 \(p_j\),设 \(a_i\)\(p_j\) 的指数为 \(e_j\),则 \(a_i^t\)\(p_j\) 的指数为 \(e_j \times t\)。需要 \(e_j \times t \ge c_j \times m_2\),即 \(t \ge \lceil \frac{c_j \times m_2}{e_j} \rceil\)

对该细胞的所有质因子取 \(\max\) 得到该细胞所需的最小时间。

Step 3:所有细胞中取 \(\min\)

AC 代码

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pa;
const int N = 1e6 + 5;
int n, m1, m2, cnt[N], p[N], tot;
void init()
{
    for (int i = 2; i * i <= m1; i++)
    {
        int s = 0;
        if (m1 % i) continue;
        while (m1 && m1 % i == 0) m1 /= i, s++;
        p[++tot] = i; cnt[tot] = s * m2;
    }
    if (m1 > 1) p[++tot] = m1, cnt[tot] = m2;  // 剩余的大质因子
}
int calc(int x, int a)                           // 计算 x 中质因子 a 的指数
{
    int s = 0;
    while (x && x % a == 0) x /= a, s++;
    return s;
}
int main()
{
    cin >> n >> m1 >> m2;
    if (m1 == 1) { cout << 0; return 0; }        // 试管只需 1 个细胞,第 0 秒即可
    init();
    int x, ans = 0x3f3f3f3f;
    for (int i = 1; i <= n; i++)
    {
        cin >> x;
        int f = 1, tmp = -1;
        for (int j = 1; j <= tot; j++)
        {
            if (x % p[j]) { f = 0; break; }      // 缺少必要质因子 → 不可能
            int c = calc(x, p[j]);
            tmp = max(tmp, (cnt[j] - 1) / c + 1); // 上取整:ceil(cnt[j] / c)
        }
        if (f) ans = min(ans, tmp);
    }
    if (ans == 0x3f3f3f3f) cout << -1 << endl;
    else cout << ans << endl;
    return 0;
}

复杂度\(O(\sqrt{m_1} + n \times k \times \log a_i)\),其中 \(k\)\(m_1\) 的不同质因子个数(\(\le 9\)\(m_1 \le 2\times 10^9\))。

总结与拓展

  • 整除关系 → 指数比较\(A \mid B\) 等价于对于每个质因子 \(p\)\(A\)\(p\) 的指数 \(\le B\)\(p\) 的指数。
  • 上取整技巧:\(\lceil a / b \rceil = (a - 1) / b + 1\)(整数除法),等价于 (a + b - 1) / b
  • 拓展:质因数分解是数论的"原子操作"。约数个数、约数和、欧拉函数等都可以通过质因数分解得到。

常见错误与坑点

错误 1:线性筛中没有 break 导致复杂度退化

错误写法:去掉 if (i % prime[j] == 0) break;

for (int i = 2; i <= n; i++) {
    if (!vis[i]) prime[++cnt] = i;
    for (int j = 1; j <= cnt && i * prime[j] <= n; j++)
        vis[i * prime[j]] = 1;               // 没有 break!
}

正确写法:必须在 i % prime[j] == 0break

for (int i = 2; i <= n; i++) {
    if (!vis[i]) prime[++cnt] = i;
    for (int j = 1; j <= cnt && i * prime[j] <= n; j++) {
        vis[i * prime[j]] = 1;
        if (i % prime[j] == 0) break;        // 保证每个合数只被最小质因子筛
    }
}

没有 break 会导致每个合数被所有质因子标记,复杂度退化为 \(O(n \log \log n)\) 甚至更差。

错误 2:GCD/LCM 公式用混

常见错误:把 \(\gcd\)\(\operatorname{lcm}\) 的关系记反了。

✅ 正确公式:\(a \times b = \gcd(a,b) \times \operatorname{lcm}(a,b)\)

❌ 错误写法:lcm = a * b / gcd(a, b) 可能溢出!应先除后乘:lcm = a / gcd(a, b) * b

错误 3:位运算符号位陷阱

int x = -1;
x >>= 1;   // 算术右移:高位补符号位 1,结果仍是 -1
unsigned int x = -1;
x >>= 1;   // 逻辑右移:高位补 0,结果是 0x7FFFFFFF

做位运算时优先使用 unsigned 类型,避免符号位带来的意外。

错误 4:组合数递推溢出

// ❌ 直接存真实值,n=100 时 C(100,50) ≈ 1e29 溢出 64 位
c[i][j] = c[i-1][j-1] + c[i-1][j];

✅ 按需取模:如果只需要判 \(k\) 的倍数,全程模 \(k\);如果需要真实值,使用高精度或 Python。

错误 5:区间筛忘记处理 \(L=1\)

\(L=1\) 时,1 既不是素数也不是合数。如果不处理会导致误统计。

if (L == 1) L = 2;   // 或 L++;

错误 6:枚举约数时忘记平方数去重

for (int i = 1; i * i <= n; i++)
    if (n % i == 0) {
        g.push_back(i);
        g.push_back(n / i);   // ❌ 当 i*i==n 时会重复加入!
    }

✅ 加上判断:if (i * i != n) g.push_back(n / i);


题单完整题解(24 题)

位运算与进制转换(4 题)

P1143 进制转换 ★★ — 进制互转基础模板

题意\(n\) 进制 → \(m\) 进制。AC 代码见例题 1。

复杂度:\(O(\log_m \text{ans} + |s|)\)

P1469 找筷子 ★★ — 异或的经典应用

题意\(n\) 个数中恰好一个出现了奇数次,找出来。AC 代码见例题 2。

复杂度:\(O(n)\),仅需 \(O(1)\) 额外空间。

P1100 高低位交换 ★★ — 位移与位掩码

题意:32 位无符号整数高低 16 位交换。AC 代码见例题 3。

复杂度:\(O(1)\)

P1017 [NOIP 2000 提高组] 进制转换 ★★★ — 负进制

题意:将十进制数转为 \(-R\) 进制(\(2 \le R \le 16\))。

核心技巧:负进制的取模运算。当余数为负数时,商 +1、余数 +R(使余数非负)。

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int n, r;
    cin >> n >> r;
    cout << n << "=";
    string s;
    while (n) {
        int t = n % r;
        n /= r;
        if (t < 0) t -= r, n += 1;   // 负进制关键:使余数非负
        s += t < 10 ? t + '0' : t - 10 + 'A';
    }
    reverse(s.begin(), s.end());
    cout << s << "(base" << r << ")" << endl;
    return 0;
}

复杂度:\(O(\log_{R} n)\)。关键理解:负进制下 % 的结果必须为非负数。


计数原理与排列组合(6 题)

P1866 编号 ★★ — 乘法原理

题意\(n\) 个变量取值范围 \([1, a_i]\) 且互不相同,求方案数。AC 代码见例题 4。

复杂度:\(O(n \log n)\)

P1246 [ECNA 1995] 编码 ★★★ — 排列序 / 字典序编号

题意:给严格递增字母串按字典序编号。AC 代码见例题 5。

复杂度:\(O(26^6)\) DFS 打表或 \(O(|s| \times 26)\) 组合数计算。

P2822 [NOIP 2016 提高组] 组合数问题 ★★★★ — 杨辉三角 + 二维前缀和

题意:多次询问 \(C_i^j\) 中有多少个是 \(k\) 的倍数。AC 代码见例题 6。

复杂度:\(O(N^2)\) 预处理 + \(O(1)\) 查询

P2789 直线交点数 ★★★ — 枚举与递推计数

题意\(n\) 条直线(可平行),求可能的不同交点数(方案数,非交点本身)。

核心技巧:设 \(f[i]\) 表示无平行限制时的交点集合,枚举平行线数量 \(k\),剩余 \(i-k\) 条与这 \(k\) 条产生 \(k \times (i-k)\) 个交点。

#include<bits/stdc++.h>
using namespace std;
int n;
bool f[10000];
int main() {
    cin >> n;
    f[0] = 1;
    for (int i = 1; i <= n; i++)
        for (int j = n * (n-1) / 2; j >= 0; j--)
            if (f[j])
                for (int k = 1; k <= i; k++)
                    f[j + k * (i - k)] = 1;
    int ans = 0;
    for (int i = 0; i <= n * (n-1) / 2; i++)
        if (f[i]) ans++;
    cout << ans;
}

复杂度:\(O(n^3)\)

P3913 车的攻击 ★★★ — 容斥原理

题意\(n \times n\) 棋盘上有 \(k\) 个车,求被攻击到的格子数。

核心技巧:去重。被攻击的行数 + 列数 - 行列交叉处重复计算的格子。

#include<bits/stdc++.h>
using namespace std;
int main() {
    long long n, k, r = 0, c = 0;
    cin >> n >> k;
    map<int,int> R, C;
    for (int i = 0; i < k; i++) {
        int x, y; cin >> x >> y;
        R[x]++; C[y]++;
    }
    r = R.size(); c = C.size();
    cout << r * n + c * n - r * c;
}

复杂度:\(O(k \log k)\)

P2638 安全系统 ★★★★ — 组合数 / 插板法

题意\(n\) 个存储区存放 \(a\) 个 0 和 \(b\) 个 1,每个区容量无限,求方案数。

核心技巧\(a\) 个相同球放入 \(n\) 个不同盒子 = \(C(a+n-1, n-1)\)(插板法)。0 和 1 独立,方案数相乘。使用 unsigned long long 防溢出。

复杂度:\(O(n+a+b)\) 递推组合数


整除理论 — 质数与素数筛法(2 题)

P3383 【模板】线性筛素数 ★★★ — 线性筛模板

题意:多次询问第 \(k\) 小素数。AC 代码见例题 7。

复杂度:\(O(n)\) 预处理 + \(O(1)\) 查询

P1835 素数密度 ★★★★ — 区间筛

题意:求 \([L, R]\) 区间素数个数(\(R-L \le 10^6\))。AC 代码见例题 8。

复杂度:\(O((R-L) \log \log R)\)


整除理论 — 约数、倍数与因数(4 题)

P2926 [USACO08DEC] Patting Heads S ★★★ — 倍数枚举法

题意:对每个数,统计能整除它的其他数个数。AC 代码见例题 9。

复杂度:\(O(N \log N)\)

P1403 [AHOI2005] 约数研究 ★★ — 除数函数公式

题意:求 \(\sum_{i=1}^n d(i)\)。AC 代码见例题 10。

复杂度:\(O(n)\)\(O(\sqrt{n})\) 整除分块

P1414 又是毕业季II ★★★ — 寻找最大公约数

题意:从 \(n\) 个数中选 \(k\) 个(\(k=1\dots n\)),求每种 \(k\) 下的最大公约数。

核心技巧:枚举每个可能的公约数 \(d\),统计 \(d\) 的倍数出现次数 \(cnt[d]\)\(cnt[d] \ge k\)\(d\) 可作为选 \(k\) 个的公约数。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, cnt[N], ans[N];
int main() {
    cin >> n;
    int mx = 0;
    for (int i = 1; i <= n; i++) {
        int x; cin >> x;
        cnt[x]++; mx = max(mx, x);
    }
    for (int i = 1; i <= mx; i++)
        for (int j = i * 2; j <= mx; j += i)
            cnt[i] += cnt[j];               // cnt[i] = i 的倍数个数
    for (int i = 1; i <= mx; i++)
        ans[cnt[i]] = max(ans[cnt[i]], i);  // 选 cnt[i] 个的最大公约数
    for (int i = n; i >= 1; i--)
        ans[i] = max(ans[i], ans[i+1]);     // 向下传递:选更少个时 gcd 不会更小
    for (int i = 1; i <= n; i++)
        cout << ans[i] << endl;
}

复杂度:\(O(N \log N)\)\(N\) 为最大值域。

P1593 因子和 ★★★★ — 约数和 + 快速幂

题意:求 \(a^b\) 的所有约数之和 \(\bmod 9901\)

核心技巧:对 \(a\) 质因数分解:\(a = \prod p_i^{e_i}\),则 \(a^b = \prod p_i^{e_i \times b}\)。约数和公式:

\[\sigma(a^b) = \prod \frac{p_i^{e_i \times b + 1} - 1}{p_i - 1} \]

需要快速幂 + 逆元(或分治求和)处理除法取模。

复杂度:\(O(\sqrt{a} \log b)\)


整除理论 — GCD/LCM(6 题)

P1029 [NOIP 2001 普及组] 最大公约数和最小公倍数问题 ★★★ — GCD/LCM 核心公式

题意:已知 \(\gcd(P,Q)=x_0, \operatorname{lcm}(P,Q)=y_0\),求 \((P,Q)\) 有序对数量。AC 代码见例题 11。

复杂度:\(O(d(y_0)^2 \log V)\)

P1072 [NOIP 2009 提高组] Hankson 的趣味题 ★★★★★ — GCD/LCM 综合

题意\(\gcd(x, a_0) = a_1\)\(\operatorname{lcm}(x, b_0) = b_1\)。AC 代码见例题 12。

复杂度:\(O(n \times d(b_1) \times \log V)\)

P4057 [Code+#1] 晨跑 ★★ — 三数 LCM

题意:求三个数的 LCM。

核心技巧\(\operatorname{lcm}(a,b,c) = \operatorname{lcm}(\operatorname{lcm}(a,b), c)\)。注意先除后乘防溢出。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    ll a, b, c;
    cin >> a >> b >> c;
    ll t = a / __gcd(a, b) * b;
    cout << t / __gcd(t, c) * c;
}

复杂度:\(O(\log \max(a,b,c))\)

P2660 zzc 种田 ★★★ — GCD 在几何上的应用

题意\(x \times y\) 的矩形,每次选一个最大的正方形切掉,求总消耗。

核心技巧:每次切掉的正方形边长 = \(\min(x, y)\)。递归/循环交替减,本质是更相减损术。答案为 \(4 \times (x + y - \gcd(x, y))\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll ans, x, y;
void dfs(ll a, ll b) {
    if (b == 0 || a == 0) return;
    ans += 4 * b * (a / b);             // 切 a/b 个边长为 b 的正方形
    dfs(b, a % b);
}
int main() {
    cin >> x >> y;
    dfs(x, y);
    cout << ans;
}

复杂度:\(O(\log \min(x,y))\),本质是欧几里得算法的过程。

P1572 计算分数 ★★ — 分数四则运算 + GCD 约分

题意:模拟分数加减,输出最简结果。

核心技巧:逐项通分,每次运算后用 GCD 约分。分母取 LCM,分子按比例调整后加减。

复杂度:\(O(n \log V)\)

P2651 添加括号III ★★★ — 分数与 GCD 性质

题意:判断表达式 \(a_1/a_2/a_3/\dots/a_n\) 能否通过加括号变成整数。

核心技巧\(a_1\) 一定是分子,\(a_2\) 一定是分母中的一部分(在最外层)。其余数可通过加括号调整到分子或分母。实际上只需判断:把 \(a_2\) 中和 \(a_1\) 互质的部分不断约掉后,\(a_2\) 能否变为 1。

复杂度:\(O(n \log V)\)


整除理论 — 算术基本定理(2 题)

P1069 [NOIP 2009 普及组] 细胞分裂 ★★★★ — 质因数分解 + 指数比较

题意:求最小 \(t\) 使 \(a_i^t\)\(m_1^{m_2}\) 的倍数。AC 代码见例题 13。

复杂度:\(O(\sqrt{m_1} + n \times k \times \log a_i)\)

P3601 签到题 ★★★★★ — 综合:质因数分解 + 欧拉函数

题意:求 \(\sum_{i=l}^{r} (i - \varphi(i)) \bmod 66662333\),其中 \(\varphi\) 为欧拉函数。\(r-l \le 10^6\)

核心技巧:区间筛 + 欧拉函数。每个数 \(i\) 贡献 \(i - \varphi(i)\)。用区间筛法在 \(O((r-l) \log r)\) 内求出每个数的 \(\varphi\) 值。

\(\varphi(n) = n \times \prod_{p \mid n} (1 - \frac{1}{p})\)。区间筛时对每个素数 \(p\),将 \([l, r]\)\(p\) 的倍数乘上 \((p-1)/p\)

复杂度:\(O((r-l) \log r)\)


课后巩固

题目 说明
P1017 [NOIP 2000 提高组] 进制转换 负进制转换——理解取模运算在负数下的行为
P2638 安全系统 插板法应用——相同球放入不同盒子
P1414 又是毕业季II 倍数枚举法——从另一个角度统计公约数
P1593 因子和 约数和公式 + 快速幂 + 等比数列求和
P3601 签到题 区间筛 + 欧拉函数——本章节综合大练习

完成本课后,你将掌握: 进制转换的标准两步法(按权展开 + 除基取余)、异或的自反性与位移操作、乘法原理的阶梯式应用、杨辉三角递推与二维前缀和优化、埃氏筛与线性筛的区别和实现、倍数枚举法的调和级数复杂度、GCD/LCM 核心公式 \(a \times b = \gcd \times \operatorname{lcm}\)、质因数分解与指数比较法。这些是 NOIP 普及组/提高组数论题的必备基础。

posted @ 2026-06-28 13:30  surprise_ying  阅读(20)  评论(0)    收藏  举报