CC++ & Algorithm

模运算的运算规则

较难8
语言版本:通用
概述:用分糖果的例子引出模运算的加法、减法、乘法规则,推导公式并展示C++和Python中如何利用这些规则简化大规模运算。

模运算的运算规则:让大数变小,轻松搞定取模

你有没有遇到过这样的情况:考试得了95分,老师说按10分一档计分,你的得分是95÷10的余数5?或者零花钱是23元,想平均分给7个朋友,最后自己剩下23÷7的余数2元?这些生活中的“余数”问题,在编程中叫做 模运算(modulo operation),用符号 % 表示(比如 23 % 7 = 2)。模运算有很多好用的规则,可以让大数字变小,避免程序报错或速度变慢。今天我们就来学会这些规则,并看看在C++和Python里怎么用。

一、模运算的三大规则:加、减、乘

1. 加法规则:先取余,再相加,最后取余

生活例子:小明有17颗糖,小红有25颗糖,他们要把糖分给9个小朋友,每人分到的糖要一样多,剩下的糖自己留着。可以先算总糖数:17+25=42,42÷9余6。也可以先算每个人各自剩下的:17÷9余8,25÷9余7,然后把余数加起来8+7=15,15÷9余6。结果一样!这就是模运算的加法规则:

(a + b) % m = ((a % m) + (b % m)) % m

公式说明:不管先加再取余,还是先取余再加再取余,结果都一样。这个规则让我们可以把大数变小再计算,避免数字太大出问题。

代码验证(C++):

#include <iostream>
using namespace std;

int main() {
    int m = 9;                     // 人数(模数)
    int a = 17, b = 25;            // 两人的糖果数
    // 直接计算
    int direct = (a + b) % m;      // 先加再取余
    // 使用规则
    int mod_way = ((a % m) + (b % m)) % m;  // 先取余再加再取余
    cout << "直接加法取余: " << direct << endl;
    cout << "规则加法取余: " << mod_way << endl;
    return 0;
}

2. 减法规则:小心负数!记得加模数

生活例子:你有50元零花钱,花了23元买文具,还剩多少?如果按7天一个星期算,每天花平均费,最后剩下多少钱可以自由花?50 - 23 = 27,27 % 7 = 6(因为27÷7余6)。但是如果用规则:50%7=1,23%7=2,1-2 = -1,-1%7在数学上通常是6(因为-1+7=6),但在C++里直接算会得到负数(-1),所以要手动加模数。

规则公式:

(a - b) % m = ((a % m) - (b % m) + m) % m

为什么加m:因为 a%m 和 b%m 都在0到m-1之间,但相减可能变成负数。加上m再取余,就能保证结果是非负的余数。这个“加m”非常重要,是新手最容易犯的错误!

代码验证(Python,自动处理负数):

m = 7
a = 50
b = 23
direct = (a - b) % m
mod_way = ((a % m) - (b % m) + m) % m
print(f"直接减法取余: {direct}")
print(f"规则减法取余: {mod_way}")
# 输出:6 和 6,结果一致

3. 乘法规则:先取余,再相乘,最后取余

生活例子:你有17颗糖,给每个小朋友分3颗糖,你一共有多少个小朋友?哦不,是17×3=51颗糖,分给9个小朋友,最后剩51%9=6颗。用规则:17%9=8,3%9=3,8×3=24,24%9=6,一样!

(a × b) % m = ((a % m) × (b % m)) % m

这个规则在编程中特别有用,因为两个大数直接乘可能溢出(比如C++里int最大约21亿,两个10亿相乘就超了)。先取模让数字变小,乘完再取模就不会溢出。

举例:你要算 123456789 × 987654321 后除以 1000000007 的余数。在C++里,直接乘会超出64位整数范围(约10^18),但先取模再乘,每个数都小于1000000007,乘积小于10^18,64位整数刚好能装下。

代码验证(C++,注意使用 long long):

#include <iostream>
using namespace std;

int main() {
    const long long MOD = 1000000007;  // 常用模数
    long long a = 123456789;           // 第一个大数
    long long b = 987654321;           // 第二个大数
    // 直接乘可能溢出?这里用 long long 试试
    long long direct = (a * b) % MOD;  // 如果 a*b 超了 long long 范围就危险
    // 规则乘:先取模
    long long mod_way = ((a % MOD) * (b % MOD)) % MOD;
    cout << "直接乘法取模: " << direct << endl;
    cout << "规则乘法取模: " << mod_way << endl;
    return 0;
}

注意:虽然这里用了 long long,但两个10^9的数乘起来是10^18,long long上限约9.2×10^18,刚好安全。如果模数更大(比如10^9+7),乘积可能接近10^18,还勉强安全。但更保险的做法是用 __int128(C++不支持标准)或 Python 的无限整数。

