第 2 讲:模运算 —— 密码学的“循环世界”

 

 导语:
如果说 XOR 是位运算的魔法,那么模运算(Modular Arithmetic)就是整个现代公钥密码学的数学地基。从保护你银行卡密码的 RSA,到比特币底层的椭圆曲线(ECC),所有的算法都在一个“有边界的数字圈”里跳舞。在 CSP/NOI 竞赛中,模运算不仅是数论题的常客,更是防止大数相乘导致数据溢出的“救命稻草”。今天,我们将打破常规,带你看看这个循环世界里的杀手级武器。

模块一:直觉建立 —— 钟表数学与“模环”

在普通的代数世界里,数字轴是向两端无限延伸的直线;但在模运算中,数字轴被首尾相接,卷成了一个。你可以把它想象成一个极客版的钟表。

核心定义:

a≡b(modn)

读作“a 与 b 对模 n 同余”。直觉上,这意味着 a 和 b 处在钟表上的同一个刻度(它们除以 n 的余数绝对相等)。

五个必须融入肌肉记忆的性质(CSP 避坑指南):

  1. 加减法(无损传递): (a±b)(mod n)=(a(mod n)±b(mod n))(mod n)

  2. 乘法(无损传递): (a×b)(mod n)=(a(mod n)×b(mod n))(mod n)

  3. 幂运算(安全降级): ab(modn)=(a(modn))b(modn)

  4. ⚠️ 除法陷阱(毁灭边缘): (a/b)(mod n)≠(a(mod n)/b(mod n))(mod n)极客铁律:在模世界里,根本不存在传统的除法,我们只用“乘以逆元”来代替除法!

  5. ⚠️ C++ 负数求模陷阱: 在 C++ 中,-5 % 3 的结果是 -2,但数学上余数必须是正数(刻度 1)。工程上处理减法取模的标准绝对安全写法是:
    (a - b % n + n) % n

模块二:CSP 核心算法实战 —— 快速幂 (Fast Power)

场景: 在密码学(如 RSA)或竞赛题中,我们经常需要计算超级大数的幂次,例如计算 me(mod n),其中指数 e 可能是 1018 级别。如果写个 for 循环,即使用目前最强的超级计算机也会算到宇宙毁灭。我们需要分治思维

⚔️ 算法推导:二进制与倍增的艺术

不要去想循环 e 次,我们换个视角,把指数看作二进制

假设要算 a13。把 13 写成二进制是 11012,也就是 13=8+4+1
所以:

a13=a8×a4×a1

我们只需要不断把底数平方(a→a2→a4→a8),碰到二进制位是 1 的时候,乘入结果集中即可!时间复杂度直接从 O(N) 降维打击到 O(log⁡N)

C++ 极客级标准模板:

cpp
#include <iostream>

// 计算 (a^b) % mod
long long fast_pow(long long a, long long b, long long mod) {
    long long res = 1;
    a %= mod; // 初始降维,防止 a 本身过大
    
    while (b > 0) {
        // 如果当前二进制最低位是 1,说明需要把当前的 a 乘入结果
        if (b & 1) {
            // 使用 __int128 防止 a * res 在 long long 范围内溢出 (10^18 * 10^18 = 10^36)
            res = (long long)((__int128)res * a % mod); 
        }
        // 底数进行倍增升级:a^1 -> a^2 -> a^4 -> a^8 ...
        a = (long long)((__int128)a * a % mod);
        // 指数右移,处理下一个二进制位
        b >>= 1; 
    }
    return res;
}

int main() {
    // RSA 加密模拟:明文 123,公钥指数 17,模数 3233
    std::cout << "加密结果: " << fast_pow(123, 17, 3233) << std::endl; 
    return 0;
}

  

模块三:深度突破 —— 模逆元与扩展欧几里得

我们前面说过,模世界没有除法。如果你要计算 (a/b)(mod n),你必须找到 b 的模逆元 x。
定义为:如果 bx≡1(modn),我们称 x 是 b 的模逆元,记作 b−1

  • 物理直觉: 在模环中,乘以 x 的效果,等同于除以 b。

  • 存在条件: gcd⁡(b,n)=1(b 与 n 必须互质,否则在环里转不到刻度 1)。

⚔️ 求解武器:扩展欧几里得 (exGCD)

数学家裴蜀告诉我们:bx≡1(modn)等价于存在一个整数 y,使得:

bx+ny=1

我们不仅要求最大公约数,还要把系数 x 和 y 顺手求出来。

C++ 核心代码实现:

cpp
#include <iostream>

// 扩展欧几里得算法:求 gcd(a,b) 的同时解出 ax + by = gcd(a,b)
void exgcd(long long a, long long b, long long &x, long long &y) {
    if (b == 0) { 
        x = 1; y = 0; 
        return; 
    }
    // 递归下沉
    exgcd(b, a % b, y, x);
    // 回溯推导
    y -= (a / b) * x;
}

// 封装模逆元函数
long long modInverse(long long a, long long m) {
    long long x, y;
    exgcd(a, m, x, y);
    // 因为 x 可能是负数,用极客铁律处理负数取模
    return (x % m + m) % m; 
}

