背景
在 [Python] 体验用欧几里得算法计算最大公约数的过程 一文中,我们用 实现了可以高效计算最大公约数的欧几里得算法()。在本文中,我们会探索扩展欧几里得算法()。本文提供了用图形化界面展示扩展欧几里得算法计算结果的完整 代码。
正文
观察线性组合的值
说明:本小节参考了 A Friendly Introduction to Number Theory 中的第 章(在 Chapter 1~6 里可以看到第 章到第 章的内容)
借助图形化界面来进行观察
对正整数 ,我们可以构造出它们的线性组合 。我用 豆包 写了如下的 程序,借助它我们可以看到小范围的 的值(整数 和 满足 , 和 的范围满足 )。
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
python3 show.py
和 的初始值分别是 和 ⬇️ 我们可以通过点击 - 按钮和 + 按钮来调整它们的值。
可能注意到的结果
调整 和 的值若干次之后,您也许会注意到,格点处的值似乎总是 的倍数。我们可以证明它。位于 处的值是 由于 是 和 的最大公约数(所以也是它们的公约数),那么
所以 ,也就是说任意格点处的值都是 的倍数。
另一个观察是,似乎总能找到某个格点,那个格点的值 满足 。以 为例,可以找到满足 的格点 ⬇️
这一点可以通过扩展欧几里得算法来证明。
我们先看看简单的情况。如果 ,那么 (注意: 时,)。此时容易验证 ,也就是说,格点 处的值,一定是 。之后我们再看看一般的情形。
对 ,我们想找到整数 使得 成立。假设我们已经为 找到了对应的整数 使得 。我们从这个等式出发,做些处理。
其实就是
整理一下,可以变为
所以可以这样选择 和 ⬇️
这个过程展示了扩展欧几里得算法的核心思想。
用 代码来实现扩展欧几里得算法
我们可以用 代码来实现上述逻辑。我写了以下 代码来实现欧几里得算法,并进行了对应的测试 ⬇️
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
python3 extended_euclidean_algorithm.py
运行结果有 行,这里就不展示完整结果了 😂
扩展欧几里得算法计算器
基于上一小节提供的代码,我们可以更进一步,用 生成一个图形化界面,来展示扩展欧几里得算法的结果。我用 豆包 生成了和 有关的代码,然后做了些小调整,最终得到的结果如下
import pygame
import sys
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)
# Pygame 初始化
pygame.init()
WIDTH, HEIGHT = 600, 400
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Extended Euclidean Algorithm Calculator")
clock = pygame.time.Clock()
# 颜色定义
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
BLUE = (0, 100, 255)
RED = (220, 30, 30)
# 字体
font = pygame.font.SysFont("arial", 24)
small_font = pygame.font.SysFont("arial", 20)
# 输入框配置
input_a = ""
input_b = ""
active_box = 0 # 0:无,1:a输入框,2:b输入框
result_text = ""
error_text = ""
# 输入框矩形
box_a = pygame.Rect(120, 80, 180, 36)
box_b = pygame.Rect(120, 140, 180, 36)
calc_btn = pygame.Rect(120, 200, 180, 40)
def reset_result():
global result_text, error_text
result_text = ""
error_text = ""
while True:
screen.fill(WHITE)
# 事件处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# 鼠标点击切换输入框/触发计算
if event.type == pygame.MOUSEBUTTONDOWN:
mx, my = event.pos
reset_result()
if box_a.collidepoint(mx, my):
active_box = 1
elif box_b.collidepoint(mx, my):
active_box = 2
elif calc_btn.collidepoint(mx, my):
# 执行计算
try:
a = int(input_a)
b = int(input_b)
if (a, b) == (0, 0):
raise ValueError("a and b cannot be both 0")
x, y, g = extended_euclidean(a, b)
# 构造输出文本
eq = f"{a} * ({x}) + {b} * ({y}) = {g}"
result_text = f"For a={a}, b={b}, below condition is true:\n{eq}"
except ValueError:
if (a, b) == (0, 0):
error_text = "a and b cannot be both 0"
else:
error_text = "Input must be integers!"
else:
active_box = 0
# 键盘输入
if event.type == pygame.KEYDOWN and active_box != 0:
key = event.key
# 退格删除
if key == pygame.K_BACKSPACE:
if active_box == 1 and len(input_a) > 0:
input_a = input_a[:-1]
elif active_box == 2 and len(input_b) > 0:
input_b = input_b[:-1]
# 数字
elif key in (pygame.K_0, pygame.K_1, pygame.K_2, pygame.K_3, pygame.K_4,
pygame.K_5, pygame.K_6, pygame.K_7, pygame.K_8, pygame.K_9):
char = event.unicode
if active_box == 1:
input_a += char
else:
input_b += char
# 绘制文字标签
label_a = font.render("a =", True, BLACK)
label_b = font.render("b =", True, BLACK)
screen.blit(label_a, (60, 82))
screen.blit(label_b, (60, 142))
# 绘制输入框
pygame.draw.rect(screen, GRAY, box_a)
pygame.draw.rect(screen, BLUE if active_box == 1 else BLACK, box_a, 2)
pygame.draw.rect(screen, GRAY, box_b)
pygame.draw.rect(screen, BLUE if active_box == 2 else BLACK, box_b, 2)
# 输入框文字
text_a = font.render(input_a, True, BLACK)
text_b = font.render(input_b, True, BLACK)
screen.blit(text_a, (box_a.x + 8, box_a.y + 4))
screen.blit(text_b, (box_b.x + 8, box_b.y + 4))
# 绘制计算按钮
pygame.draw.rect(screen, BLUE, calc_btn)
btn_text = font.render("Calculate", True, WHITE)
screen.blit(btn_text, (calc_btn.x + 12, calc_btn.y + 6))
# 绘制错误提示
if error_text:
err_surf = small_font.render(error_text, True, RED)
screen.blit(err_surf, (120, 250))
# 绘制计算结果(分行显示)
y_offset = 280
if result_text:
lines = result_text.split("\n")
for line in lines:
line_surf = small_font.render(line, True, BLACK)
screen.blit(line_surf, (60, y_offset))
y_offset += 30
pygame.display.flip()
clock.tick(60)
请将以上代码保存为 ee_calculator.py (意为扩展欧几里得算法计算器)。用以下命令可以运行 ee_calculator.py
python3 ee_calculator.py
运行效果
运行效果如下图所示 ⬇️
我们输入 和 之后,再点击 Calculate 按钮, 代码就会用扩展欧几里得算法计算出满足 的一组 值。示例效果如下 ⬇️
参考资料
- A Friendly Introduction to Number Theory 中的
- 第 章(在 Chapter 1~6 里可以看到第 章到第 章的内容)