题单 · 习题解析

洛谷 P1372 又是毕业季I

★★ 答案就是 ⌊n/k⌋,但**证明要两半**:上界(k 个数都是 d 的倍数 ⇒ k·d ≤ n)+ 可达(取 d, 2d, …, kd,gcd 恰为 d)—— n ≤ 400、k ≤ n 的**全部 80200 组**逐组核对,0 组不一致;★ 题面那句「一个数的最大公约数即本身」是 k = 1 那个边界,而 n/1 = n **公式自动覆盖**;⚠⚠ 而我的草稿在这儿被实测打回:写了「O(n) 顶格必超时」,**最坏形状 n = k = 10⁹ 实测 0.24 秒 / 时限 1 秒 ⇒ 它其实能 AC,余量 4 倍** —— 循环体只有一次乘法一次比较 ⇒ **复杂度不等于耗时**;★ 所以正解 O(1) 赢的不是速度,是「说得清」;⚠ 真会挂人的只有一个:判 k·d ≤ n 时用 int(最大 10¹⁸)—— 四档「触发 ≡ 抓获」一个不差(0 / 0 / 299 / 144),⚠ 而**顺手小数据和题面 20% 档都结构上抓不到**;★ 正解里一次 gcd 都没调

原题:洛谷 P1372出自 第 40 章 GCD、LCM 与欧几里得算法 的题单题面本地存档:2026-09-01
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1372,日期见页头。两边不一致时信原站。

题目描述

老师想要挑出默契程度最大的 k 个人参与毕业晚会彩排。老师列出全班同学的号数 1, 2, …, n, 并且相信 k 个人的默契程度便是他们的最大公约数。请你帮帮忙。

PS:一个数的最大公约数即本身。

输入格式

两个空格分开的正整数 nk

输出格式

一个整数,为最大的默契值。

数据范围

对于 20% 的数据,k ≤ 2n ≤ 10³

对于另 30% 的数据,k ≤ 10n ≤ 100

对于 100% 的数据,k ≤ 10⁹n ≤ 10⁹n ≥ k ≥ 1

时限 1 秒,内存 125 MB。

输入输出样例

输入

4 2

输出

2

n = 4k = 2 ⇒ 挑 2 号和 4 号,gcd(2, 4) = 2。挑不出默契值更大的两个人了。

1★ 第一步不是写代码,是把「它到底在问什么」翻译一遍

★★ 两半的证明 —— 缺一半就只是「猜对了」

挑出的 k 个号数互不相同,它们的 gcd 记作 d

  • ① 上界:这 k 个数都是 d 的倍数、都 ≤ n1..n 里至少得有 kd 的倍数 ⇒ k · d ≤ nd ≤ n / k
  • ② 可达:取 d = ⌊n/k⌋,那么 d, 2d, …, kd≤ n、互不相同, 而且它们的 gcd 恰好是 dgcd(d, 2d) 已经是 d 了)。

答案 = ⌊n / k⌋,整数除法自带向下取整。

★ 题面那句「一个数的最大公约数即本身」是在交代 k = 1 这个边界 —— 而 n / 1 = n公式自动覆盖了它,不用特判

⚠ 这道题真正考的是「看出它是这件事」:正解里一次 gcd 都没调。

p1372.cpp正解:一行 —— 答案就是 n / k
// P1372 正解 —— 答案就是 n / k(整数除法),O(1)
//
// ============ ★ 两半的证明(缺一半就只是「猜对了」)============
//
// ① **上界**:若能挑出 k 个不同的号数、gcd 为 d,则它们都是 d 的倍数、都 ≤ n
// ⇒ 至少存在 k 个 ≤ n 的 d 的倍数 ⇒ **k·d ≤ n** ⇒ d ≤ n/k。
// ② **可达**:取 d = ⌊n/k⌋,那么 d, 2d, …, kd 都 ≤ n,互不相同,
// 而且它们的 gcd 恰好是 d(因为 gcd(d, 2d) 已经是 d 了)。
//
// ⇒ 答案 = **⌊n / k⌋**,而整数除法自带向下取整。
//
// ★ 题面还给了一句「一个数的最大公约数即本身」⇒ k = 1 时答案是 n ——
// 而 n / 1 = n,公式**自动覆盖**了这个边界,不用特判。
//
// ⚠ 这道题真正考的是「看出它是这件事」,不是 gcd 本身:正解里**一次 gcd 都没调**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long n, k;
if (!(cin >> n >> k)) return 0;
cout << n / k << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2⚠⚠ 而暴力的命运,和我草稿里写的正好相反

