第 31~39 节是九节:DP 四类(线性、背包、区间/树形、状压/数位)、一场模拟赛,数论四块(质数、gcd 与同余、组合计数、期望)。它们训练的是同一个动作——先把问题压成一张表或一个式子,再动键盘。DP 的表是 dp 数组,数论的式子要取模;错法也一样:不是编译错误,而是答案差 1、差一个模数、差一次初始化。
先看九节的判据表。复习不是重读,是遮住右边一列,自己先说一遍,说不出来才回去补那一节。
| 节 | 内容 | 一句话判据(读完题先问自己) |
|---|---|---|
| 31 | 线性 DP 四步法 | 答案能写成“前 i 个里选、末尾状态是 j”吗?能就设 f[i](或 f[i][j]),从“最后一步”倒推 |
| 32 | 0/1 与完全背包 | 每种物品只能选一次 → 容量倒序;能选无限次 → 容量正序。记反这条,样例常能过、大数据全错 |
| 33 | 区间 DP 与树形 DP | 答案由“最后一次把区间切成两半”决定 → 枚举断点 k;环形就复制一倍,答案取遍所有长度 n 的区间 |
| 34 | 状压 DP 与数位 DP | 状态要记“哪些已经选过” → 用二进制整数当数组下标;问“1~n 里某数字出现几次” → 按位拆 |
| 35 | 模拟赛一(120 分钟 4 题) | 先暴力拿分再想正解;最后 15 分钟只查数组大小、long long、多测清空 |
| 36 | 质数与整除 | 判一堆素数且上界 ≤ 10⁷ → 埃氏筛;单个数分解 → 试除到 √n,剩下的 > 1 必是素数 |
| 37 | gcd、同余与快速幂 | P·Q = gcd(P,Q)·lcm(P,Q);ab mod p 按二进制拆,每乘一次就取模 |
| 38 | 组合计数与容斥 | C(n,m) 用杨辉三角或阶乘逆元;“至少一个”一律转成“总数 − 一个都不满足” |
| 39 | 概率与期望 | E(X+Y) = E(X) + E(Y) 恒成立,不需要独立;但每走一步要乘概率,漏乘就是 WA |
这一节 95 分钟:前 25 分钟跟着一道题走完 DP 的完整思考链,后 70 分钟做 5 道题,每题 14 分钟。顺序建议就按表里的顺序:背包 → 区间 DP → 筛法 → gcd → 组合计数,前面的题给后面的题热身。
n 位同学站成一排,第 i 位身高 Ti。音乐老师要请其中的 n − k 位同学出列,使剩下的 k 位同学排成合唱队形:存在一个人,他左边身高严格递增、右边身高严格递减。
输入格式:第一行一个整数 n(2 ≤ n ≤ 100);第二行 n 个整数 Ti(130 ≤ Ti ≤ 230)。
输出格式:一行一个整数,表示最少需要几位同学出列。
题目问“出列最少”,等价于“留下最多”。设最多留下 k 人,答案就是 n − k,于是整题变成求最长的合唱队形。
合唱队形有一个唯一的最高点 i:i 左边严格递增,i 右边严格递减。枚举这个 i,左边的最长上升子序列长度 f[i] 与右边的最长下降子序列长度 g[i] 分别算出来,拼起来是 f[i] + g[i] − 1(i 被算了两次)。
于是问题拆成两个同一个模子的小问题:以 i 结尾的最长上升子序列。f[i] = 1 + max{ f[j] : j < i 且 a[j] < a[i] },没有合法 j 时 f[i] = 1。
右边不用另写一套:把数组从右往左扫,判据仍然是 a[j] < a[i],得到的 g[i] 就是“以 i 开头的最长下降子序列”。方向变了,不等式没变——这是本题最省脑子的地方。
for (int i = 1; i <= n; ++i) { // f:从左往右扫
f[i] = 1; // 只留自己也算一种队形
for (int j = 1; j < i; ++j)
if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
}
for (int i = n; i >= 1; --i) { // g:从右往左扫,判据不变
g[i] = 1;
for (int j = n; j > i; --j)
if (a[j] < a[i]) g[i] = max(g[i], g[j] + 1);
}#include <bits/stdc++.h>
using namespace std;
int n, a[105];
int f[105], g[105]; // f[i]:以 i 结尾的最长上升子序列长度
// g[i]:以 i 开头的最长下降子序列长度
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) {
f[i] = 1; // 自己单独站,长度是 1
for (int j = 1; j < i; ++j)
if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1); // 把 a[i] 接在 a[j] 后面
}
for (int i = n; i >= 1; --i) { // 倒着扫:右边的人先算好
g[i] = 1;
for (int j = n; j > i; --j)
if (a[j] < a[i]) g[i] = max(g[i], g[j] + 1);
}
int best = 0;
for (int i = 1; i <= n; ++i)
best = max(best, f[i] + g[i] - 1); // i 在 f、g 里各算了一次,减掉一次
cout << n - best << "\n"; // 题目问的是出列人数
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
#include <bits/stdc++.h> | 一个头文件包含全部标准库。竞赛里省的不是这几个字符,是忘记 include 的那几分钟。 |
int f[105], g[105]; | n ≤ 100,开 105。开成 100 而循环写到 i = 100 就越界——习惯上多开 5 个格子。 |
f[i] = 1; | 初始化不能省。LIS 的答案至少是 1,漏了这一句 f[i] 从 0 起,best 偏小、输出偏大。 |
if (a[j] < a[i]) | 严格递增用 <。本题写了“严格”,用 <= 会把 186 186 也算成递增,答案变小。 |
f[i] = max(f[i], f[j] + 1); | 枚举“i 的前一个是谁”,取最长的那个接上。f[i] 只表示以 i 结尾,中途不能当答案。 |
for (int i = n; i >= 1; --i) | 倒序扫、j 也从 n 往 i 走,算的是“i 往右能下降多长”。依赖的 g[j] 已经算好。 |
best = max(best, f[i] + g[i] - 1); | i 是最高点,左右拼起来。漏减 1 就把最高点算了两遍,答案偏大 1。 |
cout << n - best << "\n"; | 做一次“留下最多 → 出列最少”的转换。这一步写在纸上,别只在脑子里转。 |
上面这道题没有新语法,但接下来的 5 道题会反复用到下面两个东西。先看清它们的格式,再往下做。
| 组成 | 含义 / 格式要求 |
|---|---|
a, b | 两个整数。类型必须一致:__gcd(3, 4LL) 在老版本 g++ 上编译失败。 |
返回值 | 最大公约数,正整数。gcd(a, 0) = a,所以“一边是 0”这种边界不用特判。 |
复杂度 | 欧几里得算法(辗转相除),O(log min(a, b))。10¹⁸ 的数也只要几十步。 |
lcm 怎么算 | lcm(a, b) = a / gcd(a, b) * b。先除后乘,先乘就溢出。 |
需要 include | bits/stdc++.h 已经包含,using namespace std 后可直接用。 |
int a = 12, b = 18;
cout << __gcd(a, b) << "\n"; // 6
cout << a / __gcd(a, b) * b << "\n"; // 36:先除后乘,不溢出
long long g = __gcd(6LL, 15LL); // 3,参数同为 long long不要写 a * b / gcd(a, b) 求 lcm:a = b = 10⁹ 时 a*b 已经溢出,得到的是垃圾值,而且不会报错。
gcd 为 1 是两个数互质的充要条件,数论题里这句判据出现频率极高。
| 组成 | 含义 / 格式要求 |
|---|---|
第 1 个参数 | 数组名(首地址)。二维数组直接写 dp,不要写 dp[0]。 |
第 2 个参数 | 只有“四个字节都相同”的值才安全:0、-1、0x3f(INF 常用)。它按字节填充,写 1 会得到 0x01010101 = 16843009。 |
第 3 个参数 | sizeof dp。写成 sizeof(dp) 等价;写成 n 是经典错误——只清了前 n 个字节。 |
为什么用 -1 | int 的 -1 正好是“没算过”的标记:合法答案可以是 0,用 0 当初值就分不清“算过且为 0”和“没算”。 |
要填别的值 | 用 fill(f, f + n, 值),或者写循环。fill 不要求值是字节可重复的。 |
int dp[15][15];
memset(dp, -1, sizeof dp); // 全部 -1,当“没算过”
long long f[1005];
fill(f, f + 1005, 1LL << 60); // 求最小值时的 INF
if (dp[x][y] != -1) return dp[x][y]; // 记忆化搜索的标准判据多测题每个 case 都要重新 memset,漏一次就把上一组的答案带进这一组。
memset 只对 POD 数组安全;vector 用 assign 或 fill。
回顾章的 DP 不该再背模板,而应该能用三句话把状态问出来。问完这三句,转移方程几乎是照着写:
| 类型 | 状态写法 | 转移骨架 | 本节对应的题 |
|---|---|---|---|
| 线性 DP | f[i]:前 i 个 / 以 i 结尾 | f[i] = max(f[j]) + 1(j < i) | 例题 P1091 |
| 背包 | f[j]:容量 j 的最大价值 | 0/1 倒序 j;完全背包正序 j | 挑战 1 P1048 |
| 区间 DP | f[i][j]:区间 [i,j] 的答案 | 枚举断点 k,f[i][k] + f[k+1][j] + 代价 | 挑战 2 P1063 |
| 状压 / 数位 | f[S] 或 f[pos][cnt] | 枚举这一位填什么,把新状态累加进去 | 下方 30 行的数位统计 |
表里第四行本节没有配题,但它和第 38 节的组合计数共用一个动作:按位统计贡献。看到“1 到 n 中数字 d 出现了多少次”,先想“固定某一位是 d,这一位贡献几次”,再想“高位有哪些限制”。限制想得清,就是 O(log n) 的数学;想不清,就退回数位 DP 的记忆化框架,别在考场上硬推。下一块把这条路走完。
数位题的框架只有一句话:把数字一位一位填,记录“这一位能不能随便填”。如果限制只跟某一位有关,不必写记忆化搜索,直接数每一位的贡献更快、更短。
以“1 到 x 中数字 d 出现了几次”为例。固定一个位权 p(1、10、100…),把 x 拆成高位 high、当前位 cur、低位 low。这一位填 d 的取法分三类:
| 当前位与 d 的关系 | 这一位的贡献 | 理由 |
|---|---|---|
| cur > d | (high + 1) × p | 高位取 0~high 都能填 d,低位 p 种随便填 |
| cur == d | high × p + low + 1 | 高位取 0~high−1 时低位随便填;高位取 high 时低位只能 0~low |
| cur < d | high × p | 高位最多取到 high−1,低位随便填 |
d = 0 要单独处理:最高位不可能是 0,所以 cur == 0 时贡献是 (high − 1) × p + low + 1,其余情况是 high × p;high == 0 说明已经数到最高位,直接退出。这一条漏了,前导零会被当成真的 0 计进去,答案偏大。
#include <bits/stdc++.h>
using namespace std;
long long countDigit(long long x, int d) { // 统计 1..x 中数字 d 出现的次数
long long res = 0;
for (long long p = 1; p <= x; p *= 10) { // p 是当前处理的位权
long long high = x / (p * 10);
int cur = (int)(x / p % 10);
long long low = x % p;
if (d == 0) { // 0 不能出现在最高位,单独算
if (high == 0) break;
if (cur == 0) res += (high - 1) * p + low + 1;
else res += high * p;
} else {
if (cur > d) res += (high + 1) * p;
else if (cur == d) res += high * p + low + 1;
else res += high * p;
}
}
return res;
}
int main() {
long long a, b;
cin >> a >> b; // 输出 [a,b] 中 0~9 各出现几次
for (int d = 0; d <= 9; ++d)
cout << countDigit(b, d) - countDigit(a - 1, d) << (d == 9 ? "\n" : " ");
return 0;
}输入 1 99,输出 9 20 20 20 20 20 20 20 20 20 20:0 出现 9 次(10、20、…、90),1~9 各 20 次(个位 10 次 + 十位 10 次)。这份输出就是自测数据;数位 DP 版和这一版都过,才算真懂。
数论那四块的知识点都是单行公式,考前扫一遍比读十页推导有用:
| 考点 | 必背式子 | 用错会怎样 |
|---|---|---|
| gcd / lcm | gcd(a,b) · lcm(a,b) = a · b | 直接写 a*b/g 求 lcm,10⁹ 就溢出成负数 |
| 快速幂 | ab = (ab/2)² · ab mod 2,每步 % p | 循环 b 次,b = 10⁹ 时 TLE |
| 组合数 | C(n,m) = C(n−1,m) + C(n−1,m−1) | 用阶乘硬算,n = 2000 就溢出 |
| 容斥 | |A∪B∪C| = Σ|A| − Σ|A∩B| + |A∩B∩C| | 符号不交替,答案偏大 |
| 期望 | E(X+Y) = E(X) + E(Y) | 以为必须独立才能相加,白推一小时 |
WA,答案恰好差 1:初始化漏了。LIS 的 f[i] 忘了赋 1、背包忘了 f[0] = 0、区间 DP 忘了 f[i][i] = 0。
WA,小数据对、大数据偏大:0/1 背包把容量写成正序。正序时同一件物品被反复选,样例常常看不出来。
RE(段错误):区间 DP 断环复制后数组只开了 n,实际要访问到 2n;或第二维写成了 n 而不是 2n。
TLE:区间 DP 写成了 O(n⁴)(枚举 i、j、k 之外又套一层扫描),或者把 10⁷ 的筛法写成对每个数试除到 √n。
WA,只有大样例错:乘法没开 long long。a、b 都是 10⁹ 时 a*b 已经溢出,取模后是负数。写法固定成 1LL * a * b % p。
WA,答案差一个:区间端点。求 [a,b] 用 solve(b) − solve(a−1),写成 solve(b) − solve(a) 会少算 a 本身。
WA,n = 1 或全是 0 时错:筛法边界。i 从 2 开始、comp[1] 当非素数;分解时试除到 i*i ≤ n,结束后若 n > 1,它本身就是一个素数因子。
RE / MLE:筛到 10⁸ 还开 int 数组(400 MB)。布尔筛用 bool,10⁸ 只要 100 MB;能筛到 10⁷ 就别筛到 10⁸。
关掉这一页,把 P1091 的代码手敲一遍提交。卡住的那一行,就是你没真懂的地方。
然后把 a[j] < a[i] 改成 a[j] <= a[i],用样例再交一次。答案从 4 变成了几?想清楚为什么是变小而不是变大。
最后把 f、g 两遍扫描塞进同一个 for:第 t 次迭代用 i = t 算 f,再用 i = n − t + 1 算 g。为什么这样两个方向都能满足?
数论题的版本是:先枚举(O(n) 或 O(√n))→ 找等价式(gcd·lcm = P·Q、C(n,m) 的递推)→ 算复杂度(能不能过 10⁷)→ 查 long long 与取模。四步都在纸上完成,键盘只负责翻译。
限时 100 分钟。每题先自己写 20~25 分钟;写不出来先看提示,再给自己 10 分钟;还不行才点开思路。每题都独立限时,不要一题耗死。
山洞里有 M 株草药,采第 i 株需要 ti 的时间,能得到 vi 的价值。总时间只有 T,每株草药最多采一次,求能采到的最大总价值。
输入格式:第一行两个整数 T、M(1 ≤ T ≤ 1000,1 ≤ M ≤ 100);接下来 M 行,每行两个整数 ti、vi(1 ≤ ti ≤ T,1 ≤ vi ≤ 100)。
输出格式:一行一个整数,表示最大总价值。
每株草药只有“采”和“不采”两种选择,而且每株最多一次——这是哪种背包?
把“时间”当容量,“价值”当物品价值,dp 数组只要一维就够。
一维数组的关键是循环方向:想清楚更新 f[j] 时,f[j − t] 是本轮的还是上一轮的。
设 f[j] = 用不超过 j 的时间能拿到的最大价值,初值全 0(什么都不采,价值 0)。
对每株草药 i:不采它,f[j] 不变;采它,f[j] = f[j − t[i]] + v[i]。两者取大。
容量必须倒序枚举。正序时 f[j − t[i]] 已经被第 i 株更新过,等于允许同一株采两次,答案偏大。
答案就是 f[T],复杂度 O(M·T) = 10⁵,时间与空间都宽松。
#include <bits/stdc++.h>
using namespace std;
int T, M;
int t[105], v[105];
int f[1005]; // f[j]:时间不超过 j 时的最大价值
int main() {
cin >> T >> M;
for (int i = 1; i <= M; ++i) cin >> t[i] >> v[i];
for (int i = 1; i <= M; ++i)
for (int j = T; j >= t[i]; --j) // 倒序:保证每株草药只用一次
f[j] = max(f[j], f[j - t[i]] + v[i]);
cout << f[T] << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
int f[1005]; | T ≤ 1000,开 1005。一维数组是“滚动掉物品维”的结果:第 i 轮覆盖第 i−1 轮。 |
for (int j = T; j >= t[i]; --j) | 倒序是 0/1 背包的全部秘密。j 从大到小,f[j − t[i]] 还没被本轮改过。 |
f[j] = max(f[j], f[j - t[i]] + v[i]); | 不采(f[j])与采(f[j−t[i]] + v[i])取大。 |
cout << f[T]; | f[T] 是“时间不超过 T”的最优值,不必再扫一遍 max。 |
项链由 n 颗珠子串成一圈。第 i 颗珠子有头标记 ai、尾标记 ai+1(第 n 颗的尾标记是 a1)。相邻两颗可以合并:若前一颗是 (a, b)、后一颗是 (b, c),合并后得到 (a, c),同时释放 a × b × c 点能量。
求把整圈合并成一颗珠子,最多能释放多少能量。
输入格式:第一行一个整数 n(4 ≤ n ≤ 100);第二行 n 个正整数 ai(1 ≤ ai ≤ 10⁴)。
输出格式:一行一个整数,表示最大总能量。
合并只能在相邻的两颗之间发生,而且最后一次合并一定把“环上的某一段”切成两半。
把环拉直的做法是把数组复制一份接在后面,然后只看长度为 n 的区间。
区间 DP 的枚举顺序是:长度从小到大 → 区间起点 → 断点。想清楚为什么长度必须在最外层。
断环成链:把 a 复制一份,a[i + n] = a[i]。于是任何长度为 n 的连续区间都对应环上的一种合并顺序。
设 f[i][j] 表示把第 i..j 颗珠子合并成一颗的最大能量。最后一次合并把区间分成 [i,k] 与 [k+1,j],合并后的左块是 (a[i], a[k+1]),右块是 (a[k+1], a[j+1]),释放 a[i] × a[k+1] × a[j+1]。
转移:枚举断点 k,f[i][j] = max( f[i][k] + f[k+1][j] + a[i]·a[k+1]·a[j+1] )。
答案 = max f[i][i+n−1](i = 1…n),因为环有 n 种断法。复杂度 O(n³) = 10⁶。
#include <bits/stdc++.h>
using namespace std;
int n, a[205];
long long f[205][205]; // f[i][j]:把第 i..j 颗珠子合成一颗的最大能量
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) { cin >> a[i]; a[i + n] = a[i]; } // 断环成链
for (int len = 2; len <= n; ++len) // 长度从小到大,保证小区间先算
for (int i = 1; i + len - 1 < 2 * n; ++i) { // 右端点 j <= 2n-1:能量要用 a[j+1],j = 2n 就越界
int j = i + len - 1;
for (int k = i; k < j; ++k) // 最后一次合并的断点
f[i][j] = max(f[i][j],
f[i][k] + f[k + 1][j] + 1LL * a[i] * a[k + 1] * a[j + 1]);
}
long long ans = 0;
for (int i = 1; i <= n; ++i) ans = max(ans, f[i][i + n - 1]); // n 种断法
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
a[i + n] = a[i]; | 环形问题第一步:复制一份接在后面,任何长度 n 的区间都对应环上的一种合并顺序。 |
long long f[205][205]; | 一次合并最多释放 10¹²(三个 10⁴ 相乘),n−1 次累加可到 10¹⁴,int 装不下。 |
for (int len = 2; len <= n; ++len) | 按长度从小到大:算长度 len 时,所有更短的子区间已经算好。 |
for (int k = i; k < j; ++k) | 枚举最后一次合并的分界:左边合成 (a[i], a[k+1]),右边合成 (a[k+1], a[j+1])。 |
1LL * a[i] * a[k + 1] * a[j + 1] | 合并释放的能量。三个 10⁴ 相乘是 10¹²,int 直接溢出。 |
ans = max(ans, f[i][i + n - 1]); | 长度为 n 的区间有 n 个起点,正好对应环的 n 种断法,取最大。 |
i + len - 1 <= 2 * n,右端点取到 2n 时 a[j+1] 读到空白值,答案偏小且不报错。回文质数是指既是质数、又是回文数的整数(回文数指正着读反着读一样)。给定 a、b,按从小到大输出 [a, b] 内所有回文质数,每行一个。
输入格式:一行两个整数 a、b(5 ≤ a ≤ b ≤ 10⁸)。
输出格式:每行一个回文质数,按升序输出。
要输出区间内所有满足条件的数,意味着你得先有一个“谁是质数”的表,而不是对每个数单独判。
回文数有一个性质:偶数位的回文数一定能被 11 整除。想一想这对 10⁸ 的范围意味着什么。
标记倍数时从 i × i 开始——更小的倍数已经被更小的质数标记过了。
先筛出 b 以内的所有质数(埃氏筛),再对每个质数判回文。
关键剪枝:偶数位的回文数一定能被 11 整除,所以 8 位数的回文数全都不是质数;b ≤ 10⁸ 时 9 位数只有 100000000,它不是回文。于是 b 可以直接截到 9999999,1 亿的筛法变成 1 千万,时间空间都省一个量级。
筛法:comp[i] 表示 i 被标记为合数;i 未标记就是素数,把 i²、i²+i、… 全部标记。内层循环的起点必须是 i²,否则 10⁷ 的筛法会退化成 O(n log n) 且常数很大。
判回文用整数翻转:把 x 的十进制位翻过来,和原数比较,空间 O(1),比转字符串快。
#include <bits/stdc++.h>
using namespace std;
const int N = 10000005;
bool comp[N]; // comp[i] = true 表示 i 是合数
bool pal(int x) { // 判回文:翻过来看是否相等
int t = x, r = 0;
while (t) { r = r * 10 + t % 10; t /= 10; }
return r == x;
}
int main() {
int a, b;
cin >> a >> b;
b = min(b, 9999999); // 8 位回文数必被 11 整除,截掉它们不丢答案
for (int i = 2; i <= b; ++i) {
if (!comp[i]) { // i 没被标记 → i 是素数
if (i >= a && pal(i)) cout << i << "\n";
for (long long j = 1LL * i * i; j <= b; j += i)
comp[j] = true; // 从 i*i 开始标记,更小的倍数早被筛过
}
}
return 0;
}| 片段 | 作用 |
|---|---|
b = min(b, 9999999); | 偶数位回文数必被 11 整除,8 位数的回文数都不是质数,所以截到 9999999 不丢答案。 |
if (!comp[i]) | 埃氏筛的核心:从小到大,没被标记过的就是素数。 |
for (long long j = 1LL * i * i; ...) | j 必须是 long long:i 接近 10⁷ 时 i*i 溢出 int,变成负数后死循环。 |
if (i >= a && pal(i)) | 先筛出素数再判回文,比“枚举回文再判素数”省事:素数表已经给全了。 |
r = r * 10 + t % 10; | 整数翻转。回文判据是 r == x,不需要字符串,也没有前导零问题。 |
已知两个正整数 P、Q 的最大公约数是 x0,最小公倍数是 y0。求满足条件的 (P, Q) 有多少对,其中 (P, Q) 与 (Q, P) 算两对。
输入格式:一行两个正整数 x0、y0。
输出格式:一行一个整数,表示满足条件的对数;不存在时输出 0。
先用一个等式把 P、Q 的关系压缩成两个数之间的关系,再想枚举什么。
P 和 Q 都是 x0 的倍数,把它们写成 x0·i 和 x0·j,代进等式看看。
枚举 n 的因子只需要到 √n,再检查这两个因子是否互质。
由 gcd(P,Q) · lcm(P,Q) = P · Q 得 P·Q = x0·y0。x0 不整除 y0 时无解。
令 P = x0·i、Q = x0·j,代入 P·Q = x0·y0 得 i·j = y0/x0 = n。
gcd(P,Q) = x0·gcd(i,j),要求等于 x0,所以 gcd(i,j) = 1。于是问题变成:枚举 n 的因子对 (i, j),数其中互质的有几对。
枚举到 √n 即可:每找到一个因子 i 就得到一对 (i, n/i),互质则答案加 2((i,j) 和 (j,i));i == n/i 时只加 1,避免重复。复杂度 O(√n)。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long x, y;
cin >> x >> y;
if (y % x) { cout << 0 << "\n"; return 0; } // x 不整除 y,无解
long long n = y / x, ans = 0;
for (long long i = 1; i * i <= n; ++i) {
if (n % i) continue; // i 不是 n 的因子
long long j = n / i;
if (__gcd(i, j) == 1) // i、j 互质,还原出的 P、Q 的 gcd 才恰好是 x
ans += (i == j) ? 1 : 2; // 有序对:(i,j) 与 (j,i) 各算一次
}
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
if (y % x) | P、Q 都是 x 的倍数,所以 x 必须整除 y;不整除时答案直接是 0。 |
n = y / x; | 把 x 从两个数里约掉:i、j 满足 i·j = n 且 gcd(i,j) = 1。枚举范围从 y 降到 √n。 |
for (i = 1; i * i <= n; ++i) | 枚举因子只到 √n,n 再大也只要几万次。i 用 long long 防 i*i 溢出。 |
__gcd(i, j) == 1 | 互质是“gcd(P,Q) 恰好为 x”的充要条件。不互质说明还有公因子,gcd 会更大。 |
ans += (i == j) ? 1 : 2; | (i,j) 与 (j,i) 是两个不同的有序解,各算一次;i == j 时只有一个解,只算一次。 |
组合数 C(i,j) 表示从 i 个不同物品中选出 j 个的方案数,规定 j > i 时 C(i,j) = 0。给定 k 和 t 组询问,每组给出 n、m,求有多少对 (i, j) 满足 0 ≤ i ≤ n、0 ≤ j ≤ min(i, m),且 C(i,j) 是 k 的倍数。
输入格式:第一行两个整数 t、k(1 ≤ t ≤ 10⁴,2 ≤ k ≤ 10⁹);接下来 t 行,每行两个整数 n、m(0 ≤ n, m ≤ 2000)。
输出格式:t 行,每行一个整数,表示该组询问的答案。
C(i,j) 可以用杨辉三角 O(n²) 全算出来,但真值有几百位,存不下——你只需要知道它是不是 k 的倍数。
每个询问问的是一块区域里有多少个满足条件的位置,位置总数是 n² 级别,逐组统计一定超时。
把每个位置标成 0/1 之后,询问就变成了区域求和。第 18 节学过一个工具,能让区域求和做到 O(1)。
杨辉三角只保留模 k 的余数:C[i][j] = (C[i−1][j−1] + C[i−1][j]) % k。余数为 0 就是 k 的倍数。
把“C[i][j] 是 k 的倍数”记成 0/1 矩阵 bad[i][j],并把 j > i 的格子强制置 0。这样矩形 [0..n] × [0..min(n,m)] 的和就恰好是答案——多出来的 j > i 格子贡献为 0。
对 bad 做二维前缀和:s[i][j] = bad[i][j] + s[i−1][j] + s[i][j−1] − s[i−1][j−1]。每次询问 O(1)。
预处理 O(N²) ≈ 4×10⁶,询问 O(1),总复杂度由预处理决定,完全够用。
#include <bits/stdc++.h>
using namespace std;
const int N = 2005;
int t, k;
int C[N][N]; // 杨辉三角模 k 的余数
int s[N][N]; // bad 的二维前缀和
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t >> k;
for (int i = 0; i < N; ++i) {
C[i][0] = C[i][i] = 1 % k; // 两条边界:C(i,0) = C(i,i) = 1
for (int j = 1; j < i; ++j)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % k; // 只要余数
}
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j) {
int bad = (j <= i && C[i][j] == 0) ? 1 : 0; // j > i 不是合法组合数
s[i][j] = bad;
if (i) s[i][j] += s[i - 1][j];
if (j) s[i][j] += s[i][j - 1];
if (i && j) s[i][j] -= s[i - 1][j - 1]; // 二维容斥
}
while (t--) {
int n, m;
cin >> n >> m;
if (m > n) m = n; // 题目要求 j <= min(i, m)
cout << s[n][m] << "\n";
}
return 0;
}| 片段 | 作用 |
|---|---|
C[i][0] = C[i][i] = 1 % k; | 杨辉三角的两条边界都是 1。写 1 % k 是防御写法:k 整除 1 时也不会错。 |
C[i][j] = (C[i-1][j-1] + C[i-1][j]) % k; | 只要余数:C(2000,1000) 有 600 位,存不下;判断倍数只看余数。 |
int bad = (j <= i && C[i][j] == 0); | j > i 的格子不是合法组合数,必须置 0,否则前缀和会把空白区算进去。 |
s[i][j] = bad + s[i-1][j] + s[i][j-1] - s[i-1][j-1]; | 二维前缀和:以 (i,j) 为右下角的矩形里有多少个 bad。 |
if (m > n) m = n; | 要求 j ≤ min(i, m)。m 超过 n 时直接截到 n,否则会查到无意义的区域。 |
cout << s[n][m]; | 查询 O(1)。预处理 4×10⁶ 次,1 万组询问也不会超时。 |