高精度运算

在 C++ 中,常用的整型数据类型都有其取值范围限制:

  • int:通常为 32 位,最大值约为 \(2 \times 10^9\)
  • long long:通常为 64 位,最大值约为 \(9 \times 10^{18}\)

然而,有时候需要处理的数字会超过这个范围。此时,任何内置的整型变量都无法存储。因此,需要使用高精度算法,通过数组来模拟大整数的存储和运算。

例题:P1601 A+B Problem(高精)

高精度加法的核心思想是模拟小学数学中的竖式加法

由于数字太大,通常使用字符串来读入数据。读入后,为了方便计算(让个位对齐),将字符串倒序存储到一个整型数组中。倒序意味着数组下标 0 存储个位,下标 1 存储十位,下标 2 存储百位……以此类推。

为什么倒序?

因为加法运算可能会产生进位,进位会影响高一位的数值。如果数组下标从小到大对应个位到高位,进位处理起来非常方便。如果正序存储,处理进位时需要向数组下标更小的方向操作,且可能涉及到数组头部插入(如果最高位进位),效率较低且实现麻烦。

假设有两个大整数 \(A\)\(B\),分别存储在两个数组中,需要逐位计算结果 \(C_i = A_i + B_i + \text{上一位的进位}\)。而当前位的结果为 \(C_i \bmod 10\),新的进位为 \(\left\lfloor \dfrac{C_i}{10} \right\rfloor\)

参考代码
#include <cstdio>
#include <cstring>
const int N = 505;
char s1[N], s2[N]; // 存储输入的两个大整数(字符串形式)
int a[N], b[N], c[N]; // a和b分别存储两个加数的每一位,c存储结果
int main()
{
    // 读入两个大整数
    scanf("%s%s", s1, s2);
    int len1 = strlen(s1), len2 = strlen(s2);
    
    // 将第一个字符串倒序转换为数字数组,a[0]存储个位,a[1]存储十位...
    // 这样做是为了方便对齐相加,个位对个位
    for (int i = 0; i < len1; i++) {
        a[i] = s1[len1 - i - 1] - '0';
    }
    // 将第二个字符串倒序转换为数字数组
    for (int i = 0; i < len2; i++) {
        b[i] = s2[len2 - i - 1] - '0';
    }
    
    int t = 0, last = 0; // t用于存储进位,last用于记录结果的最高位位置
    // 高精度加法核心逻辑
    // 循环条件:只要 s1 或 s2 还有位未处理,或者还有进位 t,就继续循环
    for (int i = 0; i < len1 || i < len2 || t > 0; i++) {
        if (i < len1) t += a[i]; // 加上 s1 当前位的数值(如果还没超出s1长度)
        if (i < len2) t += b[i]; // 加上 s2 当前位的数值(如果还没超出s2长度)
        c[i] = t % 10;           // 存储当前位的结果(当前和对 10 取模)
        t /= 10;                 // 计算下一位的进位
        last = i;                // 更新结果的最高位下标
    }
    
    // 倒序输出结果,从最高位打印到个位
    for (int i = last; i >= 0; i--) {
        printf("%d", c[i]);
    }
    return 0;
}

例题:P1303 A*B Problem

与加法类似,当两个整数非常大时,普通的 long long 无法容纳它们的乘积。

高精度乘法的核心思想同样是模拟竖式计算,但与加法略有不同,通常采用“先按位乘并累加,最后统一处理进位”的方法。

假设有两个大整数 \(A\)\(B\)\(A_0\) 为个位),根据乘法规律,\(A\) 的第 \(i\) 位与 \(B\) 的第 \(j\) 位相乘,结果应该累加到积 \(C\) 的第 \(i+j\) 位上

参考代码
#include <cstdio>
#include <cstring>
// s1, s2 存储输入字符串,a, b 存储转换后的整数数组
// c 存储结果,长度为两个加数长度之和
const int N = 2005;
char s1[N], s2[N];
int a[N], b[N], c[N * 2]; // c 需要开 N*2 大小
int main()
{
    // 读入两个大整数
    scanf("%s%s", s1, s2);
    int n = strlen(s1), m = strlen(s2);
    
    // 将字符串倒序转换为数字数组,为了方便对齐计算
    // a[0] 是 s1 的个位,a[1] 是十位...
    for (int i = 0; i < n; i++) a[i] = s1[n - i - 1] - '0';
    for (int i = 0; i < m; i++) b[i] = s2[m - i - 1] - '0';
    
    // 核心乘法逻辑:模拟竖式乘法
    // a 的第 i 位和 b 的第 j 位相乘,结果累加到 c 的第 i+j 位
    // 此时 c[i+j] 可能会超过 10,暂时不处理进位,最后统一处理
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            c[i + j] += a[i] * b[j];
            
    // 统一处理进位
    // 从低位到高位遍历结果数组
    // 遍历范围到 n + m - 2 即可,因为最高位进位会自动处理到 n + m - 1
    for (int i = 0; i < n + m - 1; i++) {
        if (c[i] >= 10) {
            c[i + 1] += c[i] / 10; // 进位累加到下一位
            c[i] %= 10;            // 当前位保留个位数
        }
    }
    
    // 确定结果的有效长度,去除前导零
    // 初始认为结果长度可能为 n + m(或 n + m - 1),从高位向下寻找第一个非零位
    // 注意 len > 0 的条件是为了保证结果为 0 时能输出 "0"(此时 len 会减到 0)
    int len = n + m - 1;
    while (len > 0 && c[len] == 0) len--;
    
    // 倒序输出结果
    for (int i = len; i >= 0; i--) printf("%d", c[i]);
    return 0;
}

