题单 · 习题解析

洛谷 P1045 [NOIP 2003 普及组] 麦森数

★★★ 「快速幂只要结合律」的第二个现场:这里把**「对 p 取模」换成「只留末 500 位」**—— 而那本来就是 mod 10⁵⁰⁰,理由一模一样(乘积的末 500 位只由两个乘数的末 500 位决定)⇒ [第 42 章](/ch/42-fastpow/)那五行**一个字不改**;★ 而位数走的是完全无关的一条路(2ᴾ 不是 10 的幂 ⇒ 位数 = ⌊P·log₁₀2⌋+1,**根本不用高精度**)⇒ 「位数忘了 +1」那个错法[只污染输出的第一行](/sol/p2712/);⚠⚠ **「不用快速幂肯定超时」又被打回**(这一轮第四次):连乘 P 次 2 顶格 **0.67 秒 / 时限 1 秒**,因为**「乘以 2」是 O(len) 不是 O(len²)** —— 基本运算多 **2381 倍**、秒表差一万倍,可**余量只有 1.46 倍**(和同轮 [P1217](/sol/p1217/) 一模一样的处境:不是过不了,是赌不起);★★ 补零那个坑的触发线是**算出来的**:2ᴾ−1 满 500 位 ⟺ P ≥ 499/log₁₀2 ⇒ **第一个是 P = 1658** ⇒ P < 1658 一定要补零(300/300)、之后只剩约十分之一(26/300),三格「触发 ≡ 抓获」一个不差;⚠ 而这一档上界我第一版写 1660、越线三个数,就从 300 变成 297;★ 位数用 double 安不安全也是穷举出来的(P·log₁₀2 离整数最近 **1.565×10⁻⁷**,余量三个数量级);★ 「不截断」那版**答案永远对**(900 轮 0 次),挂在长度上(顶格 2ᴾ 有 **933 193 位**,一次乘法 5.44×10¹⁰ 次运算)

原题:洛谷 P1045出自 第 42 章 快速幂与取模 的题单题面本地存档:2026-09-02
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

形如 2ᴾ − 1 的素数称为麦森数,这时 P 一定也是个素数。但反过来不一定,即如果 P 是个素数,2ᴾ − 1 不一定也是素数。 到 1998 年底,人们已找到了 37 个麦森数。最大的一个是 P = 3021377,它有 909526 位。 麦森数有许多重要应用,它与完全数密切相关。

任务:输入 P1000 < P < 3100000),计算 2ᴾ − 1位数最后 500 位数字(用十进制高精度数表示)。

输入格式

文件中只包含一个整数 P1000 < P < 3100000)。

输出格式

第一行:十进制高精度数 2ᴾ − 1 的位数。

