[Python] 扩展欧几里得算法

背景

Python 体验用欧几里得算法计算最大公约数的过程 一文中,我们用 Python\text{Python} Python 实现了可以高效计算最大公约数的欧几里得算法( Euclidean Algorithm\text{Euclidean Algorithm} Euclidean Algorithm)。在本文中,我们会探索扩展欧几里得算法( Extended Euclidean Algorithm\text{Extended Euclidean Algorithm} Extended Euclidean Algorithm)。

正文

观察线性组合的值

说明:本小节参考了 A Friendly Introduction to Number Theory 中的第 66 6 章(在 Chapter 1~6 里可以看到第 11 1 章到第 66 6 章的内容)

借助图形化界面来进行观察

对正整数 a,ba,b a,b,我们可以构造出它们的线性组合 ax+bya x + b y ax+by 。我用 豆包 写了如下的 Python\text{Python} Python 程序,借助它我们可以看到小范围的 ax+bya x + b y ax+by 的值(整数 aa a 和 bb b 满足 1≤a,b≤501\le a,b\le 50 1≤a,b≤50, xx x 和 yy y 的范围满足 −5≤x,y≤5-5\le x,y \le 5 −5≤x,y≤5)。

python 复制代码
import pygame

# ===================== 基础配置 =====================
pygame.init()

# 【关键1】大幅扩大窗口,给-5~5网格留足宽松空间
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 800
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("ax + by")

# 颜色定义
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
LIGHT_GRAY = (220, 220, 220)
LIGHT_GREEN = (144, 238, 144)
GRAY = (200, 200, 200)
BLUE = (50, 100, 200)

# 数学坐标配置:x、y 范围 -5 到 5
x_vals = range(-5, 6)
y_vals = range(-5, 6)
a = 6
b = 8
MIN_VAL = 1
MAX_VAL = 50

# 【关键2】缩小格子大小,彻底解决拥挤
CELL_SIZE = 55
CENTER_X = WINDOW_WIDTH // 2
CENTER_Y = WINDOW_HEIGHT // 2 + 30  # 微调中心位置,布局更均衡

# 字体设置(保持清晰,不偏大)
font_num = pygame.font.SysFont(None, 30)    # 格点数值
font_scale = pygame.font.SysFont(None, 26)  # 刻度文字
font_ctrl = pygame.font.SysFont(None, 32)  # 控件文字
font_btn = pygame.font.SysFont(None, 40)   # 按钮文字

# ===================== 按钮控件定义(顶部宽松布局)=====================
a_label_x, a_val_x, a_sub_x, a_add_x = 180, 230, 270, 310
b_label_x, b_val_x, b_sub_x, b_add_x = 420, 470, 510, 550
ctrl_y = 40
btn_size = 35

# ===================== 坐标转换函数 =====================
def math_to_pixel(x, y):
    px = CENTER_X + x * CELL_SIZE
    py = CENTER_Y - y * CELL_SIZE
    return px, py

