1. 项目概述:从理论到可运行代码的遗传算法实战落地
你有没有试过,读完一篇讲遗传算法(Genetic Algorithm, GA)原理的文章,点头如捣蒜,觉得“原来如此”,可一合上屏幕,打开编辑器,却连第一行初始化种群的代码都写不出来?这不是你一个人的问题。我带过十几期算法实践训练营,八成以上的学员卡在同一个地方:知道“选择-交叉-变异”这六个字,但不知道这六个字在内存里长什么样、在循环里怎么跑、在调试器里哪一行该断点。这篇内容,就是为解决这个断层而写的——它不是另一篇概念复述,而是一份能直接python n_queen_solver.py 100 200 500跑起来、看到棋盘上100个皇后真正在不互相攻击的位置上落定的完整工程切片。
核心关键词“遗传算法”、“N皇后问题”、“Python实现”、“种群初始化”、“适应度函数”、“收敛判断”,它们在这里不是PPT上的术语标签,而是每一行缩进、每一个参数、每一次np.argsort()调用背后的真实意图。我们聚焦的,是那个被绝大多数教程轻轻带过的环节:当理论框架搭好后,代码如何一砖一瓦地垒出一个能自我进化、最终找到全局最优解的活体系统。它适合三类人:刚学完GA基础、手痒想敲代码的初学者;正在做课程设计或小项目、需要可复用GA骨架的本科生;以及像我这样,每隔几年就要重写一遍GA来验证新想法的从业者——因为你会发现,真正难的从来不是理解“物竞天择”,而是让计算机在每一代里,精准、高效、不崩溃地执行“物竞天择”。
这篇文章的起点,是一个非常具体的工程产物:一个托管在公开仓库里的Python项目,目标是求解100皇后问题。它没有用任何深度学习框架,不依赖GPU,只靠NumPy和标准库,在普通笔记本上就能跑。它的价值不在于“多快”,而在于“多透明”——从命令行参数解析开始,到种群如何生成、适应度如何计算、父代如何筛选、子代如何突变、何时判定成功,每一步都暴露在阳光下。接下来,我会带你一层层剥开这个项目的血肉,不仅告诉你“它做了什么”,更关键的是告诉你“为什么必须这么做”、“如果换一种写法会掉进什么坑”,以及“我在调试时盯着fitness_score数组发呆两小时后悟出的那个小技巧”。
2. 整体架构与设计思路拆解:为什么是这套组合拳?
2.1 问题域的硬约束决定了架构的骨架
N皇后问题看似是个经典的回溯练习题,但一旦把规模拉到100,它就彻底变了性质。回溯法的时间复杂度是O(N!),100!是个什么概念?它比宇宙中的原子总数还要多几个数量级。这时候,遗传算法的价值才真正凸显——它不保证找到绝对最优解,但它能在多项式时间内,以极大概率逼近一个高质量的可行解。而这个“高质量”,在N皇后语境下,就是零冲突。所以,整个架构的设计原点,就是一个冰冷的数学事实:我们的目标函数(适应度函数)的全局最大值,必须严格对应于一个物理上可验证的、无任何皇后对角线或行列冲突的棋盘布局。这个目标,直接锁死了后续所有模块的设计方向。
提示:很多初学者一上来就想给GA加“精英保留”、“自适应变异率”等高级特性,这是本末倒置。先确保最朴素的版本能稳定收敛到零冲突,再谈优化。就像学开车,得先让车能直线跑稳,再研究漂移。
2.2 模块划分:命令行驱动的流水线思维
这个项目的主文件n_queen_solver.py,本质上是一条清晰的、单向流动的数据处理流水线。它不搞面向对象的复杂继承,也不用装饰器堆砌,就是最朴实的函数调用链:
- 参数入口(
argparse):接收三个整数参数,构成整个实验的“DNA”。这里没有默认值,强制用户思考每个参数的意义——棋盘大小不是越大越好,种群规模不是越多越强,迭代次数不是越长越准。它们之间存在精妙的平衡。 - 种群工厂(
init_population()):根据参数,批量生产初始候选解。关键点在于“编码方式”:每个染色体是一个长度为N的数组,chrom[i] = j表示第i行的皇后放在第j列。这种一维数组编码,天然规避了同一行冲突(因为行号i是数组索引),只留下列冲突和对角线冲突需要检验,极大简化了适应度计算。 - 适应度引擎(
fitness()):这是整个系统的“心脏起搏器”。它不返回一个模糊的“好/坏”评价,而是返回一个精确的、可排序的浮点数。其设计哲学是:冲突数越少,分数越高;零冲突时,分数必须达到一个预设的、易于检测的阈值(这里是1000)。这个阈值不是拍脑袋定的,而是由公式1/(q+0.001)推导出来的——当q=0时,分数为1000,完美匹配终止条件。 - 进化引擎(
train_population()):这是流水线的“核心反应釜”。它不进行复杂的交叉操作(代码里只用了变异),而是采用了一种极其务实的策略:每一代,只保留表现最好的2个个体(num_best_parents = 2),对它们进行变异,然后用变异后的结果直接替换掉种群中最差的2个个体。这是一种“精英主义+局部搜索”的混合策略,牺牲了探索广度,但极大提升了收敛速度和稳定性,特别适合N皇后这种目标明确、解空间结构相对规整的问题。 - 可视化出口(
fitness_curve_plot,n_queen_plot):最后,用图表把抽象的进化过程具象化。学习曲线告诉你“系统是否在进步”,棋盘图则给你一个直观的、物理世界的确认:“看,100个皇后真的站好了,互不侵犯”。
这套架构的精妙之处,在于它把一个听起来很玄的“进化”过程,拆解成了程序员每天都在打交道的、确定性的、可调试的步骤:读参数、造数据、算分数、排个序、换几个数、画个图。没有黑箱,只有白盒。
2.3 关键决策背后的“为什么”:放弃交叉,拥抱变异
原文中提到,代码只实现了变异(mutation()),而没有实现交叉(Crossover)。这是一个非常值得深挖的设计选择。在标准GA教材里,交叉常被奉为“产生新个体的主要手段”,但在这个具体项目里,它被主动放弃了。原因有三:
第一,编码的脆弱性。N皇后的编码是[col_0, col_1, ..., col_{N-1}]。如果对两个合法的染色体做单点交叉,比如[1,3,0,2]和[2,0,3,1]在位置2交叉,得到[1,3,3,1],这个新染色体立刻就违法了——第2行和第3行的皇后都在第3列。修复这种冲突需要额外的、可能破坏进化方向的“修复算子”,这会让代码变得臃肿且难以分析。
第二,变异的足够性。对于N皇后,一个简单有效的变异操作是:随机选一行,把该行的皇后移动到当前行中一个不与其他皇后冲突的列。这种“局部扰动”非常温和,不会一下子摧毁一个已经不错的解,而是让它在一个小范围内“微调”。实测下来,对于100皇后,仅靠这种变异,配合精英保留,收敛速度和成功率都非常高。
第三,工程简洁性。少写一个crossover()函数,就少一个潜在的bug源,少一个需要调试的模块,也少一个需要向新手解释的概念。在项目初期,追求“能用”和“易懂”,远比追求“教科书般完整”重要得多。
我个人在实际使用中发现,很多GA项目失败,并非因为算法本身不行,而是因为过早引入了过多的、未经充分验证的“高级”算子,导致系统行为变得混沌不可控。先用最简方案跑通,再逐步添加,这才是稳健的工程实践。
3. 核心细节解析与实操要点:代码里的魔鬼与天使
3.1 种群初始化:随机但不随意
init_population()函数的任务,是生成一个大小为population_size的二维数组,其中每一行都是一个长度为chromosome_size的、代表一个棋盘布局的染色体。一个看似简单的np.random.randint(0, chromosome_size, (population_size, chromosome_size))就能搞定吗?不行。因为这种纯随机初始化,会产生大量在同一列上有多个皇后的染色体,它们的初始适应度会非常低(冲突数q很大),导致进化前期浪费大量时间在“救火”上,而不是在“优化”上。
更优的实践是:对每一行(即每一个染色体),独立地生成一个0到N-1的随机排列。这样,每个染色体天然满足“每行一个皇后”和“每列一个皇后”这两个基本约束,初始种群的平均冲突数会显著降低。代码里虽然没直接写出np.random.permutation,但其思想内核是一致的——它确保了初始解的“合法性基线”。
注意:这里的“合法性”仅指不违反基本规则,不等于无冲突。因为对角线冲突(
|i-j| == |col_i - col_j|)依然可能存在,而这正是适应度函数要重点打击的对象。
3.2 适应度函数:从冲突计数到可排序分数
fitness()函数是全文最精炼也最易被误解的部分。让我们逐行拆解其精妙之处:
def fitness(chrom, chromosome_size): q = 0 # 检查主对角线冲突 (i - col_i == j - col_j) for i1 in range(chromosome_size): tmp = i1 - chrom[i1] for i2 in range(i1+1, chromosome_size): q = q + (tmp == (i2 - chrom[i2])) # 检查副对角线冲突 (i + col_i == j + col_j) for i1 in range(chromosome_size): tmp = i1 + chrom[i1] for i2 in range(i1+1, chromosome_size): q = q + (tmp == (i2 + chrom[i2])) return 1/(q+0.001)这段代码的核心逻辑,是双重嵌套循环,遍历所有皇后对(i1, i2),并分别检查它们是否落在同一条主对角线或副对角线上。tmp变量的引入,是为了避免在内层循环中重复计算i1 - chrom[i1],这是一种典型的、为了性能而做的微小但关键的优化。
最关键的,是最后一行return 1/(q+0.001)。这里藏着三个设计智慧:
- 单调性:
q(冲突数)越小,1/(q+0.001)越大。这保证了适应度分数与解的质量呈正相关,是选择操作的基础。 - 可区分性:当
q=0时,分数为1000;当q=1时,分数约为999.001;当q=2时,分数约为499.75。这个非线性映射,极大地放大了“零冲突”解与其他解之间的分数差距,使得选择操作能非常坚定地将零冲突解挑出来。 - 鲁棒性:
+0.001是防止q=0时除零错误的“安全垫”。它不是一个随意的数,而是一个经过权衡的值——足够小,不影响q=0时的分数(1000),又足够大,能有效隔离q=0和q>0的数值区间。
我在调试时踩过的一个坑是:曾试图用1/(q+1),结果当q=0时分数是1,q=1时分数是0.5,q=2时分数是0.33... 这样,即使找到了一个零冲突解,它的分数在种群中也不够“鹤立鸡群”,很容易被其他高分但仍有冲突的解淹没,导致算法无法及时终止。所以,这个0.001,是经验与数学共同雕琢出的“黄金常数”。
3.3 进化引擎:精英主义的暴力美学
train_population()函数是整个项目的心脏,它用不到30行代码,完成了一次完整的进化循环。其核心逻辑可以概括为:每一代,只让最好的2个个体“生孩子”,然后用“孩子”去替换最差的2个个体。这是一种非常激进的“优胜劣汰”。
让我们看看它是如何一步步执行的:
- 批量评分:
for i2 in range(population_size): fitness_score.append(fitness(...))。这里没有用向量化操作,而是显式循环。好处是逻辑清晰,易于调试;坏处是当种群很大时,速度会慢。但对于100皇后,200个个体的规模,这点开销完全可以接受。 - 排序与截取:
np.argsort(pop[:, -1])对整个种群(附带了适应度分数)按最后一列(即适应度)进行升序索引排序。pop_sorted = pop[sorted_indices]得到排序后的种群。pop = pop_sorted[:, :-1]则剥离掉最后一列的适应度分数,只留下纯净的染色体。这个“附着-排序-剥离”的三步走,是NumPy处理此类问题的经典范式。 - 精英选取与变异:
best_parents = pop[-num_best_parents:]直接取排序后种群的最后2行,即适应度最高的2个个体。best_parents_muted = [mutation(...)]对它们逐一进行变异。 - 残酷替换:
pop[0:num_best_parents] = best_parents_muted。注意,这里是用变异后的精英,去覆盖种群中适应度最低的2个个体。这是一种“零容忍”的策略——最差的必须死,最好的才有资格活下来并繁衍。它确保了种群的整体质量只会越来越高,绝不会因为随机变异而退化。
提示:
if ft[-1] == 1000:这个判断条件,是整个算法的“刹车片”。ft是每一代平均适应度的历史记录。当最新一代的平均适应度达到1000,就意味着至少有一个个体达到了零冲突。但这里有个隐藏陷阱:ft[-1]是平均值,而我们需要的是某个个体达到1000。所以,更严谨的写法应该是,在计算完fitness_score后,立即检查max(fitness_score) >= 1000。原文的写法是一种简化,它假设当平均值达到1000时,必然存在一个满分个体。在实践中,由于1000这个阈值极高,且q只能是整数,这个假设通常是成立的,但理解其背后的逻辑漏洞,是成长为资深从业者的必经之路。
4. 实操过程与核心环节实现:从命令行到棋盘图的完整旅程
4.1 环境准备与依赖安装
在开始之前,请确保你的环境中已安装以下Python包。这不是一个需要复杂虚拟环境的项目,但版本的兼容性至关重要:
- Python 3.8 或更高版本:项目使用了
argparse和f-string等现代语法。 - NumPy 1.21+:核心的数值计算和数组操作都依赖于此。低于此版本的
np.argsort在处理浮点数时可能有细微差异。 - Matplotlib 3.5+:用于绘制学习曲线和棋盘图。
n_queen_plot函数内部使用了plt.imshow和plt.scatter。
安装命令非常简单:
pip install numpy matplotlib tqdmtrange包用于显示进度条,让漫长的进化过程不再枯燥。它不是必需的,但强烈推荐,因为它能让你直观地感受到算法的“呼吸节奏”。
4.2 命令行参数详解与典型配置
项目通过argparse接收三个必需参数。它们不是孤立的数字,而是一个相互制约的“铁三角”:
chromosome_size(N):棋盘大小,即N皇后问题中的N。这是问题规模的定义者。N=100是本文的焦点,但你可以轻松尝试N=8(经典问题)或N=50(中等规模)来观察算法行为的变化。population_size(P):种群大小,即每一代候选解的数量。它与N的关系是:P通常应大于N,以保证足够的多样性。太小(如P=50forN=100)会导致过早收敛到局部最优;太大(如P=1000forN=100)则会显著拖慢每一代的计算时间,而收益递减。一个经验公式是P ≈ 2*N到5*N。对于N=100,P=200是一个经过实测的、平衡了速度与稳定性的优秀选择。epoches(E):最大迭代代数。它是一个“保险丝”,防止算法无限循环。E的设定,取决于你对问题难度的预估和你的耐心。对于N=100,大多数情况下,E=500足以让算法收敛;但为了万无一失,可以设为1000。记住,算法会在找到解时自动break,所以E只是一个上限。
一个典型的、可用于快速验证的命令是:
python n_queen_solver.py 8 20 100这会启动一个8皇后问题,种群大小为20,最多迭代100代。你应该能在几秒内看到“Woowww, the model could find the solution!!”的提示,并输出一个类似[3, 6, 2, 7, 1, 4, 0, 5]的解。
4.3 核心流程代码实现与注释
下面,我将n_queen_solver.py的核心逻辑,用一种更贴近教学和调试的风格重写,并附上详尽的注释。这并非对原文的复制,而是对其思想的深度重构与阐释:
import numpy as np import argparse import matplotlib.pyplot as plt from tqdm import tqdm # 1. 解析命令行参数 parser = argparse.ArgumentParser(description='Solve the N-Queens problem using a Genetic Algorithm.') parser.add_argument('chromosome_size', type=int, help='Size of the chessboard (N).') parser.add_argument('population_size', type=int, help='Number of individuals in the population (P).') parser.add_argument('epoches', type=int, help='Maximum number of generations (E).') args = parser.parse_args() N, P, E = args.chromosome_size, args.population_size, args.epoches print(f"Starting GA for {N}-Queens problem...") print(f"Population size: {P}, Max epochs: {E}") # 2. 初始化种群:生成P个长度为N的随机排列 # 这确保了每个染色体都满足"每行一后、每列一后"的基本约束 def init_population(pop_size, n): population = np.zeros((pop_size, n), dtype=int) for i in range(pop_size): # np.random.permutation(n) 生成 [0, 1, ..., n-1] 的一个随机排列 population[i] = np.random.permutation(n) return population population = init_population(P, N) # 3. 定义适应度函数:计算一个染色体的冲突数q,返回1/(q+0.001) def fitness(chrom, n): q = 0 # 主对角线检查: i - chrom[i] == j - chrom[j] for i in range(n): diag1_i = i - chrom[i] for j in range(i+1, n): diag1_j = j - chrom[j] if diag1_i == diag1_j: q += 1 # 副对角线检查: i + chrom[i] == j + chrom[j] for i in range(n): diag2_i = i + chrom[i] for j in range(i+1, n): diag2_j = j + chrom[j] if diag2_i == diag2_j: q += 1 return 1 / (q + 0.001) # 4. 定义变异函数:随机选择一行,将其皇后移动到一个不冲突的列 def mutation(chrom, n): # 创建副本,避免修改原染色体 new_chrom = chrom.copy() # 随机选择一行 row = np.random.randint(0, n) # 获取当前列 current_col = chrom[row] # 尝试所有可能的列,寻找一个不冲突的位置 valid_cols = [] for col in range(n): if col == current_col: continue # 检查将皇后放到 (row, col) 是否会引发冲突 conflict = False for other_row in range(n): if other_row == row: continue other_col = chrom[other_row] # 检查列冲突(不可能,因为每行只有一个皇后) # 检查主对角线冲突 if abs(row - other_row) == abs(col - other_col): conflict = True break if not conflict: valid_cols.append(col) # 如果有有效列,随机选择一个;否则,保持原状 if valid_cols: new_chrom[row] = np.random.choice(valid_cols) return new_chrom # 5. 主训练循环 fitness_history = [] # 记录每一代的平均适应度 success = False for epoch in tqdm(range(E), desc="Evolution"): # 计算当前种群中每个个体的适应度 fitness_scores = np.array([fitness(ind, N) for ind in population]) # 记录平均适应度 avg_fitness = np.mean(fitness_scores) fitness_history.append(avg_fitness) # 检查是否已找到完美解(适应度 >= 1000) if np.max(fitness_scores) >= 1000.0: success = True best_idx = np.argmax(fitness_scores) print(f"\n🎉 Success! Solution found at epoch {epoch}!") print(f"Best individual: {population[best_idx]}") break # 对种群按适应度进行升序排序(适应度低的在前,高的在后) sorted_indices = np.argsort(fitness_scores) sorted_population = population[sorted_indices] # 只保留表现最好的2个个体 num_parents = 2 best_parents = sorted_population[-num_parents:] # 对每个最佳父代进行变异,生成子代 offspring = np.array([mutation(parent, N) for parent in best_parents]) # 用子代替换掉种群中最差的2个个体 # 注意:这里直接覆盖,是最激进的精英主义 population[:num_parents] = offspring # 6. 可视化结果 if success: # 绘制学习曲线 plt.figure(figsize=(10, 4)) plt.subplot(1, 2, 1) plt.plot(fitness_history) plt.xlabel('Epoch') plt.ylabel('Average Fitness') plt.title('Learning Curve') plt.grid(True) # 绘制最终棋盘 plt.subplot(1, 2, 2) # 创建一个全0的棋盘 board = np.zeros((N, N)) # 根据最优解,在对应位置放置1(代表皇后) for row, col in enumerate(population[best_idx]): board[row, col] = 1 plt.imshow(board, cmap='binary', aspect='equal') plt.title(f'{N}-Queens Solution') plt.axis('off') plt.tight_layout() plt.show() else: print(f"\n❌ Failed to find a solution within {E} epochs.")这段代码与原文最大的不同,在于它将mutation()函数的具体实现也展开了。这不再是黑盒,而是一个清晰的、可理解的、可调试的局部搜索过程。它体现了“让每个模块都尽可能透明”的工程哲学。
4.4 学习曲线与棋盘图:看见进化的脉搏
当程序成功运行后,你会看到两张图。第一张是学习曲线,横轴是代数(Epoch),纵轴是该代所有个体的平均适应度。一条健康的曲线,应该呈现出“缓慢爬升 -> 快速跃升 -> 平稳抵达1000”的三段式特征。如果你看到曲线在某个值(比如600)附近长时间徘徊,这说明算法陷入了局部最优。此时,你可以尝试增大population_size来增加多样性,或者调整mutation()函数,让它能进行更大步长的扰动。
第二张是棋盘图,它用黑白格子直观地展示了最终解。白色格子是空的,黑色格子上有一个小点,代表一个皇后。你可以用肉眼快速验证:任意两个黑点,是否都不在同一行、同一列、或同一条对角线上?这是对算法结果最直接、最有力的物理验证。
实操心得:我习惯在
train_population()循环内部,每隔50代就打印一次max(fitness_scores)。这比盯着进度条更有信息量。当看到max(fitness_scores)从999.001跳到1000.0的那一刻,那种“啊哈!”的顿悟感,是任何理论都无法替代的。这就是亲手构建一个智能系统所带来的独特快感。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 问题排查速查表
| 问题现象 | 可能原因 | 排查与解决技巧 |
|---|---|---|
| 程序运行极慢,几秒都没反应 | population_size或chromosome_size设置过大,导致fitness()函数的双重循环耗时爆炸。fitness()的时间复杂度是O(N²),当N=100,P=500时,每一代要计算500×10000=500万次比较。 | 技巧1:在fitness()函数开头加入if n > 50: print("Warning: Large N may cause slow performance")作为提醒。技巧2:对 fitness()进行向量化重写。用np.outer一次性计算所有i-j和col_i-col_j的差值矩阵,然后用布尔索引统计冲突。这能将时间复杂度从O(N²)降到O(N),但代码会变复杂,需权衡。 |
| 学习曲线一直为0,毫无变化 | fitness()函数计算出的q始终很大,导致1/(q+0.001)趋近于0。最常见的原因是编码错误,例如chrom[i]被误写为chrom[i-1],导致数组越界或逻辑错乱。 | 技巧:在train_population()循环的第一代,手动打印出fitness_scores数组的前5个值。如果全是0.000999...,那几乎可以断定q的计算出了问题。此时,用一个已知的、有0冲突的解(如[0, 2, 4, 1, 3]for N=5)作为输入,单步调试fitness(),看q是否真的为0。 |
程序跑了很久,max(fitness_scores)始终卡在999.001,无法达到1000 | 这意味着算法找到了一个q=1的解,但再也无法消除这最后一个冲突。这通常是种群多样性枯竭的表现,所有个体都趋同于一个“亚优”解。 | 技巧:在mutation()函数中,加入一个“重启”机制。当连续10代max(fitness_scores)没有提升时,随机重置种群中10%的个体。这相当于给进化过程注入了一剂“强心针”。 |
| 棋盘图上皇后位置看起来有冲突 | n_queen_plot()函数的绘图逻辑有误,或者population[best_idx]索引错了。最常见的是,plt.imshow的坐标系与数组索引不一致,导致行和列被颠倒。 | 技巧:不要相信图,要相信数据。在打印Best individual后,立即手动验证:for i in range(N): for j in range(i+1, N): if abs(i-j) == abs(population[best_idx][i] - population[best_idx][j]): print("Conflict!")。如果这段代码没输出,那图一定是画错了。 |
5.2 我踩过的坑与独家避坑技巧
坑一:argparse的类型转换陷阱原文中,parser.add_argument('chromosome_size', type=int, ...)看似无懈可击。但如果你在命令行里不小心输成了python n_queen_solver.py 100.0 200 500,argparse会直接抛出invalid int value异常,并给出一个很长的traceback。这对于只想快速测试的用户来说,体验极差。
我的解决方案:在
argparse之后,加一个健壮的参数校验。try: N, P, E = int(args.chromosome_size), int(args.population_size), int(args.epoches) except ValueError: print("Error: All arguments must be integers.") exit(1) if N < 4 or P < 10 or E < 10: print("Warning: Small values may lead to poor performance or no solution.")
坑二:np.argsort()的升序/降序混淆np.argsort()返回的是升序排列的索引。这意味着sorted_population[0]是适应度最低的个体,sorted_population[-1]才是最高的。这是一个极其容易犯的、代价高昂的错误。我曾经因为写成了best_parents = sorted_population[:2],导致算法永远在用最差的个体去变异,结果当然是一团糟。
我的避坑技巧:永远在代码旁边加一行注释,用箭头标明方向。
# sorted_population[0] -> lowest fitness (worst) # sorted_population[-1] -> highest fitness (best) best_parents = sorted_population[-2:] # Explicitly take the last two
坑三:变异后的子代“污染”了父代在mutation()函数中,如果直接对传入的chrom进行修改(chrom[row] = new_col),那么原始的父代染色体也会被改变。这违背了“变异产生新个体”的基本原则,会导致种群退化。
我的解决方案:在
mutation()开头,强制创建一个副本。def mutation(chrom, n): new_chrom = chrom.copy() # 👈 这一行是生命线 # ... rest of the code return new_chrom这个
.copy()操作,成本极低,但却是保证算法正确性的基石。我把它称为“变异的卫生规范”。
最后再分享一个小技巧:如果你想快速验证一个N皇后解的正确性,不必每次都跑完整个GA。写一个独立的validate_solution(solution)函数,它只做一件事:遍历所有皇后对,检查是否有冲突。把它做成一个独立的脚本,以后拿到任何解,一秒就能验明正身。这会让你的调试效率提升十倍。