题目描述
形如 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 = 2
res[0] = 1; // 个位为1
base[0] = 2; // 个位为2
// 快速幂算法核心
while (p > 0) {
if (p % 2 == 1) {
mul(res, base); // 如果指数是奇数,结果乘以当前底数
}
mul(base, base); // 底数平方
p /= 2; // 指数减半
}
// 执行减法:2^p - 1
res[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
较小值,便于手动验证。
总结
本题考察了大数运算和快速幂算法的应用。通过合理的算法设计和优化,可以在规定时间内完成计算。关键点包括:
-
利用数学公式直接计算位数,避免大数运算
-
使用快速幂算法高效计算大数幂
-
只保留必要的位数,减少计算量
-
合理的数据结构设计,便于处理进位和借位
815

被折叠的 条评论
为什么被折叠?