4. 幂运算规则:重复乘法取模

生活例子:你想知道 2^10 除以 7 的余数。2^10=1024,1024%7=2。用规则:先取模 2%7=2,然后算 2^10 % 7 = (2^10) % 7 = 1024%7=2,这其实没简化。但真正的技巧是:要算 a^k % m,可以每次乘 a 后立刻取模,这样中间数字不会变大。

公式:

a^k % m = (a % m)^k % m

更厉害的是 快速幂:用二进制分解指数,比如算 3^100 % 7,把100写成二进制1100100,然后循环乘并取模,只做 log2(100)≈7次乘法。这能极大提升效率。

代码示例(快速幂):见下文完整示例。

二、数学推导(理解原理,不怕忘记)

这些规则为什么成立?用代数学解释:设 a 和 b 是整数,m 是正整数。将 a 写成 a = m × q1 + r1,其中 0 ≤ r1 < m。同样 b = m × q2 + r2。

  • 加法:a + b = m×(q1+q2) + (r1+r2),所以 (a+b)%m = (r1+r2)%m。
  • 减法:类似,但 r1 - r2 可能为负,所以加 m 调正。
  • 乘法:a×b = m×(m×q1×q2 + q1×r2 + q2×r1) + r1×r2,所以 (a×b)%m = (r1×r2)%m。

这些规则的实质是:余数只和余数有关,与商无关

三、新手最容易犯的错误

  1. 忘记减法要加模数:在C++中,(a - b) % m 可能是负数,直接使用会导致错误结果。正确的做法是 ((a % m) - (b % m) + m) % m
  2. 先运算再取模导致溢出:比如计算 (a * b) % m,如果 a 和 b 都很大(比如接近10^18),直接乘会溢出。应该先取模再乘。
  3. 使用 int 类型存储大数:C++中 int 最大约21亿,如果 a 和 b 是10^9,a*b 就溢出了。要用 long long 或更大类型。
  4. 混淆除法和模运算:除法没有简单的模规则,不能直接写成 (a / b) % m 等于 ((a%m) / (b%m)) % m。正确的除法需要模逆元,那是进阶内容。
  5. 认为 Python 不需要考虑:Python 虽然整数无限大,但大整数运算很慢。使用模运算规则可以加快速度(尤其是幂运算),而且能让代码更清晰。

四、完整可运行的 C++ 示例(含快速幂)

下面是一个完整的程序,演示加法、减法、乘法、幂运算的各种规则,并对比直接计算和规则计算的结果。

#include <iostream>
using namespace std;

// 快速幂:计算 base^exp % mod
long long fast_pow(long long base, long long exp, long long mod) {
    long long result = 1;          // 结果初始为1
    base = base % mod;             // 先取模,防止第一步溢出
    while (exp > 0) {              // 当指数大于0时循环
        if (exp & 1) {             // 如果当前指数二进制最低位是1
            result = (result * base) % mod;  // 乘上当前的base
        }
        base = (base * base) % mod;          // base自乘(平方)
        exp >>= 1;                 // 指数右移一位(除以2)
    }
    return result;
}

int main() {
    const long long MOD = 1000000007;  // 模数,常用的大质数
    long long a, b;                    // 输入的两个数
    
    cout << "请输入两个大整数 a 和 b(例如 123456789 987654321):";
    cin >> a >> b;
    
    // === 加法测试 ===
    long long add_direct = (a + b) % MOD;          // 直接加法取模
    long long add_mod = ((a % MOD) + (b % MOD)) % MOD;  // 规则加法
    cout << "直接加法取模: " << add_direct << endl;
    cout << "规则加法取模: " << add_mod << endl;
    cout << "结果相等: " << (add_direct == add_mod) << endl;
    
    // === 减法测试(注意负数处理) ===
    long long sub_direct = (a - b) % MOD;          // 可能为负
    if (sub_direct < 0) sub_direct += MOD;         // 手动调整非负
    long long sub_mod = ((a % MOD) - (b % MOD) + MOD) % MOD;  // 规则
    cout << "直接减法取模: " << sub_direct << endl;
    cout << "规则减法取模: " << sub_mod << endl;
    cout << "结果相等: " << (sub_direct == sub_mod) << endl;
    
    // === 乘法测试 ===
    // 注意:a和b可能很大,直接乘可能溢出 long long,这里假设安全
    long long mul_direct = (a * b) % MOD;          // 直接乘法
    long long mul_mod = ((a % MOD) * (b % MOD)) % MOD;  // 规则
    cout << "直接乘法取模: " << mul_direct << endl;
    cout << "规则乘法取模: " << mul_mod << endl;
    cout << "结果相等: " << (mul_direct == mul_mod) << endl;
    
    // === 幂运算测试(快速幂) ===
    long long exponent;                            // 指数
    cout << "请输入指数 exponent(比如 100):";
    cin >> exponent;
    long long pow_direct = 1;                     // 直接计算 a^exponent 会溢出,这里仅演示规则,不直接算
    // 用快速幂按规则计算
    long long pow_mod = fast_pow(a, exponent, MOD);
    cout << a << " 的 " << exponent << " 次方模 " << MOD << " = " << pow_mod << endl;
    
    return 0;
}

