前言
您是否注意过以下规律?
-
自然数 的平方 似乎总是满足以下两个等式中的某一个
-
自然数 的 次方 似乎总是满足以下两个等式中的某一个
-
自然数 的 次方似乎总是满足以下等式
如果这些规律成立,我们能否证明它们?如果这些规律不(都)成立,我们可以找到反例吗?
本文会提供可以对此进行探索的 代码(包括图形化版本和非图形化版本)。其中图形化版本的示例效果如下 👇
本文会在此基础上,对费马小定理进行介绍。
正文
用较小的质数进行试验
不借助图形化界面
我们可以用较小的质数 进行一些试验。我想通过 代码来自动生成下图展示的这种表格 ⬇️
"""
A tool for generating modular powers for a given prime number p.
"""
from typing import List
class SimpleModularPowDisplayer:
"""A class to display modular powers for a given positive integer m.
Attributes:
m (int): a positive integer (greater than or equal to 2)
"""
def __init__(self, m: int) -> None:
"""Initialize the displayer with a positive integer m.
Args:
m (int): a positive integer
Raises:
ValueError: when m is not an integer or m < 2
"""
if not isinstance(m, int) or m < 2:
raise ValueError("m must be an integer that is greater than or equal to 2")
self.m = m
def _get_header_elements(self) -> List[str]:
"""Generate the header elements for the Markdown table.
Returns:
List[str]: Header cells for the Markdown table (LaTeX-formatted)
"""
header = [f"$a$"]
for exp in range(2, self.m + 1):
header.append(f"$a^{{{exp}}} \\bmod {self.m}$")
return header
def print_header(self) -> None:
"""Print the header row of the Markdown table."""
elements = self._get_header_elements()
print(f"| {' | '.join(elements)} |")
def _get_row_elements(self, a: int) -> List[str]:
"""Generate the elements for a single row of the Markdown table.
Args:
a (int): the base number (0 <= a < m)
Returns:
List[str]: Cell values for the row (LaTeX-formatted)
"""
elements = [f"${a}$"]
current = a
for _ in range(2, self.m + 1):
current = (current * a) % self.m
elements.append(f"${current}$")
return elements
def print_row(self, a: int) -> None:
"""Print a single row of the Markdown table for a given base a.
Args:
a (int): an integer that satisfies the condition 0 <= a < m
Raises:
ValueError: If a is outside the range [0, m-1]
"""
if not 0 <= a < self.m:
raise ValueError(f"a should be inside [0, {self.m-1}]")
elements = self._get_row_elements(a)
print(f"| {' | '.join(elements)} |")
def show_markdown_result(self) -> None:
"""Print the complete Markdown table for modular powers of m."""
print(f"#### $m={self.m}$")
self.print_header()
# Print the separator row
separator = ["---"] * len(self._get_header_elements())
print(f"| {' | '.join(separator)} |")
# Traverse all numbers from 0 to m-1
for a in range(self.m):
self.print_row(a)
if __name__ == "__main__":
# A list with prime numbers
PRIME_LIST = [2, 3, 5, 7, 11]
for prime in PRIME_LIST:
try:
displayer = SimpleModularPowDisplayer(prime)
displayer.show_markdown_result()
print("\n")
except ValueError as e:
print(f"Error occurred when processing p={prime}: {e}")
请将上述代码保存为 print_markdown_table.py。用如下命令可以运行 print_markdown_table.py
python3 print_markdown_table.py
运行结果包含了 个 markdown 表格 ⬇️
借助图形化界面
直接看表格,感觉互动性有点差,我用 豆包 和 trae 写了如下的代码
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
开始时的效果如下 ⬇️
我们可以选择不同的 值,来查看对应的计算结果。以 为例,效果如下 ⬇️
借助图形化界面来探索规律,感觉方便些。
找规律
通过观察以上 个 markdown 表格以及使用图形化界面,我们可以发现一些规律 ⬇️ (也许您还能发现其他规律)
- 对满足 的整数 而言, 似乎总是成立 ( 为任意正整数)
- 对满足 的整数 而言, 似乎总是成立
我们试试证明它们。
规律一的证明
我们尝试证明以下规律
对满足 的整数 而言, 总是成立 ( 为任意正整数, 为任意质数)
的情况最简单,我们先看看这种情况 ⬇️
因此
所以
如果我们能将以下 个等式左边右边各自相乘,就可以得到要证的等式 👉
但是能否这样做乘法呢?答案是肯定的。
如果 和 成立,那么以下两个命题成立
也就是说,存在整数 使得以下两个等式成立
所以
那么
由此可见,在模 运算中,我们确实可以在 运算两边做乘法。
于是 对任意正整数 都成立。
规律二的证明
说明:本小节的证明来自 灵茶山艾府 所写的 分享丨模运算的世界:当加减乘除遇上取模(模运算恒等式/费马小定理/组合数) 一文(有改动)。由于其中所讲的都是经典的数学知识,我想其中的证明应该不存在版权问题,但引用其中的内容时,应该注明出处。于是我写了这么一段说明
我们的目标是,证明或者证伪以下命题
对 的整数 而言,。
首先,对任意整数 , 可以用二项式定理进行展开。
而
- 当 时,
- 当 时, 成立,由算术基本定理可得,
- 当 时,
所以
其中 所表示的各项均可被 整除。
于是我们可以得出
令 就会得到 ⬇️
当 时, 显然成立。
假设 时, 成立( 是某个自然数)。也就是说, 成立。
那么 时,
其中 表示某个整数。继续化简,可以得到 ⬇️
注意到以下两个命题成立
那么 成立,也就是说, 成立。
运用数学归纳法,可以得到 对任意自然数 都成立。
- 当 时, 显然成立
- 当 时,由于 ,那么 。利用 算术基本定理 一文中提到的 引理一,可以得出 ,即 。⬅️ 这就是费马小定理
费马小定理的简单应用
我们回到 背景 中所提到的 个问题
问题 1
自然数 的平方 是否总是满足以下两个等式中的某一个
当 时, 显然成立。当 时,由费马小定理可得,。问题 解决了。
问题 2
自然数 的 次方 是否总是满足以下两个等式中的某一个
当 时, 显然成立。当 时,由费马小定理可得,。问题 解决了。
问题 3
自然数 的 次方是否总是满足
当 时,,所以 ,那么 成立。
当 时,由费马小定理可得 ,所以 也成立。
也就是说对任意自然数 , 都成立。
考虑到 和 的奇偶性相同,那么 。
由算术基本定理可知,当 时, 的质因数分解结果是唯一的,那么这个分解结果中至少包含一个 ,至少包含一个 。所以 。而 时, 显然成立。所以对任意自然数 而言,。换句话说 对任意整数 成立。
问题 解决了。
参考资料
- 灵茶山艾府 所写的 分享丨模运算的世界:当加减乘除遇上取模(模运算恒等式/费马小定理/组合数)
- 我自己写的 算术基本定理