[Python] 借助图形化界面探索模n运算的规律

背景

进行模 nn n 运算时,会有一些规律。如果我们能通过图形化界面来展示模 nn n 运算的结果,应该更容易发现这些规律。本文只关心 nn n 是质数的情况。

正文

受到 A Friendly Introduction to Number Theory 中第 99 9 章 (Congruences, Powers, and Fermat's Little Theorem) 的启发,我觉得可以借助图形化界面来探索模 nn n 运算的规律。本文只关心 nn n 是质数的情况。

代码

我用 豆包trae 写了如下的代码 (我在 Python 费马小定理 一文中也提供了这部分代码)

python 复制代码
import pygame

# ===================== 1. 初始化配置 =====================
pygame.init()

# 可选的质数 p 列表
PRIMES = [2, 3, 5, 7, 11, 13, 17, 19]
# 初始选中的 p(默认 None,未选择)
selected_p = None

# 基础窗口设置(自适应大小,最小600x600)
MIN_WIDTH, MIN_HEIGHT = 650, 650
screen = pygame.display.set_mode((MIN_WIDTH, MIN_HEIGHT), pygame.RESIZABLE)
pygame.display.set_caption("a^n mod p visualization tool")

# 颜色配置
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
BLUE = (60, 130, 220)    # 按钮颜色
LIGHT_BLUE = (140, 190, 240)
RED = (220, 60, 60)     # 标题颜色
GREEN = (50, 180, 50)   # 结果文字颜色
DARK_BLUE = (30, 80, 180) # a/n标签颜色

def get_fonts(cell_size):
    """根据单元格大小动态调整字体"""
    base_size = max(10, int(cell_size * 0.55))
    font_grid = pygame.font.SysFont("Arial", base_size)
    font_an = pygame.font.SysFont("Arial", base_size)
    return font_grid, font_an

font_btn = pygame.font.SysFont("Arial", 24)
font_label = pygame.font.SysFont("Arial", 26)

def calc_pow(a, n, p):
    """calc a**n mod p"""
    if n == 0:
        return 1
    temp = calc_pow(a, n // 2, p)
    if n % 2 == 0:
        return (temp * temp) % p
    return (temp * temp * a) % p

# ===================== 2. 按钮绘制与点击判断 =====================
def draw_buttons():
    """绘制顶部的p选择按钮"""
    btn_width, btn_height = 60, 40
    start_x = 20
    start_y = 20
    spacing = 10

    for i, p in enumerate(PRIMES):
        btn_x = start_x + i * (btn_width + spacing)
        btn_rect = pygame.Rect(btn_x, start_y, btn_width, btn_height)
        
        # 选中的按钮变色
        color = LIGHT_BLUE if p == selected_p else BLUE
        pygame.draw.rect(screen, color, btn_rect, border_radius=5)
        
        # 按钮文字
        text = font_btn.render(f"p={p}", True, WHITE)
        text_rect = text.get_rect(center=btn_rect.center)
        screen.blit(text, text_rect)

def get_clicked_p(mouse_pos):
    """判断点击了哪个p按钮,返回对应的p值"""
    btn_width, btn_height = 60, 40
    start_x = 20
    start_y = 20
    spacing = 10

    for i, p in enumerate(PRIMES):
        btn_x = start_x + i * (btn_width + spacing)
        btn_rect = pygame.Rect(btn_x, start_y, btn_width, btn_height)
        if btn_rect.collidepoint(mouse_pos):
            return p
    return None

def resize_window(p):
    """根据选择的p自动调整窗口大小"""
    cell_size = min(70, (MIN_WIDTH - 120) // p)
    _, font_an = get_fonts(cell_size)
    max_num_str = str(p)
    max_label_width = font_an.size(max_num_str)[0]
    label_space = max_label_width + 10
    
    needed_width = cell_size * p + label_space + 60
    needed_height = 120 + cell_size * p + label_space + 60
    
    needed_width = max(needed_width, MIN_WIDTH)
    needed_height = max(needed_height, MIN_HEIGHT)
    
    return pygame.display.set_mode((needed_width, needed_height), pygame.RESIZABLE)

# ===================== 3. 网格绘制(显式标注a和n的值) =====================
def draw_grid(p):
    """绘制a-n网格,显式展示a、n和a^n mod p的值"""
    if p is None:
        tip = font_label.render("Please select p value", True, RED)
        screen.blit(tip, (screen.get_width()//2 - tip.get_width()//2, 100))
        return

    cell_size = min(70, (screen.get_width() - 120) // p)
    font_grid, font_an = get_fonts(cell_size)
    
    max_num_str = str(p)
    max_label_width = font_an.size(max_num_str)[0]
    label_space = max_label_width + 10

    total_grid_width = cell_size * p + label_space
    grid_start_x = (screen.get_width() - total_grid_width) // 2 + label_space
    grid_start_y = 120 + label_space
    grid_start_x = (screen.get_width() - total_grid_width) // 2 + label_space
    grid_start_y = 120 + label_space

    # -------- 1. 绘制坐标轴标题 --------
    n_title = font_label.render("n →", True, DARK_BLUE)
    n_title_x = grid_start_x + (cell_size * p) // 2
    n_title_y = 85
    screen.blit(n_title, (n_title_x - n_title.get_width()//2, n_title_y))

    a_title = font_label.render("a ↓", True, DARK_BLUE)
    a_title_x = (screen.get_width() - total_grid_width) // 2 - 25
    a_title_y = grid_start_y + (cell_size * p) // 2
    screen.blit(a_title, (a_title_x, a_title_y - a_title.get_height()//2))

    # -------- 2. 绘制顶部:显式展示所有 n 的值(横坐标) --------
    for i in range(p):
        n = i + 1
        x = grid_start_x + i * cell_size + cell_size//2
        y = grid_start_y - label_space//2
        n_text = font_an.render(str(n), True, DARK_BLUE)
        n_rect = n_text.get_rect(center=(x, y))
        screen.blit(n_text, n_rect)

    # -------- 3. 绘制左侧:显式展示所有 a 的值(纵坐标) --------
    for a in range(p):
        x = grid_start_x - label_space//2
        y = grid_start_y + a * cell_size + cell_size//2
        a_text = font_an.render(str(a), True, DARK_BLUE)
        a_rect = a_text.get_rect(center=(x, y))
        screen.blit(a_text, a_rect)

    # -------- 4. 绘制网格主体 + 计算结果 --------
    for i in range(p):
        n = i + 1
        for a in range(p):
            x = grid_start_x + i * cell_size
            y = grid_start_y + a * cell_size
            cell_rect = pygame.Rect(x, y, cell_size, cell_size)

            pygame.draw.rect(screen, BLACK, cell_rect, 1)

            val = calc_pow(a, n, p)
            val_text = font_grid.render(str(val), True, GREEN)
            val_rect = val_text.get_rect(center=cell_rect.center)
            screen.blit(val_text, val_rect)

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

    # 绘制按钮
    draw_buttons()
    # 绘制网格(含显式a/n标注)
    draw_grid(selected_p)

    # 事件处理
    for event in pygame.event.get():
        # 关闭窗口
        if event.type == pygame.QUIT:
            running = False
        
        # 鼠标点击事件
        if event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            # 获取点击的p值
            clicked_p = get_clicked_p(mouse_pos)
            if clicked_p is not None:
                selected_p = clicked_p
                screen = resize_window(selected_p)

    # 刷新界面
    pygame.display.flip()

pygame.quit()

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

复制代码
python3 show_mod_pow.py

开始时的效果如下 ⬇️

选择不同的 pp p 值,就能看到 an  mod  pa^n\bmod {p} anmodp 的计算结果。以 p=5p=5 p=5 为例,计算结果如下

探索

p=2,3,5,7p=2,3,5,7 p=2,3,5,7 的情况为例,我进行了一些探索,我把对应的规律展示在下方(为了便于查看,我把四张图合并到一起了)

规律一: a2a^2 a2 分布对称

第一个规律是: a2a^2 a2 分布对称,即对满足 0<a<p0\lt a\lt p 0<a<p 的 aa a 而言, a2≡(p−a)2(modp)a^2\equiv (p-a)^2\pmod {p} a2≡(p−a)2(modp)

这个规律不难证明 ⬇️
a2−(p−a)2=a2−(p2−2pa+aa)=−p2+2paa^2-(p-a)^2=a^2-(p^2-2pa+a^a)=-p^2+2pa a2−(p−a)2=a2−(p2−2pa+aa)=−p2+2pa


p∣(−p2+2pa)p\mid (-p^2+2pa) p∣(−p2+2pa)

所以 p∣(a2−(p−a)2)p\mid (a^2-(p-a)^2) p∣(a2−(p−a)2)。所以 a2≡(p−a)2(modp)a^2\equiv (p-a)^2 \pmod {p} a2≡(p−a)2(modp) 成立(其实 aa a 可以是任意整数)

规律二: 费马小定理

第二个规律是:对满足 0<a<p0\lt a\lt p 0<a<p 的 aa a 而言, ap−1≡1(modp) a^{p-1}\equiv 1\pmod {p} ap−1≡1(modp) 这个规律其实就是费马小定理。我在 Python 费马小定理 一文中对其进行了更多介绍。

规律三: ap≡a(modp) a^p\equiv a\pmod {p} ap≡a(modp)

第三个规律是:对满足 0≤a<p0\le a\lt p 0≤a<p 的 aa a 而言, ap≡a(modp) a^p\equiv a\pmod {p} ap≡a(modp)

这个规律不难证明 ⬇️

  • 如果 a=0a=0 a=0,那么 0p≡0(modp) 0^p\equiv 0\pmod {p} 0p≡0(modp) 显然成立
  • 如果 0<a<p0\lt a\lt p 0<a<p,那么由费马小定理可得 ap−1≡1(modp) a^{p-1}\equiv 1 \pmod {p} ap−1≡1(modp)。等式两边一起乘以 aa a,可以得出 ap≡a(modp) a^{p}\equiv a \pmod {p} ap≡a(modp)

证明结束

参考资料

相关推荐
Hesionberger1 小时前
快速求解完全平方数的最少数量
开发语言·数据结构·python·算法·leetcode·c#
xywww1685 小时前
大模型 API 选型实战:GPT、Gemini、Claude 接入时该看哪些指标?
运维·服务器·人工智能·python·gpt·langchain
夜雪一千10 小时前
Python enumerate() 函数完整详解:遍历同时获取索引,告别手动计数
服务器·windows·python
能有时光10 小时前
PyTorch KernelAgent 源码解读 ---(4)--- ExtractorAgent
人工智能·pytorch·python
_Jimmy_11 小时前
Python 协程库如何使用以及有哪些使用场景
python
aqi0011 小时前
15天学会AI应用开发(十七)使用LangGraph实现会话记忆功能
人工智能·python·大模型·ai编程·ai应用
第一程序员12 小时前
Rust Agent 子进程执行:Command 之前,先定义输入和超时
python·rust·github
skywalk816312 小时前
设计并实现段言的 C FFI 绑定机制 @Trae
c语言·开发语言·python·编程
weixin_BYSJ198712 小时前
SpringBoot + MySQL 乒乓球运动员信息管理系统项目实战--附源码04954
java·javascript·spring boot·python·django·flask·php