第 2 ~ 11 行:十进制高精度数 2ᴾ − 1 的最后 500 位数字。 (每行输出 50 位,共输出 10 行,不足 500 位时高位补 0

不必验证 2ᴾ − 1P 是否为素数。

时限 1 秒,内存 125 MB(128000 KB)。(题目来源:NOIP 2003 普及组第四题)

输入输出样例

输入

1279

输出

386
00000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000
00000000000000104079321946643990819252403273640855
38615262247266704805319112350403608059673360298012
23944173232418484242161395428100779138356624832346
49081399066056773207629241295093892203457731833496
61583550472959420547689811211693677147548478866962
50138443826029173234888531116082853841658502825560
46662248318909188018470682222031405210266984354887
32958028878050869736186900714720710555703168729087

P = 12792¹²⁷⁹ − 1 有 386 位,所以前两行半都是补出来的 0。

1★★★ 关键一步:「取模」被换成了「只留末 500 位」

★★★ 这是「快速幂只要结合律」的第二个现场

上一道 P3390 把「乘」换成了矩阵乘;这道题换的是另一样东西:

「对 p 取模」换成「只留末 500 位」。

而这两件事本来就是同一件事 —— 只留末 500 位就是 mod 10⁵⁰⁰。 它成立的理由和取模一模一样:

乘积的末 500 位,只由两个乘数的末 500 位决定。

第 42 章那五行一个字不改,只是把 res * a % p 换成「高精度乘法,乘完把超出 500 位的那些位直接扔掉」。

★ 而这道题的另一半(位数)走的是完全不同的一条路:

  • 2ᴾ 不是 10 的幂 ⇒ 2ᴾ − 12ᴾ 位数相同
  • 位数 = ⌊P·log₁₀2⌋ + 1 —— 一行 double,根本不用高精度

⇒ ★★ 一道题里两个问,用的是两套完全无关的工具 —— 所以下面那个「位数忘了 +1」的错法只污染输出的第一行

p1045.cpp★ 正解:高精度快速幂(压位,4 位一个 limb)—— 顶格不到 0.1 毫秒
// P1045 麦森数 —— 正解:位数用对数算,末 500 位用**高精度快速幂**
//
// ★★★ 这道题是「快速幂只要结合律」的第二个现场(第一个是 [P3390](/sol/p3390/)):
// 这里把「取模」换成了「**只留末 500 位**」—— 而那本来就是 mod 10⁵⁰⁰。
// 它成立的理由和取模一模一样:**乘积的末 500 位,只由两个乘数的末 500 位决定。**
// ⇒ 那五行一个字不改。
//
// 两件事各走各的路:
// ① **位数**:2^P − 1 和 2^P 位数相同(2^P 不是 10 的幂)⇒ 位数 = ⌊P·log₁₀2⌋ + 1。
// ⚠ 这一半**根本不用高精度**。
// ② **末 500 位**:高精度快速幂算 2^P,再减 1。
// ★ 减 1 不会借位:P ≥ 1 时 2^P 的末位是 2/4/6/8 ⇒ 只改最后一位。
//
// ⚠ 压位:一个 int 存 4 位十进制 ⇒ 500 位只要 125 个 limb,
// 一次乘法 125² = 15 625 次 —— 而快速幂一共只做四十几次乘法。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
static const int L = 125; // 125 个 limb × 4 位 = 500 位
static const int BASE = 10000;
struct Big { int a[L]; Big() { memset(a, 0, sizeof a); } };
/** 只保留末 500 位的乘法(也就是 mod 10⁵⁰⁰) */
static Big mul(const Big& x, const Big& y) {
static ll tmp[L];
memset(tmp, 0, sizeof tmp);
for (int i = 0; i < L; i++) {
if (!x.a[i]) continue;
for (int j = 0; i + j < L; j++) // ★ i + j ≥ L 的那些位,正是被「取模」丢掉的
tmp[i + j] += (ll)x.a[i] * y.a[j];
}
Big z;
ll carry = 0;
for (int i = 0; i < L; i++) { ll v = tmp[i] + carry; z.a[i] = (int)(v % BASE); carry = v / BASE; }
return z; // carry 溢出去的也丢掉 —— 同样是取模
}
int main() {
int p;
if (scanf("%d", &p) != 1) return 0;
printf("%d\n", (int)(p * log10(2.0)) + 1); // ① 位数
Big res, base; // ② 末 500 位:快速幂
res.a[0] = 1;
base.a[0] = 2;
for (int b = p; b; b >>= 1) {
if (b & 1) res = mul(res, base);
base = mul(base, base);
}
res.a[0] -= 1; // ★ 2^P 末位是偶数 ⇒ 不借位
char s[501];
for (int i = 0; i < L; i++) { // 铺成 500 个字符,高位在前
int v = res.a[i];
for (int d = 0; d < 4; d++) { s[500 - (i * 4 + d) - 1] = char('0' + v % 10); v /= 10; }
}
for (int r = 0; r < 10; r++) { fwrite(s + r * 50, 1, 50, stdout); putchar('\n'); }
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 两处顺手的账
  • 压位:一个 int 存 4 位十进制 ⇒ 500 位只要 125 个 limb, 一次乘法 125² = 15 625 次 —— 而快速幂顶格一共只做 37 次乘法(合计 162 768 次 limb 乘法)。
  • 减 1 不会借位P ≥ 12ᴾ 的末位是 2/4/6/8 ⇒ 减 1 只改最后一个数字。 (下面「忘了减 1」那个错法的输出和正解只差一个字符,正是这个原因。)

2⚠⚠ 不用快速幂行不行 —— 又一次「肯定超时」被实测打回

p1045Brute.cpp⚠ 第 ① 版:连乘 P 次 2(每次只留末 500 位)—— 它其实过得去
// ⚠ 第 ① 版:不用快速幂 —— 老老实实乘 P 次 2(每次只留末 500 位)
//
// ★★ 它比你以为的强得多,而理由很具体:**乘以 2 是 O(len) 的,不是 O(len²)。**
// 快速幂省下来的是「四十几次 125×125 的乘法」,
// 而这一版做的是「三百多万次 125 长度的扫描」——
// ⇒ 基本操作多了几百倍,可每一次都便宜得多。
// ⇒ 这一版到底过不过得去,见页面第 1 步那张表(结论和直觉不一样)。
#include <bits/stdc++.h>
using namespace std;
static const int L = 125;
static const int BASE = 10000;
int main() {
int p;
if (scanf("%d", &p) != 1) return 0;
printf("%d\n", (int)(p * log10(2.0)) + 1);
static int a[L];
memset(a, 0, sizeof a);
a[0] = 1;
for (int t = 0; t < p; t++) { // 乘 P 次 2
int carry = 0;
for (int i = 0; i < L; i++) { int v = a[i] * 2 + carry; a[i] = v % BASE; carry = v / BASE; }
}
a[0] -= 1;
char s[501];
for (int i = 0; i < L; i++) { int v = a[i]; for (int d = 0; d < 4; d++) { s[500 - (i * 4 + d) - 1] = char('0' + v % 10); v /= 10; } }
for (int r = 0; r < 10; r++) { fwrite(s + r * 50, 1, 50, stdout); putchar('\n'); }
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 基本运算多 2381 倍,秒表却只差一万倍 —— 而它没超时
顶格 P = 3 099 999(独占实测,3 次取中位数) 基本运算次数 本机
★ 快速幂 162 768 次 limb 乘法(37 次高精度乘法) 0.07 毫秒
⚠ 连乘 P 次 2 387 499 875 次 limb 运算(2381 倍) 686 毫秒 / 时限 1000

连乘那一版本机 0.67 秒,它没超时 —— 而理由很具体:

「乘以 2」是 O(len) 的,不是 O(len²) 快速幂省下的是「几十次 125×125 的乘法」, 而连乘做的是「三百多万次长度 125 的扫描」—— 次数多得多,但每一次便宜得多。

⚠ 而余量只有 1.46 倍 —— 和同一轮的 P1217 一模一样的处境「它不是过不了,是赌不起」。 ⇒ 这一轮「XX 肯定超时」这句话已经被打回第四次了。

3⚠ 「不截断」那一版:答案永远对,只是跑不完

p1045NoTrunc.cpp⚠ 高精度乘法不截断 —— 答案对,长度爆炸
★ 对拍九百轮 0 次不一致 —— 挂它的是长度,不是答案

末 500 位从来不受高位影响 ⇒ 不截断算出来的答案和正解逐字节相同(三档 900 轮 0 次)。

而顶格 P = 3 099 999 时:

量的是 实测
2ᴾ 有多少位 933 193
压位后多少个 limb 233 299
一次乘法要多少次运算 5.44 × 10¹⁰

⇒ 而快速幂要做三十几次这样的乘法。想都不用想。 ⇒ 又一个「答案对但跑不完」,对拍和样例都是聋的

4⚠ 三个真会答错的地方 —— 官方样例把三个全挡住了

p1045NoDec.cpp✗ 错法一:忘了减 1(输出只差最后一个字符)
p1045Digits.cpp✗ 错法二:位数忘了 +1(只错第一行)
p1045Fmt.cpp✗ 错法三:把末 500 位当成一个「数」打印,去掉了前导零
p1045Gen.cpp生成器(四档:照题面缩小 / 不足 500 位 / 足 500 位 / 顶格)
★★ 补零那个坑的触发条件是一条能算出来的线:P = 1658
档位(每档 300 轮) ✗ 忘了减 1 ✗ 位数少 1 ✗ 去掉前导零 (那 500 个字符首位是 0 的轮数)
0 照题面缩小(P ∈ [1001, 20000] 300 300 45 45
1 P ∈ [1001, 1657]不足 500 位 300 300 300 300
2 P ∈ [1661, 20000](足 500 位) 300 300 26 26

★ 三格「触发 ≡ 抓获」一个不差。而那条线是算出来的

2ᴾ − 1 满 500 位 ⟺ ⌊P·log₁₀2⌋ + 1 ≥ 500P ≥ 499 / log₁₀2 = 1657.6第一个满 500 位的 P 是 1658(度量程序穷举验过)。

P < 1658一定要补零(档 1 是 300 / 300); P ≥ 1658 时只有「第 500 位恰好是 0」才触发 ⇒ 约十分之一(档 2 是 26 / 300)。 ⚠ 而官方样例 P = 1279(386 位)正落在第一种里 —— 一测就死

★ 顺带:这一档的上界我第一版写的是 1660,越过那条线三个数, 那一档就从「精确的 300」变成 297。 ⇒ 「结构性的 0(或 300)要把那个结构写全了才成立」,同一轮里第二次。

★ 位数那一行用 double 算,安不安全 —— 也是量出来的

⌊P·log₁₀2⌋ 会不会因为 double 的误差取错?把 P ∈ (1000, 3.1×10⁶) 每一个都试一遍, 看 P·log₁₀2 离整数最近有多近:

量的是 实测
最近的那次距离 1.565 × 10⁻⁷P = 325 147
double 在这个量级(约 10⁶)上的相对误差 10⁻¹⁰

够用,而且余量还有三个数量级。 ★ 这类「浮点够不够」的问题,和「int 够不够」一样是能算、也能穷举的

5★ 哪一版就已经能过了

p1045Count.cpp本页所有数字的出处
// P1045 的度量程序:./p1045Count csv (本页的数字都出自它)
//
// line : ★ 「不足 500 位」那条线精确在哪个 P(穷举求出)。
// prec : ★★ 位数用 double 算安不安全 —— 把 (1000, 3.1×10⁶) 里每个 P 的
// frac(P·log₁₀2) 离整数最近的距离量出来,和 double 的误差比一比。
// ops : ★★★ 快速幂做几次「125×125 的乘法」↔ 连乘做几次「长度 125 的扫描」。
// ms : 顶格 P = 3.1×10⁶ 的秒表:正解 / 连乘 / 不截断(后者只能算,跑不动)。
// len : 不截断的话,2^P 有多少位、多少个 limb、一次乘法多少次基本运算。
//
// ⚠ 秒表用 steady_clock 在进程内量,每档 3 次取中位数。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
typedef long long ll;
static const int L = 125, BASE = 10000;
static const int PMAX = 3100000;
struct Big { int a[L]; Big() { memset(a, 0, sizeof a); } };
static ll mulOps;
static Big mul(const Big& x, const Big& y) {
static ll tmp[L]; memset(tmp, 0, sizeof tmp);
for (int i = 0; i < L; i++) { if (!x.a[i]) continue; for (int j = 0; i + j < L; j++) { tmp[i + j] += (ll)x.a[i] * y.a[j]; mulOps++; } }
Big z; ll c = 0;
for (int i = 0; i < L; i++) { ll v = tmp[i] + c; z.a[i] = (int)(v % BASE); c = v / BASE; }
return z;
}
static double med3(double a, double b, double c) { return max(min(a, b), min(max(a, b), c)); }
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
const double LG2 = log10(2.0);
/* ① 「不足 500 位」那条线 */
int line = 0;
for (int p = 1001; p <= 3000; p++) if ((int)(p * LG2) + 1 >= 500) { line = p; break; }
/* ② 位数公式用 double 安不安全:frac(P·log₁₀2) 离 0 / 1 最近多少 */
double minFrac = 1.0;
int minP = 0;
for (int p = 1001; p < PMAX; p++) {
long double v = (long double)p * (long double)LG2;
long double f = v - floorl(v);
double d = (double)min(f, (long double)1.0 - f);
if (d < minFrac) { minFrac = d; minP = p; }
}
/* ③ 两种写法的基本运算次数 */
ll fastMuls = 0, fastOps;
{
mulOps = 0;
Big res, base; res.a[0] = 1; base.a[0] = 2;
for (int b = PMAX - 1; b; b >>= 1) { if (b & 1) { res = mul(res, base); fastMuls++; } base = mul(base, base); fastMuls++; }
fastOps = mulOps;
}
ll bruteOps = (ll)(PMAX - 1) * L;
/* ④ 秒表:顶格 P */
static volatile int VP = PMAX - 1;
double msFast, msBrute;
{
double t[3];
for (int r = 0; r < 3; r++) {
int p = VP;
auto t0 = steady_clock::now();
Big res, base; res.a[0] = 1; base.a[0] = 2;
for (int b = p; b; b >>= 1) { if (b & 1) res = mul(res, base); base = mul(base, base); }
volatile int sink = res.a[0]; (void)sink;
t[r] = duration<double, milli>(steady_clock::now() - t0).count();
}
msFast = med3(t[0], t[1], t[2]);
}
{
double t[3];
for (int r = 0; r < 3; r++) {
int p = VP;
auto t0 = steady_clock::now();
static int a[L]; memset(a, 0, sizeof a); a[0] = 1;
for (int i = 0; i < p; i++) { int c = 0; for (int j = 0; j < L; j++) { int v = a[j] * 2 + c; a[j] = v % BASE; c = v / BASE; } }
volatile int sink = a[0]; (void)sink;
t[r] = duration<double, milli>(steady_clock::now() - t0).count();
}
msBrute = med3(t[0], t[1], t[2]);
}
/* ⑤ 不截断的话有多大 */
ll topDigits = (ll)((PMAX - 1) * LG2) + 1;
ll topLimbs = (topDigits + 3) / 4;
double topOnceOps = (double)topLimbs * (double)topLimbs;
if (csv) {
printf("line,%d\n", line);
printf("minFrac,%.3e\n", minFrac);
printf("minP,%d\n", minP);
printf("fastMuls,%lld\n", fastMuls);
printf("fastOps,%lld\n", fastOps);
printf("bruteOps,%lld\n", bruteOps);
printf("opsRatio,%.0f\n", (double)bruteOps / (double)fastOps);
printf("msFast,%.2f\n", msFast);
printf("msBrute,%.0f\n", msBrute);
printf("bruteOverFast,%.0f\n", msBrute / msFast);
printf("topDigits,%lld\n", topDigits);
printf("topLimbs,%lld\n", topLimbs);
printf("topOnceOps,%.3e\n", topOnceOps);
return 0;
}
printf("① 「2^P − 1 不足 500 位」那条线:第一个满 500 位的 P 是 **%d**\n", line);
printf("\n② 位数用 double 算安不安全:P ∈ (1000, 3.1×10⁶) 里,P·log₁₀2 离整数最近的距离是 %.3e(P = %d)\n",
minFrac, minP);
printf(" ⇒ double 在这个量级上的误差约 1e−10 ⇒ **够用,而且余量还有三四个数量级**\n");
printf("\n③ 基本运算次数(顶格 P = %d)\n", PMAX - 1);
printf(" 快速幂:%lld 次高精度乘法,一共 %lld 次 limb 乘法\n", fastMuls, fastOps);
printf(" 连乘 :%d 次「乘 2」,一共 %lld 次 limb 运算 ⇒ 是快速幂的 %.0f 倍\n",
PMAX - 1, bruteOps, (double)bruteOps / (double)fastOps);
printf("\n④ 秒表(独占实测,3 次取中位数;时限 1000 ms)\n");
printf(" ★ 快速幂 %.2f ms / 连乘 %.0f ms ⇒ 差 %.0f 倍\n", msFast, msBrute, msBrute / msFast);
printf(" ⚠ 而**两者都过得去** —— 连乘赢在「乘 2 是 O(len) 而不是 O(len²)」\n");
printf("\n⑤ 如果不截断:2^P 有 %lld 位、%lld 个 limb,**一次**乘法就要 %.3e 次运算 ⇒ 想都不用想\n",
topDigits, topLimbs, topOnceOps);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
两版都能过,而正解快一万倍
版本 顶格 P = 3 099 999 交上去
⚠ 连乘 P 次 2 0.67 秒 / 时限 1 秒(余量 1.46 倍) ⚠ 能过,但赌不起
⚠ 高精度不截断 一次乘法就要 5.44 × 10¹⁰ 次运算 TLE
★ 高精度快速幂(压位) 不到 0.1 毫秒 AC

⇒ 这道题真正教的,是 「取模」这两个字可以有多宽 —— 它在第 42 章正文里是 % p,在 P3390 里是矩阵, 在这里是「只留末 500 位」。三处用的是同一段五行代码。