0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1045,日期见页头。两边不一致时信原站。
题目描述
形如 2ᴾ − 1 的素数称为麦森数,这时 P 一定也是个素数。但反过来不一定,即如果 P 是个素数,2ᴾ − 1 不一定也是素数。
到 1998 年底,人们已找到了 37 个麦森数。最大的一个是 P = 3021377,它有 909526 位。
麦森数有许多重要应用,它与完全数密切相关。
任务:输入 P(1000 < P < 3100000),计算 2ᴾ − 1 的位数和最后 500 位数字(用十进制高精度数表示)。
输入格式
文件中只包含一个整数 P(1000 < P < 3100000)。
输出格式
第一行:十进制高精度数 2ᴾ − 1 的位数。
第 2 ~ 11 行:十进制高精度数 2ᴾ − 1 的最后 500 位数字。
(每行输出 50 位,共输出 10 行,不足 500 位时高位补 0)
不必验证 2ᴾ − 1 与 P 是否为素数。
时限 1 秒,内存 125 MB(128000 KB)。(题目来源:NOIP 2003 普及组第四题)
输入输出样例
输入
1279
输出
386 00000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000 00000000000000104079321946643990819252403273640855 38615262247266704805319112350403608059673360298012 23944173232418484242161395428100779138356624832346 49081399066056773207629241295093892203457731833496 61583550472959420547689811211693677147548478866962 50138443826029173234888531116082853841658502825560 46662248318909188018470682222031405210266984354887 32958028878050869736186900714720710555703168729087
P = 1279 ⇒ 2¹²⁷⁹ − 1 有 386 位,所以前两行半都是补出来的 0。
1★★★ 关键一步:「取模」被换成了「只留末 500 位」
上一道 P3390 把「乘」换成了矩阵乘;这道题换的是另一样东西:
「对 p 取模」换成「只留末 500 位」。
而这两件事本来就是同一件事 —— 只留末 500 位就是 mod 10⁵⁰⁰。
它成立的理由和取模一模一样:
乘积的末 500 位,只由两个乘数的末 500 位决定。
⇒ 第 42 章那五行一个字不改,只是把 res * a % p
换成「高精度乘法,乘完把超出 500 位的那些位直接扔掉」。
★ 而这道题的另一半(位数)走的是完全不同的一条路:
2ᴾ不是 10 的幂 ⇒2ᴾ − 1和2ᴾ位数相同;- 位数
= ⌊P·log₁₀2⌋ + 1—— 一行double,根本不用高精度。
⇒ ★★ 一道题里两个问,用的是两套完全无关的工具 —— 所以下面那个「位数忘了 +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;}点「运行 ▶」看结果
- 压位:一个
int存 4 位十进制 ⇒ 500 位只要 125 个 limb, 一次乘法125² = 15 625次 —— 而快速幂顶格一共只做 37 次乘法(合计 162 768 次 limb 乘法)。 - 减 1 不会借位:
P ≥ 1时2ᴾ的末位是 2/4/6/8 ⇒ 减 1 只改最后一个数字。 (下面「忘了减 1」那个错法的输出和正解只差一个字符,正是这个原因。)
2⚠⚠ 不用快速幂行不行 —— 又一次「肯定超时」被实测打回
// ⚠ 第 ① 版:不用快速幂 —— 老老实实乘 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;}点「运行 ▶」看结果
顶格 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⚠ 「不截断」那一版:答案永远对,只是跑不完
末 500 位从来不受高位影响 ⇒ 不截断算出来的答案和正解逐字节相同(三档 900 轮 0 次)。
而顶格 P = 3 099 999 时:
| 量的是 | 实测 |
|---|---|
2ᴾ 有多少位 |
933 193 |
| 压位后多少个 limb | 233 299 |
| 一次乘法要多少次运算 | ★ 5.44 × 10¹⁰ |
⇒ 而快速幂要做三十几次这样的乘法。想都不用想。 ⇒ 又一个「答案对但跑不完」,对拍和样例都是聋的。
4⚠ 三个真会答错的地方 —— 官方样例把三个全挡住了
| 档位(每档 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 ≥ 500⟺P ≥ 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)要把那个结构写全了才成立」,同一轮里第二次。
⌊P·log₁₀2⌋ 会不会因为 double 的误差取错?把 P ∈ (1000, 3.1×10⁶) 每一个都试一遍,
看 P·log₁₀2 离整数最近有多近:
| 量的是 | 实测 |
|---|---|
| 最近的那次距离 | 1.565 × 10⁻⁷(P = 325 147) |
double 在这个量级(约 10⁶)上的相对误差 |
约 10⁻¹⁰ |
⇒ 够用,而且余量还有三个数量级。 ★ 这类「浮点够不够」的问题,和「int 够不够」一样是能算、也能穷举的。
5★ 哪一版就已经能过了
// 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;}点「运行 ▶」看结果