模运算的运算规则
较难8模运算的运算规则:让大数变小,轻松搞定取模
你有没有遇到过这样的情况:考试得了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。
这些规则的实质是:余数只和余数有关,与商无关。
三、新手最容易犯的错误
- 忘记减法要加模数:在C++中,
(a - b) % m可能是负数,直接使用会导致错误结果。正确的做法是((a % m) - (b % m) + m) % m。 - 先运算再取模导致溢出:比如计算
(a * b) % m,如果 a 和 b 都很大(比如接近10^18),直接乘会溢出。应该先取模再乘。 - 使用 int 类型存储大数:C++中 int 最大约21亿,如果 a 和 b 是10^9,a*b 就溢出了。要用 long long 或更大类型。
- 混淆除法和模运算:除法没有简单的模规则,不能直接写成
(a / b) % m等于((a%m) / (b%m)) % m。正确的除法需要模逆元,那是进阶内容。 - 认为 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,看看你的程序能不能快速算出结果!
例题精讲
在模运算中,计算 (123456789 + 987654321) mod 1000000007 时,为了避免中间结果溢出,下列哪种做法是正确的?
已知 a mod m = 5, b mod m = 8,且 m=12,那么 (a - b) mod m 的结果是?
对于任意整数 a, b 和正整数 m,都有 (a + b) mod m = (a mod m) + (b mod m)。
// 计算 (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) ) % ___;
}// 计算 (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 每次翻倍后需要取模,以防止溢出。请填入合适的模数。