p1372Brute.cpp★ 从 n 往下试 d —— O(n),而它其实能 AC
// P1372 第 ① 版:从大到小试每一个 d
//
// ★ 「k 个人的默契程度 = 他们号数的最大公约数」⇒ 要挑 k 个**互不相同**的号数(1..n),
// 让它们的 gcd 最大。若 gcd 是 d,这 k 个数都是 d 的倍数 ⇒ 至少要有 k 个 ≤ n 的 d 的倍数
// ⇒ **k·d ≤ n**。
//
// ⇒ 这一版就照这句话枚举:从 n 往下试 d,第一个满足 k·d ≤ n 的就是答案。O(n)。
//
// ⚠⚠ **我的草稿在这儿写的是「顶格 n = 10⁹ 一秒钟悬」—— 实测打回来了:**
// 最坏形状是 k = n(要一路试到 d = 1,整整 10⁹ 次),
// 本机三次实测 **0.24 / 0.24 / 0.25 秒**,而时限是 1 秒 ⇒ **它其实过得去,余量 4 倍**。
// ★ 道理不玄:这个循环体只有「一次乘法 + 一次比较」,
// 现代 CPU 上 10⁹ 次这种简单循环就是 **0.2 秒量级**([第 45 章](/ch/45-estimate/)那把尺子)。
// ⇒ ★★ **「O(n) 配 n = 10⁹ 必然超时」是个想当然 —— 复杂度不等于耗时,循环体里有什么才算。**
//
// ⇒ 所以这一版不是「拿 20 分的暴力」,它是**能 AC 的**;
// 正解 O(1) 赢的不是速度,是「说得清为什么」(见页面第 ② 步那两半证明)。
//
// ⚠ k·d 最大到 10⁹ × 10⁹ = 10¹⁸ ⇒ **必须 long long**(见 p1372Ovf.cpp)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long n, k;
if (!(cin >> n >> k)) return 0;
for (long long d = n; d >= 1; d--)
if (d * k <= n) { cout << d << '\n'; return 0; }
cout << 0 << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 「O(n) 配 n = 10⁹ 必然超时」是个想当然

我的草稿在这一版的注释里写的是「顶格 n = 10⁹ 一秒钟悬」。实测把它打回来了。

这个暴力最坏的形状是 k = n(要一路试到 d = 1,整整 10⁹ 次)。 本机三次实测(A 机 · WSL2 · 2026-09-01 · 独占 · /usr/bin/time):

输入 耗时
n = k = 10⁹(最坏形状) 0.24 / 0.24 / 0.25 秒
时限 1 秒

它过得去,余量 4 倍。

★ 道理不玄:这个循环体只有一次乘法 + 一次比较, 现代 CPU 上 10⁹ 次这种循环就是 0.2 秒量级第 45 章那把尺子)。 ⇒ ★★ 复杂度不等于耗时 —— 循环体里有什么才算。 ⇒ 所以正解 O(1) 赢的不是速度,是「说得清为什么」。这道题考的本来就是那两半证明。

★ 而它的次数有一个闭式:试的次数 = n − ⌊n/k⌋ + 1n ≤ 300 的全部 45 150 组逐组核对,0 组不成立)—— ⇒ k = 1 时一次就中,k = n 时试满 n 次。

3✗ 唯一那个真会挂人的坑:判 k·d ≤ n 时用 int

p1372Ovf.cpp✗ 拿 32 位去装 k·d
★ 这条线是算出来的,而四个档位「触发 ≡ 抓获」一个不差

题面 n, k ≤ 10⁹k · d 最大到 10¹⁸,是 int 上限的 4.6 亿倍。

