洛谷P1045 [NOIP 2003 普及组] 麦森数
题目描述
形如 2P−1 的素数称为麦森数,这时 P 一定也是个素数。但反过来不一定,即如果 P 是个素数,2P−1 不一定也是素数。到 1998 年底,人们已找到了 37 个麦森数。最大的一个是 P=3021377,它有 909526 位。麦森数有许多重要应用,它与完全数密切相关。
任务:输入 P(1000<P<3100000),计算 2P−1 的位数和最后 500 位数字(用十进制高精度数表示)
输入格式
文件中只包含一个整数 P(1000<P<3100000)
输出格式
第一行:十进制高精度数 2P−1 的位数。
第 2∼11 行:十进制高精度数 2P−1 的最后 500 位数字。(每行输出 50 位,共输出 10 行,不足 500 位时高位补 0)
不必验证 2P−1 与 P 是否为素数。
输入输出样例
输入 #1复制
1279
输出 #1复制
386 00000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000 00000000000000104079321946643990819252403273640855 38615262247266704805319112350403608059673360298012 23944173232418484242161395428100779138356624832346 49081399066056773207629241295093892203457731833496 61583550472959420547689811211693677147548478866962 50138443826029173234888531116082853841658502825560 46662248318909188018470682222031405210266984354887 32958028878050869736186900714720710555703168729087
说明/提示
【题目来源】
NOIP 2003 普及组第四题
题目分析
麦森数是指形如 2^p - 1 的数,其中 p 是一个素数。本题要求计算 2^p - 1 的位数以及最后500位数字(不足500位时前面补0)。
难点分析
大数计算:当 p 很大时(题目中 p 最大为3100000),2^p 的值极其巨大,无法用常规数据类型存储。
高效计算:直接进行 p 次乘法会超时,需要使用快速幂算法。
位数计算:不需要实际计算整个数就能确定位数。
存储优化:只需存储最后500位,可以节省空间。
数学原理
位数计算
对于 2^p - 1 的位数,我们可以利用对数性质:
设 N = 2^p - 1,由于 2^p 远大于1,所以 N ≈ 2^p。
根据对数性质:log10(2^p) = p · log10(2)
数的位数 d = floor(log10(N)) + 1 ≈ floor(p · log10(2)) + 1
快速幂算法
快速幂算法通过二分思想将时间复杂度从 O(p) 降低到 O(log p):
当 p 为偶数时:a^p = (a^(p/2))^2
当 p 为奇数时:a^p = a · (a^((p-1)/2))^2
算法设计
数据结构
使用整型数组存储大数,每个元素存储若干位数字(这里选择每元素存储1位数字,便于理解)。
核心算法
计算位数:直接使用公式 d = floor(p · log10(2)) + 1
计算最后500位:使用快速幂算法,只保留最后500位
减法处理:计算 2^p - 1,注意借位问题
代码实现
cpp
代码详细解析
头文件说明
cpp
#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;// 大数乘法(只保留最后500位)
// a: 第一个大数数组(每位存储一个数字)
// b: 第二个大数数组(每位存储一个数字)
void mul(int a[], int b[]) {int tmp[1005] = {0}; // 临时结果数组,初始化为0// 模拟竖式乘法,计算每一位的乘积for (int i = 0; i < 500; i++) {for (int j = 0; j < 500; j++) {if (i + j < 1000) { // 只计算可能影响最后500位的部分tmp[i + j] += a[i] * b[j];}}}// 处理进位,只保留最后500位for (int i = 0; i < 500; i++) {if (i < 999) {tmp[i + 1] += tmp[i] / 10; // 进位到高位}tmp[i] %= 10; // 保留个位数}// 将结果复制回a数组(只取最后500位)for (int i = 0; i < 500; i++) {a[i] = tmp[i];}
}// 快速幂计算 2^p
// p: 指数
void qpow(int p) {int res[1005] = {0}; // 结果数组,存储最终结果int base[1005] = {0}; // 底数数组,存储当前底数// 初始化:res = 1, base = 2res[0] = 1; // 个位为1base[0] = 2; // 个位为2// 快速幂算法核心while (p > 0) {if (p % 2 == 1) {mul(res, base); // 如果指数是奇数,结果乘以当前底数}mul(base, base); // 底数平方p /= 2; // 指数减半}// 执行减法:2^p - 1res[0] -= 1; // 个位减1// 处理借位for (int i = 0; i < 500; i++) {if (res[i] < 0) {res[i] += 10; // 借位res[i + 1] -= 1;}}// 输出最后500位,每50位换行for (int i = 499; i >= 0; i--) {cout << res[i];if (i % 50 == 0 && i != 0) {cout << endl;}}
}int main() {int p;cin >> p;// 计算位数:使用对数公式int digits = (int)(p * log10(2)) + 1;cout << digits << endl;// 计算并输出最后500位qpow(p);return 0;
}
大数乘法函数 mul
这是整个程序的核心函数,负责处理大数乘法运算:
cpp
void mul(int a[], int b[]) {int tmp[1005] = {0}; // 临时数组,大小1005确保有足够空间
临时数组 tmp 用于存储乘法运算的中间结果。我们分配1005个元素是为了确保有足够的空间处理进位,即使两个500位的数相乘也不会溢出。
cpp
for (int i = 0; i < 500; i++) {for (int j = 0; j < 500; j++) {if (i + j < 1000) {tmp[i + j] += a[i] * b[j];}}}
这部分代码模拟手工竖式乘法。外层循环遍历第一个数的每一位,内层循环遍历第二个数的每一位。a[i] * b[j] 的结果应该放在 tmp[i+j] 位置,因为这是相应位的乘积。
条件 i+j < 1000 是一个优化,只计算可能影响最后500位的乘积,因为更高位的结果最终会被舍弃。
cpp
for (int i = 0; i < 500; i++) {if (i < 999) {tmp[i + 1] += tmp[i] / 10;}tmp[i] %= 10;}
这部分处理进位。对于每一位,如果值大于等于10,就将十位部分加到下一位,只保留个位在当前位。
cpp
for (int i = 0; i < 500; i++) {a[i] = tmp[i];}
}
最后将临时数组的前500位复制回原数组,完成乘法运算。
快速幂函数 qpow
这个函数使用快速幂算法高效计算 2^p:
cpp
void qpow(int p) {int res[1005] = {0}; // 结果数组int base[1005] = {0}; // 底数数组
res 数组存储最终结果,初始化为1(2^0 = 1)。base 数组存储当前底数,初始化为2。
cpp
res[0] = 1;base[0] = 2;
初始化结果和底数。使用数组的0索引表示个位,这是大数存储的常见方式。
cpp
while (p > 0) {if (p % 2 == 1) {mul(res, base);}mul(base, base);p /= 2;}
这是快速幂算法的核心循环:
如果当前指数p是奇数,将结果乘以当前底数
无论指数奇偶,底数都要平方
指数除以2(向下取整)
通过这种方式,算法复杂度从O(p)降低到O(log p)。
cpp
res[0] -= 1;for (int i = 0; i < 500; i++) {if (res[i] < 0) {res[i] += 10;res[i + 1] -= 1;}}
计算 2^p - 1,从个位开始减1,并处理可能的借位。
cpp
for (int i = 499; i >= 0; i--) {cout << res[i];if (i % 50 == 0 && i != 0) {cout << endl;}}
}
逆序输出结果数组(因为数组是低位在前),每50位换行,满足题目输出格式要求。
主函数 main
cpp
int main() {int p;cin >> p;int digits = (int)(p * log10(2)) + 1;cout << digits << endl;qpow(p);return 0;
}
主函数逻辑清晰:
读入指数p
计算并输出位数
调用快速幂函数计算并输出最后500位
算法优化分析
时间复杂度
位数计算:O(1)
快速幂算法:O(log p)
每次乘法:O(500^2) = O(250000)
总时间复杂度为 O(250000 × log p),对于p ≤ 3100000,log p ≈ 22,总操作数约550万次,在合理范围内。
空间复杂度
使用固定大小的数组,空间复杂度为O(1)。
优化技巧
只保留必要位数:只计算和存储最后500位,大大减少计算量
快速幂算法:将指数运算从线性时间优化到对数时间
循环优化:在乘法中通过条件判断减少不必要的计算
常见问题解答
1. 为什么使用数组存储大数?
因为2^p的值可能达到数百万位,远超任何基本数据类型的表示范围。数组可以灵活地存储任意位数的大数。
2. 为什么数组的0索引表示个位?
这种存储方式(低位在前)便于进行进位处理,因为进位是向高位(索引增加的方向)进行的。
3. 快速幂算法为什么有效?
快速幂算法利用了指数的二进制表示性质。例如计算2^13:
13的二进制是1101
2^13 = 2^(8+4+1) = 2^8 × 2^4 × 2^1
通过平方操作可以快速得到2^1, 2^2, 2^4, 2^8等幂次
4. 如何处理减法借位?
从个位开始减1,如果当前位不够减,就向高位借位(当前位加10,高位减1)。
测试样例
样例1:p = 1279
这是题目提供的一个测试样例,可以用来验证程序正确性。
样例2:p = 1000
较小值,便于手动验证。
总结
本题考察了大数运算和快速幂算法的应用。通过合理的算法设计和优化,可以在规定时间内完成计算。关键点包括:
利用数学公式直接计算位数,避免大数运算
使用快速幂算法高效计算大数幂
只保留必要的位数,减少计算量
合理的数据结构设计,便于处理进位和借位