[Python] 模 n 乘法的逆元计算器

48 阅读1分钟

背景

[Python] 扩展欧几里得算法 一文中,我们已经接触了扩展欧几里得算法。借助扩展欧几里得算法,我们可以高效地计算模 nn 乘法的逆元。本文会介绍相关的内容。

要点

  • 对自然数 aa 和正整数 nn 而言,ax1(modn)a x \equiv 1 \pmod{n} 有解的充分必要条件是 gcd(a,n)=1gcd(a,n)=1
  • 在扩展欧几里得算法的基础上,我们可以高效地求出 aa 的逆元(即上式中的 xx
  • 再借助 Pygame\text{Pygame} 实现图形化界面,我们就可以制作一个模 nn 乘法的逆元计算器了

正文

对自然数 aa 和 正整数 nn 而言,是否可以找到一个整数 xx 使得 ax1(modn)a x\equiv 1 \pmod{n} 成立呢?

由于以下两个等式成立(其中 kk 是任意整数),我们只需要关心满足 0a,x<n0\le a,x \lt n 条件的整数 a,xa,x 就够了。

  • ax(a+kn)x(modn)ax\equiv (a+kn)x \pmod{n}
  • axa(x+kn)(modn)ax\equiv a(x+kn) \pmod{n}

我们可以先用比较小的 nn 试一试。

用较小的 nn 做尝试

我们可以用暴力的方法,对较小的 nn 做尝试 ⬇️

def show_markdown_result(n):
    print(f"#### $n={n}$ 时")
    print(f"| $a$ | 满足 $ax\\equiv 1 \\pmod{{{n}}}$ 和 $0\\le x\\lt {n}$ 的 $x$ |")
    print("| --- | --- |")
    for a in range(n):
        matched_result = []
        for x in range(n):
            if (a * x - 1) % n == 0:
                matched_result.append(x)
        if matched_result:
            joined_result = ','.join(map(str, matched_result))
            print(f"| ${a}$ | ${joined_result}$ |")
        else:
            print(f"| ${a}$ | (不存在) |")

for n in range(1, 10):
    show_markdown_result(n)
    print("\n")

请将以上代码保存为 show.py,用下方的命令可以运行 show.py

python3 show.py

运行结果中包含了 99markdown 表格 ⬇️

n=1n=1

aa满足 ax1(mod1)ax\equiv 1 \pmod{1}0x<10\le x\lt 1xx
0000

n=2n=2

aa满足 ax1(mod2)ax\equiv 1 \pmod{2}0x<20\le x\lt 2xx
00(不存在)
1111

n=3n=3

aa满足 ax1(mod3)ax\equiv 1 \pmod{3}0x<30\le x\lt 3xx
00(不存在)
1111
2222

n=4n=4

aa满足 ax1(mod4)ax\equiv 1 \pmod{4}0x<40\le x\lt 4xx
00(不存在)
1111
22(不存在)
3333

n=5n=5

aa满足 ax1(mod5)ax\equiv 1 \pmod{5}0x<50\le x\lt 5xx
00(不存在)
1111
2233
3322
4444

n=6n=6

aa满足 ax1(mod6)ax\equiv 1 \pmod{6}0x<60\le x\lt 6xx
00(不存在)
1111
22(不存在)
33(不存在)
44(不存在)
5555

n=7n=7

aa满足 ax1(mod7)ax\equiv 1 \pmod{7}0x<70\le x\lt 7xx
00(不存在)
1111
2244
3355
4422
5533
6666

n=8n=8

aa满足 ax1(mod8)ax\equiv 1 \pmod{8}0x<80\le x\lt 8xx
00(不存在)
1111
22(不存在)
3333
44(不存在)
5555
66(不存在)
7777

n=9n=9

aa满足 ax1(mod9)ax\equiv 1 \pmod{9}0x<90\le x\lt 9xx
00(不存在)
1111
2255
33(不存在)
4477
5522
66(不存在)
7744
8888

一些规律

从上一小节的数据来看,似乎有以下规律成立(有待证明或者证伪)

  1. aann 互质(即 gcd(a,n)=1gcd(a,n)=1)时,似乎总能找到满足 ax1(modn)a x \equiv 1 \pmod{n}xx
  2. aann 互质(即 gcd(a,n)1gcd(a,n)\ne1)时,似乎总是无法找到满足 ax1(modn)a x \equiv 1 \pmod{n}xx
  3. 当我们限制 xx 满足 0x<n0\le x\lt n 时,能让 ax1(modn)a x \equiv 1 \pmod{n} 成立的 xx 似乎最多只有 11 个。

规律一的证明

第一个规律,我们可以用扩展欧几里得算法来证明。当 aann 互质(即 gcd(a,n)=1gcd(a,n)=1)时,借助扩展欧几里得算法,可以找到满足 ax0+ny0=1a x_0 + n y_0=1 的整数 x0,y0x_0,y_0。于是 ax01=ny0ax_0-1=ny_0,所以 ax01(modn)ax_0\equiv 1 \pmod{n}。令 x=x0modnx=x_0 \bmod n,就找到了符合要求的 xx

规律二的证明

第二个规律,可以用反证法证明。当 aann 互质(即 gcd(a,n)1gcd(a,n)\ne1)时,假设可以找到某个 xx 使得 ax1(modn)a x \equiv 1 \pmod{n}xx 成立,那么存在某个整数 kk 使得 ax+kn=1ax+kn=1 成立。以下两个等式显然成立

  • gcd(a,n)axgcd(a,n)\mid ax
  • gcd(a,n)kngcd(a,n) \mid kn

那么 gcd(a,n)(ax+kn)gcd(a,n) \mid (ax+kn) 成立,即 gcd(a,n)1gcd(a,n) \mid 1 成立,这与 gcd(a,n)1gcd(a,n)\ne1 矛盾。第二个规律得证。有了规律一和规律二,我们就知道 ax1(modn)a x \equiv 1 \pmod{n} 有解的充分必要条件是 gcd(a,n)=1gcd(a,n)=1

规律三的证明

第三个规律,也可以证明。假设可以找到 x1,x2x_1,x_2 分别满足

  • ax11(modn)a x_1 \equiv 1 \pmod{n}
  • ax21(modn)a x_2 \equiv 1 \pmod{n}

我们在第一个等式两边乘以 x2x_2,会得到

x2ax1x2(modn)x_2 a x_1 \equiv x_2 \pmod{n}

我们在第二个等式两边乘以 x1x_1,会得到

x1ax2x1(modn)x_1 a x_2 \equiv x_1 \pmod{n}

而模 nn 的乘法满足交换律和结合律(这里就不证明了),所以

x2ax1ax2x1(modn)x_2 a x_1 \equiv ax_2 x_1 \pmod{n}
ax2x1ax1x2(modn)ax_2 x_1 \equiv ax_1 x_2 \pmod{n}
ax1x2x1ax2(modn)ax_1 x_2 \equiv x_1 a x_2 \pmod{n}

于是 x1x2(modn)x_1\equiv x_2 \pmod{n}

因此,当我们限制 xx 满足 0x<n0\le x\lt n 时,能让 ax1(modn)a x \equiv 1 \pmod{n} 成立的 xx 最多只有 11

用程序来处理

核心逻辑

[Python] 扩展欧几里得算法 一文中已经提供了扩展欧几里得算法的代码。我们在它的基础上,再加上计算逆元的方法即可 ⬇️

def extended_euclidean(a, b):
    if (a, b) == (0, 0):
        raise ValueError("a and b cannot be both 0")
    if b == 0:
        return (1, 0, a)
    x, y, g = extended_euclidean(b, a % b)
    return (y, x - a // b * y, g)

def calc_inverse_element(a, n):
    x, _, gcd = extended_euclidean(a, n)
    if gcd == 1:
        return x % n
    return None

添加 GUI 之后的完整程序

豆包 帮我完成了和 Pygame\text{Pygame} 相关的代码。完整的 Python\text{Python} 程序如下 ⬇️

import pygame

def extended_euclidean(a, b):
    if (a, b) == (0, 0):
        raise ValueError("a and b cannot be both 0")
    if b == 0:
        return (1, 0, a)
    x, y, g = extended_euclidean(b, a % b)
    return (y, x - a // b * y, g)

def calc_inverse_element(a, n):
    x, _, gcd = extended_euclidean(a, n)
    if gcd == 1:
        return x % n
    return None

# ===================== Pygame 初始化 =====================
pygame.init()

WIDTH, HEIGHT = 700, 480
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Inverse Element Calculator")

# 颜色配置
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (220, 220, 220)
BLUE = (50, 100, 200)
RED = (200, 50, 50)
GREEN = (50, 180, 50)

# 字体
font_input = pygame.font.SysFont("Arial", 36)
font_text = pygame.font.SysFont("Arial", 32)
font_result = pygame.font.SysFont("Arial", 38)
font_equation = pygame.font.SysFont("Arial", 32)
font_btn = pygame.font.SysFont("Arial", 34)

# ===================== 布局:全部水平居中,对称美观 =====================
# 输入框组居中
INPUT_X = WIDTH // 2 - 110  # 输入框水平居中
INPUT_WIDTH = 220
INPUT_HEIGHT = 50

# 输入a
input_a_rect = pygame.Rect(INPUT_X, 110, INPUT_WIDTH, INPUT_HEIGHT)
input_a_text = ""
a_active = False

# 输入n
input_n_rect = pygame.Rect(INPUT_X, 180, INPUT_WIDTH, INPUT_HEIGHT)
input_n_text = ""
n_active = False

# 按钮居中
btn_rect = pygame.Rect(WIDTH // 2 - 150, 260, 300, 60)
btn_text = "Calculate"

# 结果
result_text = ""
equation_text = ""
result_color = BLACK

# ===================== 主循环 =====================
running = True
while running:
    screen.fill(WHITE)

    # -------------------- 文字与输入框(居中对齐)--------------------
    # Input a 标签(与输入框垂直居中)
    label_a = font_text.render("Input a:", True, BLACK)
    screen.blit(label_a, (INPUT_X - 120, 118))
    
    # Input n 标签(与输入框垂直居中)
    label_n = font_text.render("Input n:", True, BLACK)
    screen.blit(label_n, (INPUT_X - 120, 188))

    # 绘制输入框a
    pygame.draw.rect(screen, BLUE if a_active else GRAY, input_a_rect, 2)
    screen.blit(font_input.render(input_a_text, True, BLACK), (input_a_rect.x+10, input_a_rect.y+5))

    # 绘制输入框n
    pygame.draw.rect(screen, BLUE if n_active else GRAY, input_n_rect, 2)
    screen.blit(font_input.render(input_n_text, True, BLACK), (input_n_rect.x+10, input_n_rect.y+5))

    # 绘制按钮(居中)
    pygame.draw.rect(screen, BLUE, btn_rect, border_radius=8)
    btn_surface = font_btn.render(btn_text, True, WHITE)
    screen.blit(btn_surface, (btn_rect.centerx - btn_surface.get_width()//2, btn_rect.centery - btn_surface.get_height()//2))

    # 结果文字(居中显示,更美观)
    result_surface = font_result.render(result_text, True, result_color)
    screen.blit(result_surface, (WIDTH // 2 - result_surface.get_width()//2, 350))

    # 公式提示(底部居中)
    formula = font_equation.render(equation_text, True, BLACK)
    screen.blit(formula, (WIDTH // 2 - formula.get_width()//2, 420))

    # ===================== 事件处理 =====================
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        # 鼠标点击
        if event.type == pygame.MOUSEBUTTONDOWN:
            if input_a_rect.collidepoint(event.pos):
                a_active = True
                n_active = False
            elif input_n_rect.collidepoint(event.pos):
                n_active = True
                a_active = False
            elif btn_rect.collidepoint(event.pos):
                result_text = ""
                try:
                    a = int(input_a_text)
                    n = int(input_n_text)
                    if a <= 0 or n <= 0:
                        result_text = "ERROR: Please input positive integers!"
                        result_color = RED
                    else:
                        inv = calc_inverse_element(a, n)
                        if inv is not None:
                            result_text = f"x: {inv}"
                            result_color = GREEN
                            equation_text = f"{a} * {inv} ≡ 1 (mod {n})"
                        else:
                            result_text = f"ERROR: {a} and {n} are not relatively prime, no inverse exists!"
                            result_color = RED
                except ValueError:
                    result_text = "ERROR: Please input valid integers!"
                    result_color = RED
            else:
                a_active = False
                n_active = False

        # 键盘输入
        if event.type == pygame.KEYDOWN:
            if a_active:
                if event.key == pygame.K_BACKSPACE:
                    input_a_text = input_a_text[:-1]
                elif event.unicode.isdigit():
                    input_a_text += event.unicode
            if n_active:
                if event.key == pygame.K_BACKSPACE:
                    input_n_text = input_n_text[:-1]
                elif event.unicode.isdigit():
                    input_n_text += event.unicode

    pygame.display.flip()

pygame.quit()

请将完整的程序保存为 inverse_element_calculator.py。使用如下的命令可以运行 inverse_element_calculator.py

python3 inverse_element_calculator.py

初始界面如下图所示 ⬇️

image.png

此时需要用户输入 aann。我输入了 34342323,然后点击 Calculate 按钮,就可以计算 aa 对应的逆元了 ⬇️

image.png

由于扩展欧几里得算法很高效,所以即使输入比较大 aann,也可以立刻算出结果,示例效果如下 ⬇️ (a=63245986,n=102334155a=63245986,n=102334155)

image.png

参考资料