int main() {
    // 求 3 在模 11 下的逆元 (因为 3 * 4 = 12 ≡ 1 mod 11,所以逆元是 4)
    std::cout << "3 mod 11 的逆元是: " << modInverse(3, 11) << std::endl; 
    return 0;
}

  

模块四:密码学视角 —— 离散对数与“单向陷门”

为什么模运算能保护人类互联网的机密?因为它创造了数学上的“单向陷门函数” (One-way Trapdoor Function)

  1. 正向极快(快速幂): 给定底数 g、指数 x、模数 p,求 y=gx(mod p),只需要毫秒级。

  2. 反向极难(离散对数难题 DLP): 给定 g, y, p,想要求出指数 x。在这座没有规律的模数迷宫里,目前全人类都没有找到除了“暴力尝试”以外的绝对高效多项式算法(当量子计算机和 Shor 算法普及前)。

🧪 迷你真实实验:Diffie-Hellman (DH) 密钥交换模型
假设你在星巴克连着开放 Wi-Fi(有黑客在监听所有数据),你如何与银行协商出一个除了你们俩谁都不知道的密码?

  1. 公开参数: 你和银行明文约定一个生成元 g=5,一个大素数模数 p=23

  2. 你(Alice): 悄悄在心里选一个私钥 a=6。算出 A=56(mod 23)=8。把 8 明文发给银行。

  3. 银行(Bob): 悄悄选一个私钥 b=15。算出 B=515(mod 23)=19。把 19 明文发给你。

  4. 奇迹时刻:

  • 你拿到 19,计算 (19)a(mod 23)=196(mod 23)=2

  • 银行拿到 8,计算 (8)b(mod 23)=815(mod 23)=2

  • 数学底层逻辑: (gb)a(mod p)≡(ga)b(modp)≡gab(mod p)

  1. 黑客的绝望: 黑客截获了 g=5,p=23,A=8,B=19。但他不知道 a 或 b,他面对的是离散对数难题,算不出密钥 2!

模块五:攻击者视角 —— 巧用“指数溢出” (BSGS 算法)

漏洞场景: 在 CSP 进阶题目或不严谨的工程代码中,如果素数 p 选得太小(比如 231−1 级别),黑客就可以使用 BSGS(大步小步法,Baby-Step Giant-Step) 进行降维打击。

黑客逻辑(分块与哈希):
我们要求解 ax≡b(mod p)。暴力枚举需要 O(p) 的时间。

黑客设一个步长 m=⌈p⌉。然后将未知数 x 拆分为 x=i×m−j (其中 0≤i,j≤m)。

原式变为:

aim−j≡b(mod p)⇒aim≡b×aj(mod p)

攻击步骤:

  1. 小步(Baby Step): 把右边 b×aj(mod p)的所有可能结果算出来,存入一个哈希表(std::unordered_map)。时间 O(根号p)

  2. 大步(Giant Step): 枚举左边的 i,计算 aim(mod p)。去哈希表里查有没有相同的数值。一旦查到,破解成功,解出 x=i×m−j!时间 O(根号p)。

C++ 黑客攻击代码:

cpp
#include <iostream>
#include <cmath>
#include <unordered_map>

// 依赖前面写的 fast_pow
extern long long fast_pow(long long a, long long b, long long mod);

// BSGS 破解离散对数: a^x = b (mod p),返回 x
long long bsgs(long long a, long long b, long long p) {
    a %= p; b %= p;
    if (a == 0) return b == 0 ? 1 : -1;
    
    long long m = ceil(sqrt(p));
    std::unordered_map<long long, long long> hash_table;
    
    // Baby Step: 预处理右边 b * a^j (mod p)
    long long aj = 1;
    for (long long j = 0; j <= m; ++j) {
        long long val = (b * aj) % p;
        hash_table[val] = j;
        aj = (aj * a) % p;
    }
    
    // Giant Step: 枚举左边 a^(i*m) (mod p)
    long long am = fast_pow(a, m, p);
    long long aim = 1;
    for (long long i = 1; i <= m; ++i) {
        aim = (aim * am) % p;
        // 查表:如果哈希表存在这个值,碰撞成功!
        if (hash_table.count(aim)) {
            long long j = hash_table[aim];
            return i * m - j; // 返回破解出的私钥 x
        }
    }
    return -1; // 破解失败
}

int main() {
    // 破解 5^x ≡ 8 (mod 23)
    long long secret_x = bsgs(5, 8, 23);
    std::cout << "通过 BSGS 破解出的私钥 x: " << secret_x << std::endl; // 输出 6
    return 0;
}

  

总结:
模运算是密码学的“围墙”。它让计算在一个可控的范围内循环,既利用“正向极快、反向极难”的数学不对称性构建了屏障,又警示我们在工程落地时(如模数 p 的大小选择)容不得半点偷懒。掌握了快速幂、扩欧和 BSGS,你不仅拿到了通往高级算法竞赛的钥匙,更具备了洞悉现代网络安全底层的黑客直觉。

posted on 2026-05-06 22:43  upwifi  阅读(66)  评论(0)    收藏  举报

导航