qinghuan
← 回到知识探索

计算机趣闻 / 可下载小项目

循环表上可以直接除以2吗?先看它有没有把不同位置挤在一起

在模6里,2×2和2×5都落在4;但模12里乘5却能唯一倒推。用时钟式循环编号理解逆元与互素条件。

展开本文目录
  1. 下载完整练习包
  2. 先把循环规则写成余数式
  3. 模6里除以2,为什么会漏掉一个答案?
  4. 为什么正好看最大公约数是不是1?
  5. 余数每缩小一次,系数也跟着记账
  6. 核对器把所有候选逐个乘回去
  7. 不要把一个有条件的公式当通用快捷键
  8. 记住这三点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
模逆
乘上它以后,按指定模数取余会得到1的整数。
互素
两个整数除了1没有其他正公因子,也就是gcd为1。
打开完整术语小抄 ↗

想象编号0到11围成一个圆,超过11就从0重新数。每次跳5格,跳x次落在7:我们希望从终点7倒推出跳了几次,且只比较0到11次这个范围。这里的“除以5”要先确认循环映射能不能倒过来。

普通除法里 4÷2=2,但在0到5的循环上,2×2和2×5取余都得到4。两个不同输入被压到同一位置,单凭结果4就无法唯一恢复原来是哪一个。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • modular.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B modular.py

解压到独立练习文件夹,在含 modular.py 的目录打开终端运行。整数模逆与小规模线性同余枚举;模数至少2;不代表密码学恒定时间或密钥生成实现。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先把循环规则写成余数式

“每次跳5格,跳x次落在7”写作 5x≡7 (mod 12),意思是5x和7相差12的整数倍。我们只求0到11中的代表值;其他相差12的次数在这张循环表里落点相同。

若能找到一个数k,使5k除以12余1,就能把两边都乘k。5×5=25余1,所以k=5;x与7×5=35同余,代表值为11。验证5×11=55除以12余7,才完成回查。

模6里除以2,为什么会漏掉一个答案?

逐个检查0到5:2x取余的结果为0、2、4、0、2、4。因此2x≡4有x=2与x=5两个代表值,而2x≡1一个都没有。直接写x=4/2只留下2,丢掉了5。

若同时把模数也约去共同因子,2x≡4 (mod 6)可化为x≡2 (mod 3),再放回0到5就得到两项。关键是原来按6区分的位置被合并,不能只改等式两边而保持原模数不变。本程序保留枚举结果,避免把多解问题偷偷改成唯一解。

为什么正好看最大公约数是不是1?

逆元要求 ax+my=1,其中y是某个整数。如果a和m有公因子d>1,那么左边每一项都能被d整除,和也能,无法等于1。这说明有共同因子时一定没有乘法逆元。

反过来,欧几里得算法不只算出gcd,还能把它写成a和m的整数倍之和。若gcd为1,就得到了ax+my=1,取模m后my消失,留下ax≡1。于是x取余就是逆元。这是充分条件,和前面的必要条件合起来得到“当且仅当互素”。

余数每缩小一次,系数也跟着记账

普通欧几里得不断把一对数(old_r,r)换成(r,old_r-q×r),其中q是整数商。它们的最大公约数不变,但较小的余数持续下降,最终留下gcd。

扩展版本同步更新a的系数(old_s,s)。初始a的系数是1,m的系数是0;每做一次“旧数减q倍当前数”,系数也做同样运算。代码只保留a的系数,m的系数没有打印,但它一直存在于整数线性组合里。最后gcd不是1就返回None,而不是凑一个近似小数。

核对器把所有候选逐个乘回去

运行 python -B modular.py。inverse_5_mod12是5,clock_5x_mod12_equals7是11;2x_mod6_equals4是[2,5],2x_mod6_equals1是空列表。euclid_trace给出每步的两数与商。

程序枚举模数2到40,以及每个模数下a从-2m到2m的输入。独立参考直接把0到m-1全部候选乘回去找余1的值,不调用gcd、pow或欧几里得。结果再与math.gcd条件、Python的pow(a,-1,m)交叉核对;存在逆元时额外验证三个目标余数的唯一解。

