洛谷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)。

难点分析

  1. 大数计算:当 p 很大时(题目中 p 最大为3100000),2^p 的值极其巨大,无法用常规数据类型存储。

  2. 高效计算:直接进行 p 次乘法会超时,需要使用快速幂算法。

  3. 位数计算:不需要实际计算整个数就能确定位数。

  4. 存储优化:只需存储最后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位数字,便于理解)。

核心算法

  1. 计算位数:直接使用公式 d = floor(p · log10(2)) + 1

  2. 计算最后500位:使用快速幂算法,只保留最后500位

  3. 减法处理:计算 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;
}

主函数逻辑清晰:

  1. 读入指数p

  2. 计算并输出位数

  3. 调用快速幂函数计算并输出最后500位

算法优化分析

时间复杂度

  • 位数计算:O(1)

  • 快速幂算法:O(log p)

  • 每次乘法:O(500^2) = O(250000)

总时间复杂度为 O(250000 × log p),对于p ≤ 3100000,log p ≈ 22,总操作数约550万次,在合理范围内。

空间复杂度

使用固定大小的数组,空间复杂度为O(1)。

优化技巧

  1. 只保留必要位数:只计算和存储最后500位,大大减少计算量

  2. 快速幂算法:将指数运算从线性时间优化到对数时间

  3. 循环优化:在乘法中通过条件判断减少不必要的计算

常见问题解答

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

较小值,便于手动验证。

总结

本题考察了大数运算和快速幂算法的应用。通过合理的算法设计和优化,可以在规定时间内完成计算。关键点包括:

  1. 利用数学公式直接计算位数,避免大数运算

  2. 使用快速幂算法高效计算大数幂

  3. 只保留必要的位数,减少计算量

  4. 合理的数据结构设计,便于处理进位和借位

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值