例题:P1009 [NOIP 1998 普及组] 阶乘之和

在前面的讲解中,使用数组来处理高精度加法和乘法,这种方式对于单次运算来说已经足够。但是,在一些复杂的题目中,需要频繁地进行高精度数的初始化、乘法、加法,甚至可能会涉及多个高精度数的交互。

如果每次运算都重写一遍数组操作逻辑,代码会变得冗长、难以维护且容易出错。此时,将高精度整数封装成一个结构体,可以极大地简化代码结构,使其像使用内置类型一样方便,这样一来运算逻辑都可以封装为函数。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>

using namespace std;

// 高精度整数结构体,使用 vector 存储每一位数字(逆序存储,d[0] 为个位)
struct BigInt {
    vector<int> d;
    
    BigInt() {}
    // 将普通整数转换为高精度整数
    BigInt(int n) {
        if (n == 0) d.push_back(0);
        while (n > 0) {
            d.push_back(n % 10);
            n /= 10;
        }
    }
    
    // 输出高精度整数
    void print() const {
        if (d.empty()) {
            printf("0");
            return;
        }
        // 逆序输出,从最高位开始
        for (int i = d.size() - 1; i >= 0; i--) {
            printf("%d", d[i]);
        }
    }
};

// 高精度整数乘以低精度整数
BigInt multiply(const BigInt& a, int b) {
    BigInt c;
    int carry = 0, n = a.d.size();
    // 模拟竖式乘法,逐位相乘并处理进位
    for (int i = 0; i < n || carry > 0; i++) {
        int cur = carry + (i < n ? a.d[i] : 0) * b;
        c.d.push_back(cur % 10);
        carry = cur / 10;
    }
    // 去除多余的前导零
    while (c.d.size() > 1 && c.d.back() == 0) c.d.pop_back();
    return c;
}

// 高精度整数加法
BigInt add(const BigInt& a, const BigInt& b) {
    BigInt c;
    int carry = 0, n = a.d.size(), m = b.d.size();
    // 模拟竖式加法,逐位相加并处理进位
    for (int i = 0; i < max(n, m) || carry > 0; i++) {
        int sum = carry + (i < n ? a.d[i] : 0) + (i < m ? b.d[i] : 0);
        c.d.push_back(sum % 10);
        carry = sum / 10;
    }
    return c;
}

int main() {
    int n;
    // 读取 n
    if (scanf("%d", &n) != 1) return 0;
    
    // sum 用于存储最终的阶乘之和,f 用于存储当前的阶乘 i!
    BigInt sum(0), f(1);
    
    // 循环计算 1! 到 n! 并累加
    for (int i = 1; i <= n; i++) {
        // 计算 i! = (i-1)! * i
        f = multiply(f, i);
        // 累加到 sum
        sum = add(sum, f);
    }
    
    // 输出结果
    sum.print();
    printf("\n");
    
    return 0;
}

如果不进行封装,需要在循环内部手动处理数组、长度变量、临时数组等,代码复杂度会大大增加。

习题:P1591 阶乘数码

解题思路

\(1000!\) 大约有两千多位数字,需要使用高精度算法来模拟计算过程。

参考代码
#include <cstdio>
#include <vector>
using namespace std;

// 高精度整数结构体,使用 vector 存储每一位数字(逆序存储,d[0] 为个位)
struct BigInt {
    vector<int> d;
    
    // 默认构造函数
    BigInt() {}
    
    // 从 int 初始化高精度整数
    BigInt(int n) {
        if (n == 0) d.push_back(0);
        while (n > 0) {
            d.push_back(n % 10);
            n /= 10;
        }
    }
    
    // 高精度整数乘以低精度整数 x
    // 直接在原对象上修改,不返回新对象
    void multiply(int x) {
        int carry = 0, n = d.size();
        // 逐位相乘并处理进位
        for (int i = 0; i < n; i++) {
            int cur = d[i] * x + carry;
            d[i] = cur % 10; // 当前位保留个位
            carry = cur / 10; // 计算新的进位
        }
        // 处理剩余的进位,可能扩展多位
        while (carry > 0) {
            d.push_back(carry % 10);
            carry /= 10;
        }
    }
    
    // 统计数码 a 在当前高精度整数中出现的次数
    int count(int a) {
        int cnt = 0;
        for (int digit : d) {
            if (digit == a) cnt++;
        }
        return cnt;
    }
};

void solve() {
    int n, a; 
    scanf("%d%d", &n, &a);
    
    // 初始化阶乘结果为 1
    BigInt f(1);
    
    // 计算 n! = 1 * 2 * ... * n
    for (int i = 2; i <= n; i++) f.multiply(i);
    
    // 统计并输出数码 a 出现的次数
    printf("%d\n", f.count(a));
}

int main()
{
    int t; 
    scanf("%d", &t);
    // 处理 t 组数据
    while (t--) solve();
    return 0;
}
posted @ 2025-12-24 16:01  RonChen  阅读(10)  评论(0)    收藏  举报