# ===================== 绘制控件函数 =====================
def draw_controls():
    # 绘制a选择区
    screen.blit(font_ctrl.render("a =", True, BLACK), (a_label_x, ctrl_y))
    screen.blit(font_ctrl.render(f"{a}", True, BLUE), (a_val_x, ctrl_y))
    pygame.draw.rect(screen, GRAY, (a_sub_x, ctrl_y, btn_size, btn_size))
    screen.blit(font_btn.render("-", True, BLACK), (a_sub_x+12, ctrl_y+2))
    pygame.draw.rect(screen, GRAY, (a_add_x, ctrl_y, btn_size, btn_size))
    screen.blit(font_btn.render("+", True, BLACK), (a_add_x+10, ctrl_y+2))

    # 绘制b选择区
    screen.blit(font_ctrl.render("b =", True, BLACK), (b_label_x, ctrl_y))
    screen.blit(font_ctrl.render(f"{b}", True, BLUE), (b_val_x, ctrl_y))
    pygame.draw.rect(screen, GRAY, (b_sub_x, ctrl_y, btn_size, btn_size))
    screen.blit(font_btn.render("-", True, BLACK), (b_sub_x+12, ctrl_y+2))
    pygame.draw.rect(screen, GRAY, (b_add_x, ctrl_y, btn_size, btn_size))
    screen.blit(font_btn.render("+", True, BLACK), (b_add_x+10, ctrl_y+2))

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

    # 1. 绘制浅网格线(底部留足空间给x轴刻度,不覆盖)
    for x in x_vals:
        px, _ = math_to_pixel(x, 0)
        # 竖线底部缩短,不遮挡x轴刻度
        pygame.draw.line(screen, LIGHT_GRAY, (px, 80), (px, WINDOW_HEIGHT-50), 1)
    for y in y_vals:
        _, py = math_to_pixel(0, y)
        # 横线左侧缩短,不遮挡y轴刻度
        pygame.draw.line(screen, LIGHT_GRAY, (50, py), (WINDOW_WIDTH-50, py), 1)

    # 2. 绘制格点数值(宽松间距,无拥挤)
    for y in y_vals:
        for x in x_vals:
            val = a * x + b * y
            px, py = math_to_pixel(x, y)
            text = font_num.render(f"{val}", True, BLACK)
            text_rect = text.get_rect(center=(px, py))
            screen.blit(text, text_rect)

    # 3. 【关键3】修复x轴刻度:位置调至可见区域,彻底显示
    for x in x_vals:
        px, py = math_to_pixel(x, -6)  # 精准定位,不超出窗口
        text = font_scale.render(f"x={x}", True, BLACK)
        text_rect = text.get_rect(center=(px, py))
        bg_rect = pygame.Rect(text_rect.left - 4, text_rect.top - 2, 
                             text_rect.width + 8, text_rect.height + 4)
        pygame.draw.rect(screen, LIGHT_GREEN, bg_rect)
        screen.blit(text, text_rect)

    # 4. 【关键4】优化y轴刻度:位置宽松,不拥挤
    for y in y_vals:
        px, py = math_to_pixel(-6, y)  # 精准定位,完全可见
        text = font_scale.render(f"y={y}", True, BLACK)
        text_rect = text.get_rect(right=px, centery=py)
        bg_rect = pygame.Rect(text_rect.left - 4, text_rect.top - 2, 
                             text_rect.width + 8, text_rect.height + 4)
        pygame.draw.rect(screen, LIGHT_GREEN, bg_rect)
        screen.blit(text, text_rect)

    # 5. 绘制标题
    title = font_ctrl.render("a * x + b * y", True, BLACK)
    screen.blit(title, (WINDOW_WIDTH//2 - title.get_width()//2, 80))

    # 6. 绘制a、b选择控件
    draw_controls()

    # 7. 刷新屏幕
    pygame.display.flip()

    # ===================== 事件处理 =====================
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        
        # 鼠标点击控制a/b数值
        if event.type == pygame.MOUSEBUTTONDOWN:
            mx, my = event.pos
            if a_sub_x <= mx <= a_sub_x+btn_size and ctrl_y <= my <= ctrl_y+btn_size:
                a = max(MIN_VAL, a - 1)
            elif a_add_x <= mx <= a_add_x+btn_size and ctrl_y <= my <= ctrl_y+btn_size:
                a = min(MAX_VAL, a + 1)
            elif b_sub_x <= mx <= b_sub_x+btn_size and ctrl_y <= my <= ctrl_y+btn_size:
                b = max(MIN_VAL, b - 1)
            elif b_add_x <= mx <= b_add_x+btn_size and ctrl_y <= my <= ctrl_y+btn_size:
                b = min(MAX_VAL, b + 1)

pygame.quit()

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

bash 复制代码
python3 show.py

aa a 和 bb b 的初始值分别是 66 6 和 88 8 ⬇️ 我们可以通过点击 - 按钮和 + 按钮来调整它们的值。

可能注意到的结果

调整 aa a 和 bb b 的值若干次之后,您也许会注意到,格点处的值似乎总是 gcd(a,b)gcd(a, b) gcd(a,b) 的倍数。我们可以证明它。位于 (x,y)(x,y) (x,y) 处的值是 ax+bya x + b y ax+by 由于 gcd(a,b)gcd(a,b) gcd(a,b) 是 aa a 和 bb b 的最大公约数(所以也是它们的公约数),那么

  • gcd(a,b)∣agcd(a,b)\mid a gcd(a,b)∣a
  • gcd(a,b)∣bgcd(a,b)\mid b gcd(a,b)∣b

所以 gcd(a,b)∣(ax+by)gcd(a, b)\mid (a x + b y) gcd(a,b)∣(ax+by),也就是说任意格点处的值都是 gcd(a,b)gcd(a, b) gcd(a,b) 的倍数。

另一个观察是,似乎总能找到某个格点,那个格点的值 vv v 满足 v=gcd(a,b)v=gcd(a, b) v=gcd(a,b)。这一点可以通过扩展欧几里得算法来证明。

我们先看看简单的情况。如果 b=0b=0 b=0,那么 gcd(a,b)=agcd(a,b)=a gcd(a,b)=a (注意: b=0b=0 b=0 时, a≠0a\ne 0 a=0)。此时容易验证 1×a+0×b=a=gcd(a,b)1 \times a + 0 \times b=a=gcd(a,b) 1×a+0×b=a=gcd(a,b),也就是说,格点 (1,0)(1,0) (1,0) 处的值,一定是 gcd(a,b)gcd(a,b) gcd(a,b)。之后我们再看看一般的情形。

a,ba,b a,b,我们想找到整数 x,yx,y x,y 使得 ax+by=gcd(a,b)a x + b y = gcd(a,b) ax+by=gcd(a,b) 成立。假设我们已经为 b,a  mod  bb,a \bmod b b,amodb 找到了对应的整数 x′,y′x',y' x′,y′ 使得 bx′+(a  mod  b)y′=gcd(a,b)b x' + (a \bmod b) y'= gcd(a,b) bx′+(amodb)y′=gcd(a,b)。我们从这个等式出发,做些处理。
bx′+(a  mod  b)y′=gcd(a,b)b x' + (a \bmod b) y'= gcd(a,b) bx′+(amodb)y′=gcd(a,b)

其实就是
bx′+(a−⌊a/b⌋b)y′=gcd(a,b)b x' + (a - \lfloor a / b \rfloor b) y'= gcd(a,b) bx′+(a−⌊a/b⌋b)y′=gcd(a,b)

整理一下,可以变为
ay′+b(x′−⌊a/b⌋y′)=gcd(a,b)a y' + b (x'-\lfloor a/b\rfloor y')= gcd(a,b) ay′+b(x′−⌊a/b⌋y′)=gcd(a,b)

所以可以这样选择 xx x 和 yy y ⬇️

  • x=y′x=y' x=y′
  • y=x′−⌊a/b⌋y′y=x'-\lfloor a/b \rfloor y' y=x′−⌊a/b⌋y′

这个过程展示了扩展欧几里得算法的核心思想。

Python\text{Python} Python 代码来实现扩展欧几里得算法

我们可以用 Python\text{Python} Python 代码来实现上述逻辑。我写了以下 Python\text{Python} Python 代码来实现欧几里得算法,并进行了对应的测试 ⬇️

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)

for a in range(100):
    for b in range(100):
        if (a, b) == (0, 0):
            continue
        (x, y, g) = extended_euclidean(a, b)
        if a * x + b * y != g:
            raise ValueError("extend_euclidean failed")
        else:
            print(f"{a} * {x} + {b} * {y} = {g}")

请将以上代码保存为 extended_euclidean_algorithm.py。使用以下命令可以运行 extended_euclidean_algorithm.py

bash 复制代码
python3 extended_euclidean_algorithm.py 

运行结果有 99999999 9999 行,这里就不展示完整结果了 😂

参考资料

相关推荐
Duckdblab1 小时前
DuckDB 性能调优终极指南:打造闪电般的分析体验
python
带派擂总2 小时前
Python全栈开发精华版最全合集(包含各种面试题) Day24_异常和错误
python
To_OC4 小时前
LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了
javascript·算法·leetcode
金銀銅鐵5 小时前
n^5 和 n 的个位数是否总相等?
python·数学
aqi008 小时前
15天学会AI应用开发(九)利用Chroma持久化向量数据
人工智能·python·大模型·ai编程·ai应用
金銀銅鐵8 小时前
借助 Pygame 探索最大公约数的规律
python·数学·游戏
To_OC20 小时前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
ServBay1 天前
9 个 Python 第三方库推荐,不用 AI 都好像多出一个团队
后端·python