运行示例

请输入两个大整数 a 和 b(例如 123456789 987654321):123456789 987654321
直接加法取模: 111111109
规则加法取模: 111111109
结果相等: 1
直接减法取模: 864197531
规则减法取模: 864197531
结果相等: 1
直接乘法取模: 0
规则乘法取模: 0
结果相等: 1
请输入指数 exponent(比如 100):50
123456789 的 50 次方模 1000000007 = 926752979

(注意:乘法结果0是因为 123456789×987654321 可以被 1000000007 整除,实际验证。)

五、完整可运行的 Python 示例

Python 的整数无限大,但使用模规则可以让代码更高效。

MOD = 1000000007  # 模数

a = int(input("请输入第一个大整数 a:"))  # 输入a
b = int(input("请输入第二个大整数 b:"))  # 输入b

# 加法
add_direct = (a + b) % MOD
add_mod = ((a % MOD) + (b % MOD)) % MOD
print(f"直接加法取模: {add_direct}")
print(f"规则加法取模: {add_mod}")

# 减法(Python % 已经自动处理负数)
sub_direct = (a - b) % MOD
sub_mod = ((a % MOD) - (b % MOD) + MOD) % MOD
print(f"直接减法取模: {sub_direct}")
print(f"规则减法取模: {sub_mod}")

# 乘法
mul_direct = (a * b) % MOD
mul_mod = ((a % MOD) * (b % MOD)) % MOD
print(f"直接乘法取模: {mul_direct}")
print(f"规则乘法取模: {mul_mod}")

# 快速幂(用内置 pow 函数)
exponent = int(input("请输入指数 exponent:"))
pow_mod = pow(a, exponent, MOD)  # Python 内置快速幂取模
print(f"{a}{exponent} 次方模 {MOD} = {pow_mod}")

六、相关知识点指引

学会了加减乘的模运算规则,下一步可以学习:

  • 模逆元:解决除法取模的问题(类似分数的倒数)。
  • 快速幂:高效计算大指数取模,在密码学中常用。
  • 欧拉定理/费马小定理:用于求解模幂和模逆元。
  • 同余方程:比如解 3x ≡ 1 (mod 7),寻找 x 的值。
  • 大数取模:对于超出整数范围的大数(比如100位数字),可以边读入边取模。

这些是编程竞赛和数学编程的基础,掌握好模运算规则,就能轻松处理很多取模问题。现在你可以试试用这些规则,计算 (123456789^123456789) % 1000000007,看看你的程序能不能快速算出结果!

例题精讲

1单选题

在模运算中,计算 (123456789 + 987654321) mod 1000000007 时,为了避免中间结果溢出,下列哪种做法是正确的?

A先计算两个数的和再取模
B先分别取模再相加,然后取模
C直接计算和再取模,因为整数加法不会溢出
D先分别取模再相加,不需要再取模
2单选题

已知 a mod m = 5, b mod m = 8,且 m=12,那么 (a - b) mod m 的结果是?

A-3
B9
C3
D15
3判断题

对于任意整数 a, b 和正整数 m,都有 (a + b) mod m = (a mod m) + (b mod m)。

4填空题
// 计算 (a + b) % m,其中 a, b 可能很大(超过int范围),m为正整数。
// 在C++中,为了防止加法溢出,可以写:
long long a, b, m;
long long result = ( (a % m) + (b % m) ) % m;
// 填空:请补充完整表达式,使得 result = (a + b) % m 且安全。
long long safe_add_mod(long long a, long long b, long long m) {
    return ( (a % m) + (b % m) ) % ___;
}
5填空题
// 计算 (a * b) % m,其中 a, b 可能很大,使用模乘法规则防止溢出。
// 下面使用循环累加(俄罗斯农民乘法)实现模乘:
long long mul_mod(long long a, long long b, long long m) {
    long long res = 0;
    a %= m;
    while (b > 0) {
        if (b & 1) {
            res = (res + a) % m;
        }
        a = (a * 2) % ___;
        b >>= 1;
    }
    return res;
}
// 填空:在循环中,a 每次翻倍后需要取模,以防止溢出。请填入合适的模数。