档位(每档 300 轮) n · k > 2³¹ 的轮数 int真被抓
0 顺手写的(n ≤ 60 0 精确的 0
1 题面 20% 那一档(k ≤ 2 0 精确的 0
★ 2 nk 都大 299 299
4 边界档(k = 1k = n 144 144

★★ 四个格子全部一个不差 —— 因为这个 bug 的触发条件只有一层dn 开始试,第一次判断就是 d = n ⇒ 只要 n·k > 2³¹ 就当场出事, 不像别的 bug 还要「而且那一步得影响到答案」。

⚠ 而前两行说明:顺手写的小数据和题面自己给的 20% 档,都结构上抓不到它。

p1372Gen.cpp(五个档位)★ 每一档都照着「某个错法靠什么现形」造
// P1372 的生成器:./p1372Gen 种子 [档位]
//
// ★ 每个版本靠什么现形:
// · p1372Ovf(判 k·d ≤ n 时用 int)→ 要 **n·k 越过 2³¹**(第一次判断就是 d = n)。
// ⇒ 顺手写的小数据(n、k 十来个)**结构上抓不到**;档 2 起才有。
// · p1372Brute → 它是**正确**的(只是慢),抓不到才对。
//
// 档位:
// 0 ★ 顺手写的样子:n ∈ [1, 60],k ∈ [1, n]
// 1 题面 20% 那一档:n ≤ 10³,k ≤ 2
// 2 ★★ n、k 都大到让 n·k 越过 2³¹(n ∈ [10⁵, 10⁶],k ∈ [10⁴, n])
// 3 ★ 顶格:n ∈ [10⁸, 10⁹],k ∈ [1, n]
// 4 ★ 边界档:k = 1(题面那句「一个数的最大公约数即本身」)或 k = n
//
// ⚠ 题面保证 n ≥ k ≥ 1 —— 生成器要亲手守住它。
// ⚠ rng() 一律先落到具名变量再传参。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int mode = argc > 2 ? atoi(argv[2]) : 0;
mt19937_64 rng(seed * 1000003ull + 20260901ull);
long long n, k;
if (mode == 1) { n = 1 + (long long)(rng() % 1000ull); k = 1 + (long long)(rng() % 2ull); if (k > n) k = n; }
else if (mode == 2) { n = 100000 + (long long)(rng() % 900001ull); k = 10000 + (long long)(rng() % (unsigned long long)(n - 9999)); }
else if (mode == 3) { n = 100000000LL + (long long)(rng() % 900000001ull); k = 1 + (long long)(rng() % (unsigned long long)n); }
else if (mode == 4) { n = 1 + (long long)(rng() % 1000000000ull); k = (rng() % 2ull) ? 1 : n; }
else { n = 1 + (long long)(rng() % 60ull); k = 1 + (long long)(rng() % (unsigned long long)n); }
printf("%lld %lld\n", n, k);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
p1372Count.cpp本页所有数字的出处(含全范围穷举那两半证明)
// P1372 的度量程序:./p1372Count csv (本页的数字都出自它)
//
// exh : ★★ 输入空间小的时候「算一遍」比对拍充分 ——
// 把 n ≤ 400、k ≤ n 的**全部** 80 200 组跑一遍,
// 逐组拿 O(n) 暴力核对公式 ⌊n/k⌋,顺带核对「d, 2d, …, kd 真的都 ≤ n 且互不相同」。
// line : ★ int 溢出那条线是算出来的:n·k > 2³¹ 才会现形 ⇒ 各档里满足它的组数。
// ms : 暴力在题面顶格 n = 10⁹ 上要多久(正解是 O(1))。
//
// ⚠ 这里**复刻**了 p1372Gen.cpp 的档位逻辑(同一个 mt19937_64、同一个种子公式)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static void gen(unsigned seed, int mode, long long& n, long long& k) {
mt19937_64 rng(seed * 1000003ull + 20260901ull);
if (mode == 1) { n = 1 + (long long)(rng() % 1000ull); k = 1 + (long long)(rng() % 2ull); if (k > n) k = n; }
else if (mode == 2) { n = 100000 + (long long)(rng() % 900001ull); k = 10000 + (long long)(rng() % (unsigned long long)(n - 9999)); }
else if (mode == 3) { n = 100000000LL + (long long)(rng() % 900000001ull); k = 1 + (long long)(rng() % (unsigned long long)n); }
else if (mode == 4) { n = 1 + (long long)(rng() % 1000000000ull); k = (rng() % 2ull) ? 1 : n; }
else { n = 1 + (long long)(rng() % 60ull); k = 1 + (long long)(rng() % (unsigned long long)n); }
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 全范围穷举 */
const long long LIM = 400;
long long total = 0, bad = 0, badWitness = 0;
for (long long n = 1; n <= LIM; n++)
for (long long k = 1; k <= n; k++) {
total++;
long long f = n / k;
long long b = 0;
for (long long d = n; d >= 1; d--) if (d * k <= n) { b = d; break; }
if (f != b) bad++;
// ★ 顺带把「可达」那一半也验一遍:d, 2d, …, kd 都 ≤ n
if (f * k > n) badWitness++;
}
/* ② int 溢出那条线:各档里 n·k > 2³¹ 的组数 */
const long long INT_MAX_LL = 2147483647LL;
int over[5] = {0};
for (int m = 0; m < 5; m++)
for (int s = 1; s <= 300; s++) {
long long n, k; gen(s, m, n, k);
if (n * k > INT_MAX_LL) over[m]++;
}
/* ③ 换一把机器无关的尺子:暴力要试多少个 d —— 而它有一个闭式 */
// 从 d = n 往下试到第一个满足 k·d ≤ n 的 d = ⌊n/k⌋ ⇒ 试的次数 = n − ⌊n/k⌋ + 1。
// ⇒ k = 1 时一次就中;**k = n 时要试满 n 次**(最坏)。
long long stepsBad = 0;
for (long long n = 1; n <= 300; n++)
for (long long k = 1; k <= n; k++) {
long long cnt = 0;
for (long long d = n; d >= 1; d--) { cnt++; if (d * k <= n) break; }
if (cnt != n - n / k + 1) stepsBad++;
}
long long worstSteps = 1000000000LL; // k = n = 10⁹ ⇒ 试满 10⁹ 次
/* ④ 真跑一次不可省略的循环,量它一次判断有多贵 */
volatile long long sink = 0;
const long long N2 = 200000000LL;
auto t0 = steady_clock::now();
for (long long d = N2; d >= 1; d--) { sink += d & 1; }
double msWorst = duration<double, milli>(steady_clock::now() - t0).count();
double proj = msWorst * 5.0; // 外推到 n = 10⁹
long long ans = (long long)sink;
if (csv) {
printf("exhTotal,%lld\nexhBad,%lld\nexhWitnessBad,%lld\n", total, bad, badWitness);
for (int m = 0; m < 5; m++) printf("over%d,%d\n", m, over[m]);
printf("stepsBad,%lld\nworstSteps,%lld\n", stepsBad, worstSteps);
printf("msWorst,%.0f\nprojMs,%.0f\nans,%lld\n", msWorst, proj, ans);
return 0;
}
printf("① 全范围穷举 n ≤ %lld、k ≤ n(共 %lld 组):\n", LIM, total);
printf(" 公式 ⌊n/k⌋ 和 O(n) 暴力不一致的组数 = %lld\n", bad);
printf(" 「可达」那一半(d, 2d, …, kd 都 ≤ n)不成立的组数 = %lld\n\n", badWitness);
printf("② int 溢出那条线(要 n·k > 2³¹)—— 各档 300 轮里满足它的组数:\n");
const char* NM[5] = {"0 顺手(n ≤ 60)", "1 题面 20% 档(k ≤ 2)", "2 ★ n、k 都大", "3 顶格随机", "4 边界(k = 1 或 k = n)"};
for (int m = 0; m < 5; m++) printf(" %-26s %3d / 300\n", NM[m], over[m]);
printf("\n③ 换一把机器无关的尺子:暴力试的次数 = n − ⌊n/k⌋ + 1\n");
printf(" (n ≤ 300 的全部组合逐组核对这个闭式,不成立的有 %lld 组)\n", stepsBad);
printf(" ⇒ k = 1 时一次就中;**k = n 时要试满 n 次** ⇒ 顶格 %lld 次\n", worstSteps);
printf("\n④ 顶格那 10⁹ 次到底有多贵:2×10⁸ 次空转实测 %.0f 毫秒 ⇒ 外推 10⁹ 次约 **%.0f 毫秒**\n",
msWorst, proj);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 一页纸
关键一步 翻译成「1..n 里有没有 kd 的倍数」⇒ k·d ≤ n ⇒ 答案 ⌊n/k⌋
证明要两半 上界(必要)+ 可达(构造 d, 2d, …, kd)—— 全范围穷举 80 200 组,0 组不一致
⚠ 我的草稿错在哪 写了「O(n) 顶格会超时」,实测 0.24 秒、余量 4 倍 —— 复杂度不等于耗时
⚠ 真会挂人的 k·dint(最大 10¹⁸)—— 而顺手数据和题面 20% 档都抓不到
gcd 的关系 ★ 正解一次 gcd 都没调 —— 这道题练的是「看出它是这件事」