高精度运算

在 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;
}

例题:P2142 高精度减法

计算两个正整数 \(a\)\(b \ (0 \lt a,b \le 10^{10086})\) 的差,即求 \(a-b\) 的值。需要特别注意的是,如果 \(a \lt b\),结果会是负数,此时需要在输出的答案前加上负号 -

解题思路

数据的位数高达 \(10086\) 位,这远远超出了 C++ 中任何内置基本数据类型的表示范围。因此,必须使用高精度算法来解决。

高精度减法模拟小学学习的竖式减法,分为几个核心步骤:

  1. 大小比较与负号处理:默认情况下,用大数减去小数。如果输入的 \(a \lt b\),需要交换两个数,用 \(b-a\) 计算绝对值,并在最后输出答案时先打印一个负号 -。(比较方法:先比较长度即位数,位数多的数字更大;若位数相同,则可以使用字符串比较函数 strcmp 逐位比较)。
  2. 逆序存储:读入字符串后,将其转换成数字并反转存储到数组中。让个位对应数组下标 0,十位对应下标 1,……,这样做的目的是为了让对齐低位(个位)的操作更加方便,也利于后续的进位/借位处理。
  3. 按位按列相减(借位):从低位到高位遍历,计算 c[i] += a[i] - b[i](这里用 += 是因为高位可能因为前一位借位而减了 1)。如果当前位的结果小于 0,说明不够减,需要向高位借位(当前位加 10,下一位减 1,表示借走了一个 10)。
  4. 去除前导零:相减后,高位可能会出现多余的 0(例如 \(123-120=003\))。需要从最高位开始检查,把多余的 0 砍掉,但要保证如果结果本身就是 0,则保留一个 0
  5. 逆序输出:如果有负号标记则先输出 -,然后从高位到低位(即逆序遍历数组)打印结果。
参考代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10105;
char s1[N], s2[N];
int a[N], b[N], c[N];
// 比较两个大数的大小,如果 s1 代表的数小于 s2,返回 true
bool cmp_less(int l1, int l2) {
    // 1. 如果长度不相等,长度短的数字小
    if (l1 != l2) return l1 < l2;
    // 2. 如果长度相等,通过字典序比较大小
    return strcmp(s1, s2) < 0;
}
int main()
{
    scanf("%s%s", s1, s2);
    int l1 = strlen(s1), l2 = strlen(s2);
    bool neg = false; // 标记最终结果是否为负数
    char *num1 = s1, *num2 = s2;
    // 如果第一个数小于第二个数,交换指针,使其变为大数减小数
    if (cmp_less(l1, l2)) {
        num1 = s2; num2 = s1;
        swap(l1, l2); // 保持 l1 是大数的长度,l2 是小数的长度
        neg = true; // 结果必然为负
    }
    // 将字符转换为数字,并【逆序】存入数组中(个位在下标 0)
    for (int i = 0; i < l1; i++) a[i] = num1[l1 - i - 1] - '0';
    for (int i = 0; i < l2; i++) b[i] = num2[l2 - i - 1] - '0';
    // 模拟竖式减法
    for (int i = 0; i < l1; i++) {
        c[i] += a[i] - b[i]; // 当前位作差(考虑前面借过来的负值)
        if (c[i] < 0) { // 如果不够减,向高位借一当十
            c[i] += 10; 
            c[i + 1]--; // 高位减 1
        }
    }
    // 去掉前导 0,例如 003 变成 3
    // 且当 l1 == 1 时停止,防止把结果本身就是 0 的情况也删掉
    while (l1 > 1 && c[l1 - 1] == 0) l1--;
    // 输出结果
    if (neg) printf("-"); // 如果是负数,先打印负号
    for (int i = l1 - 1; i >= 0; i--) printf("%d", c[i]); // 逆序打印输出
    printf("\n");
    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;
}

例题:P1480 A/B Problem(高精度除法Ⅰ)

计算两个非负整数 \(a \ (0 \le a \le 10^{5000})\)\(b \ (1 \le b \le 10^9)\) 的商(向下取整)。

解题思路

由于被除数 \(a\) 极大,使用字符串来读入。而除数 \(b\) 在内置整型范围内,因此这道题属于高精度数除以低精度数的问题。

可以模拟人类手动做长除法(竖式除法)的过程:

  1. 从高位到低位逐位处理被除数。
  2. 维护一个余数 \(r\),每次处理新的一位时,将上一轮的余数乘以 \(10\),再加上当前位的数字,组成当前的被除数。
  3. 当前位的商即为 \(\lfloor r/b \rfloor\),余数更新为 \(r \bmod b\)
  4. 最终输出答案时,需要注意抹除高位产生的前导零,但如果最终答案本身就是 0,则需要保留一个 0
参考代码
#include <cstdio>
#include <cstring>
using ll = long long;
const int N = 5005;
char s[N];
int ans[N];
int main()
{
    ll b;
    scanf("%s%lld", s, &b);
    int len = strlen(s);
    ll r = 0;
    for (int i = 0; i < len; i++) {
        r = r * 10 + (s[i] - '0');
        ans[i] = r / b;
        r %= b;
    }
    // 去掉前导 0,但如果结果为 0,保留最后一个 0
    int st = 0;
    while (st < len - 1 && ans[st] == 0) st++;
    for (int i = st; i < len; i++) printf("%d", ans[i]);
    printf("\n");
    return 0;
}
posted @ 2025-12-24 16:01  RonChen  阅读(33)  评论(0)    收藏  举报