展开收起代码 · 小实验python · 74 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Invert a cyclic multiplier only when it preserves every residue uniquely."""
import math
import json


def inverse(a, modulus):
    if any(type(x) is not int for x in (a, modulus)) or modulus < 2:
        raise ValueError("integer a and modulus >= 2 required")
    a %= modulus
    old_r, r, old_s, s = a, modulus, 1, 0
    trace = []
    while r:
        quotient = old_r // r
        trace.append({"left": old_r, "right": r, "quotient": quotient})
        old_r, r = r, old_r - quotient * r
        old_s, s = s, old_s - quotient * s
    if old_r != 1:
        return None, trace
    return old_s % modulus, trace


def all_solutions(a, b, modulus):
    # Deliberately independent bounded oracle, no gcd, pow or Euclidean steps.
    if not 2 <= modulus <= 100:
        raise ValueError("enumeration limited to moduli 2..100")
    return [x for x in range(modulus) if (a * x - b) % modulus == 0]


def main():
    checks, cases = 0, 0
    def check(ok):
        nonlocal checks
        if not ok:
            raise AssertionError("modular check failed")
        checks += 1
    for modulus in range(2, 41):
        for a in range(-2 * modulus, 2 * modulus + 1):
            found, _ = inverse(a, modulus)
            answers = all_solutions(a, 1, modulus)
            check(answers == ([] if found is None else [found]))
            check((found is not None) == (math.gcd(a, modulus) == 1))
            if found is not None:
                check(found == pow(a, -1, modulus))
                for b in (0, 1, modulus - 1):
                    check(all_solutions(a, b, modulus) == [(b * found) % modulus])
            else:
                try:
                    pow(a, -1, modulus)
                except ValueError:
                    check(True)
                else:
                    raise AssertionError("pow accepted noninvertible base")
            cases += 1
    for args in [(1, 1), (1, 0), (True, 7), (1, 7.0)]:
        try:
            inverse(*args)
        except ValueError:
            check(True)
        else:
            raise AssertionError("invalid modulus accepted")
    answer, trace = inverse(5, 12)
    check(answer == 5)
    check(all_solutions(2, 4, 6) == [2, 5])
    check(all_solutions(2, 1, 6) == [])
    print(json.dumps({"checks_passed": checks, "a_modulus_cases": cases,
                      "clock_5x_mod12_equals7": (7 * answer) % 12,
                      "inverse_5_mod12": answer, "euclid_trace": trace,
                      "2x_mod6_equals4": all_solutions(2, 4, 6),
                      "2x_mod6_equals1": all_solutions(2, 1, 6),
                      "scope": "integer arithmetic; no cryptographic constant-time or key generation guarantee"}, indent=2))


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B modular.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。整数模逆与小规模线性同余枚举;模数至少2;不代表密码学恒定时间或密钥生成实现。

运行后对照结果
{
  "checks_passed": 15820,
  "a_modulus_cases": 3315,
  "clock_5x_mod12_equals7": 11,
  "inverse_5_mod12": 5,
  "euclid_trace": [
    {
      "left": 5,
      "right": 12,
      "quotient": 0
    },
    {
      "left": 12,
      "right": 5,
      "quotient": 2
    },
    {
      "left": 5,
      "right": 2,
      "quotient": 2
    },
    {
      "left": 2,
      "right": 1,
      "quotient": 2
    }
  ],
  "2x_mod6_equals4": [
    2,
    5
  ],
  "2x_mod6_equals1": [],
  "scope": "integer arithmetic; no cryptographic constant-time or key generation guarantee"
}

不要把一个有条件的公式当通用快捷键

模数是素数且a不被它整除时,可用费马小定理相关公式求逆;模数是合数时,不能直接写a^(m-2)当逆元。本文一律用扩展欧几里得,先检查结果gcd。Python从3.8起支持整数的pow(a,-1,m),同样要求互素。

程序输入模数必须至少2,a可以为负并先归一为0到m-1。欧几里得迭代次数随模数的二进制长度增长,但大整数除法和乘法本身有成本,不能把它称作固定时间。这里没有密码学恒定时间、防侧信道或密钥生成保证,也没有实现任意线性同余的高效求解器。

记住这三点

  • 乘法逆元要求乘回去余1,它与普通小数倒数不同。
  • 有共同因子会合并输入,方程可能无解或有多个解。
  • 先验证gcd与模数条件,再选求逆算法。

别把例子用过头

  • 在同余式中只除两边,却忘了模数和解的个数可能变化。
  • 把pow(a,m-2,m)无条件用于合数模数。
  • 把没有逆元写成返回0,误以为得到有效答案。

自己试一小步

换一张循环表后,你能说明为什么能或不能唯一倒推吗?

  1. 自选一个合数模数,选两个步长:一个与模数互素,一个不互素。
  2. 列出两个步长对全部位置的映射,找重复或缺失的落点。
  3. 对其中一个目标余数求全部解,并解释是否可用逆元。

做到这些,就算完成

  • 用乘回取余核对每个候选。
  • 交付gcd、解集和适用条件;空集与None含义不同。
  • 先说明求的是唯一逆元还是给定同余式的全部解,再选择返回形式。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • MIT 18.310:模运算与初等代数讲义 (新标签页)↗Michel Goemans / MIT OpenCourseWare · 英文

    支持欧几里得算法、整数系数恒等式及互素条件。

    2026-10-18 已实际打开核对:第1至5页;文本抽取有不等号缺失,因此结合公式含义与另两份来源核对,未复制受损符号。

  • CP-Algorithms 作者:Modular Inverse (新标签页)↗CP-Algorithms 贡献者 · 英文

    支持逆元存在当且仅当 gcd 为1,以及扩展欧几里得求逆。

    2026-10-18 已实际打开核对:Definition、Extended Euclidean algorithm 与 prime-modulus 边界;未把素数幂公式套到合数模。

  • Python 3.14:pow 的模逆契约 (新标签页)↗Python Software Foundation · 英文

    支持 pow(a,-1,m) 对整数互素输入计算模逆及版本范围。

    2026-10-18 已实际打开核对:pow 的 negative exponent/mod 参数说明和38模97示例;正文运行环境3.14.0支持该接口。