Python:遗传算法解决八皇后问题

1 八皇后问题

有一个8乘8的棋盘,现在要将八个皇后放到棋盘上,满足:对于每一个皇后,在自己所在的行、列、两个对角线都没有其他皇后。

在这里插入图片描述


2 遗传算法简介

遗传算法介绍。

2.1 遗传算法的流程图

在这里插入图片描述

2.2 遗传算法的详细步骤

在这里插入图片描述


3 思考过程

以下步骤:

1、染色体编码。首先规定同列只能出现一个皇后。每一个棋盘,对应于一个长度为8的串,每一个数的范围是[1, 8],第k个数字所代表的含义是第k列中皇后所在的行数,如[3,2,5,4,3,2,1,3]代表棋盘上从第一列到第八列,皇后所摆放的行数分别为第3,2,5,4,3,2,1,3行。

2、初始种群设定。从待选序列中选择n个序列,作为初始种群的n个个体。

3、适应度函数设定。适应度函数设为【攻击的皇后对数n】,0<=n<=28。最优解要满足【攻击的皇后对数n为0】。

4、遗传操作设计。(1)选择:使用轮盘赌选择方法。(2)交叉:种群中的个体随机地两两相互交叉,但是不能重复交叉,即每一个个体只能与唯一一个其他个体相互交叉,共有n/2个交叉对(n为种群规模),且这些交叉对里面任意一个个体均会出现且仅出现一次。(3)变异:检查种群中的每个个体,看是否有重复的数字,有的话去掉重复的数字只剩一个。如个体12746737有三个7,意味着第七行有三个皇后,一定不符合要求,所以要变异。此时仅保留最后一个i,再用个体中在1到8中未出现的数字替换重复的数字。如上面的12746737缺少58,故将58【随机地】放在前两个7的位置,如可以将8放在第3个位置(第一个7的位置),5放在第6个位置(第二个7的位置),得到12846537。


4 我的程序

4.1 程序1

程序1:generate_init_seq.py。如果8个皇后在8*8的棋盘上可以随意摆放,当然是不能在同一个格子里放超过一个皇后的情况下,本来所有需要测试是否满足要求的序列共有64*63*…*57=1.78e+14个,这太多了。所以此程序的工作是筛选出那些【每行与每列都只有一个皇后存在】的序列,这样的序列有8*7*6*5*4*3*2=40320个,可以大大缩减后续程序的运行时间,而且这样在后面处理每个序列时只需要考虑两条对角线上和所在的行上有没有其他皇后即可(不用考虑列)。如下:

import json
import time

start = time.time()

seq = [[i, j, k, l, m, n, o, p]
       for i in range(1, 9)
       for j in range(1, 9)
       for k in range(1, 9)
       for l in range(1, 9)
       for m in range(1, 9)
       for n in range(1, 9)
       for o in range(1, 9)
       for p in range(1, 9)
       if all([i != j, i != k, i != l, i != m, i != n, i != o, i != p,
               j != k, j != l, j != m, j != n, j != o, j != p,
               k != l, k != m, k != n, k != o, k != p,
               l != m, l != n, l != o, l != p,
               m != n, m != o, m != p,
               n != o, n != p,
               o != p])]  # 筛选出【每行与每列都只有一个皇后】的序列

print('有' + str(len(seq)) + '个可能的序列')

with open('seq.json', 'w') as file_object:
    json.dump(seq, file_object)

end = time.time()

print('Successful!')
print('已将生成的序列存储到文件seq.json中,用时' + str('%.2f' % (end - start)) + 's')

输出如下。注意会生成一个文件seq.json,我上传到了csdn上,你可以看看这里,你也可以运行程序1,就可以在自己电脑上得到一个文件,除了运行时间有区别,其他输出和我这个是一样的:

40320个可能的序列
Successful!
已将生成的序列存储到文件seq.json中,用时23.78s

4.2 程序2

程序2:functions.py。包括四个函数:calculate_fitness, selection, crossover, mutation,分别完成遗传算法中的适应度计算、选择、交叉、变异操作。如下:

import numpy as np
import random

def calculate_fitness(s):
    """
    计算个体的适应度,大小设为【攻击的皇后对数n】,0<=n<=28
    最优解要满足【攻击的皇后对数n为0】
    """
    a = np.array([0] * 81)  # 在开始时创建一个有81个0的一维数组
    a = a.reshape(9, 9)  # 改为9*9二维数组。为方便后面使用,只用后八行和后八列的8*8部分,作为一个空白棋盘
    n = 0  # 适应度大小初始化为0

    for i in range(1, 9):
        a[s[i - 1]][i] = 1  # 根据【个体】,即序列从第一列到最后一列的顺序,在对应位置放一个皇后,生成当前序列对应的棋盘

    for i in range(1, 9):  # 检查当前序列的八个皇后在各自的行及两条对角线上是否有其他皇后,不需判断同列是否有其他皇后,因为编码方式决定了同一列有且只有一个皇后
        for k in list(range(1, i)) + list(range(i + 1, 9)):  # 检查每个皇后各自所在的行上是否有其他皇后
            if a[s[i - 1]][k] == 1:  # 有其他皇后
                n += 1
        t1 = t2 = s[i - 1]
        for j in range(i - 1, 0, -1):  # 看左半段的两条对角线
            if t1 != 1:
                t1 -= 1
                if a[t1][j] == 1:
                    n += 1  # 正对角线左半段上还有其他皇后

            if t2 != 8:
                t2 += 1
                if a[t2][j] == 1:
                    n += 1  # 次对角线左半段上还有其他皇后

        t1 = t2 = s[i - 1]  # 继续检查右半段的两条对角线
        for j in range(i + 1, 9):  # 看右半段的两条对角线
            if t1 != 1:
                t1 -= 1
                if a[t1][j] == 1:
                    n += 1  # 正对角线右半段上还有其他皇后

            if t2 != 8:
                t2 += 1
                if a[t2][j] == 1:
                    n += 1  # 次对角线右半段上还有其他皇后
    return int(n/2)  # 返回n/2,因为A攻击B也意味着B攻击A,因此返回n的一半

def selection(populations, percents):
    """
    对种群populations进行遗传算法的“选择”操作
    使用轮盘赌法
    """
    temp = 0
    for i in range(len(populations)):  # 累计百分数
        temp += percents[i]
        percents[i] = temp

    percents.insert(0, 0)  # 在开头插入一个零,方便后续比较
    new_populations = []  # 存放选择后的种群
    for i in range(len(populations)):  # 产生n个随机数,其中n为种群规模;根据随机数落在百分数列表中的位置去选择相应个体(轮盘赌)
        a = random.random()
        for pos in range(len(populations)):
            if percents[pos] <= a < percents[pos + 1]:
                new_populations.append(populations[pos])  # 存放选择到的个体
                break  # 找到a所在的范围就退出循环,测试下一个随机数
    return new_populations  # 返回新一代种群

def crossover(populations):
    """
    对种群populations进行“交叉”操作
    随机将种群中的个体分为两组,然后将两个组中对应位置的个体两两交叉,从每个个体的第5个数开始做交叉
    如第一组的第一个个体的后4个数和第二组的第一个个体的后4个数做交叉,第一组的第二个个体的后4个数和第二组的第二个个体的后4个数做交叉,以此类推
    """
    temp1 = []
    temp2 = []
    new_populations = []  # 存放交叉后的种群
    n = len(populations)
    for i in range(n):
        tmp = random.choice(populations)
        populations.remove(tmp)  # 移除已选到的个体,避免选到相同个体
        if i < n / 2:  # 随机选择种群中的个体,分成两组
            temp1.append(tmp)  # temp1保存前一半随机选到的个体
            continue
        temp2.append(tmp)  # temp2保存后一半随机选到的个体

    temp1 = temp1 + temp2  # 将两个列表合成为一个列表;这是一种技巧,方便后续交叉
    temp2 = temp1[::-1]  # 为temp1的倒序输出

    for i in range(n):  # 交叉
        new_populations.append(temp1[i][:4] + temp2[i][4:])
    return new_populations

def mutation(populations):
    """"
    对种群populations进行“变异”操作
    检查种群中的每个个体,看是否有重复的数字,因为重复的数字意味着在某一行有多于或等于2个皇后存在,故此个体不符合要求,需要变异
    如个体12746737有三个7,意味着第七行有三个皇后,不符合要求
    此时仅保留最后一个i,再用个体中在1到8中未出现的数字替换重复的数字
    如上面的12746737缺少58,故将58【随机地】放在前两个7的位置,如可以将8放在第3个位置(第一个7的位置),5放在第6个位置(第二个7的位置),得到12846537
    """
    new_populations = []  # 存放变异后的种群
    for item in populations:
        tmp = list({1, 2, 3, 4, 5, 6, 7, 8} - set(item))  # 找出个体中在1到8中未出现的数字,用来在后面代替重复的数字
        for i in range(1, 9):  # 检查数字i出现次数a是否为1,如果次数a>1,则找出前a-1个数字i的位置,用个体在1到8中未出现的数字【随机】进行替换
            a = item.count(i)
            if a > 1:
                for k in range(a - 1):  # 如果数字i出现的次数a大于1,就替换前a-1个的数字,仅留一个数字i
                    a = random.choice(tmp)
                    tmp.remove(a)
                    item[item.index(i)] = a
        new_populations.append(item)
    return new_populations

此程序无任何输出,只是定义了4个函数以供主程序调用。

4.3 程序3

程序3:main.py。为主程序,通过调用程序2的四个函数,完成遗传算法解决八皇后问题的全过程。如下:

import json
import random
from functions import calculate_fitness, selection, crossover, mutation

populations = []  # 存放初始种群及后代种群
n = 100  # 种群规模为n
iterations = 10  # 设置迭代次数,避免无限循环

with open('seq.json', 'r') as file_object:
    seqs = json.load(file_object)  # 载入保存好的序列

for i in range(n):
    tmp = random.choice(seqs)
    populations.append(tmp)  # 保存每次随机选到的“个体”(这里是一个序列)
    seqs.remove(tmp)  # 移除已选到的个体,避免选到相同个体

print('第0代(初始)种群中的前几个个体如下:')  # 只输出前几个个体,全部输出就太多了,不好看
print(', '.join(str(seq) for seq in populations[:5]))

for iteration in range(iterations + 1):  # 加上初始种群(第0代),最多到iterations+1代种群
    fitness = []  # 存放种群中每个个体的适应度
    good = []  # 保存满足条件的个体
    for item in populations:
        a = calculate_fitness(item)
        fitness.append(a)
        if a == 0:  # 若当前种群存在适应度为0的个体,则此个体为满足条件的一个解
            good.append(item)
    if good:  # 若good不为空时,代表当前代种群中有满足条件的解
        print('经过计算以及多次迭代,当前代,也就是第' + str(iteration) + '代种群中存在满足条件的个体。')
        print('当前代种群由下列个体组成(只输出前几项):\n' + ', '.join(str(p) for p in populations[:5]) + '\n满足条件的个体为:')
        print(', '.join(str(g) for g in good))
        break
    print('经过计算,第' + str(iteration)+'代不行')
    sum_of_fitness = sum(fitness)
    percents = [float(fitness[i]/sum_of_fitness) for i in range(n)]

    populations = mutation(crossover(selection(populations, percents)))  # 依次进行选择、交叉、变异操作

输出如下:

0代(初始)种群中的前几个个体如下:
[8, 5, 2, 1, 4, 7, 6, 3], [7, 4, 6, 3, 5, 1, 8, 2], [4, 2, 8, 5, 1, 7, 3, 6], [1, 3, 2, 7, 6, 4, 8, 5], [6, 5, 3, 4, 8, 1, 7, 2]
经过计算,第0代不行
经过计算,第1代不行
经过计算,第2代不行
经过计算以及多次迭代,当前代,也就是第3代种群中存在满足条件的个体。
当前代种群由下列个体组成(只输出前几项):
[8, 5, 1, 4, 3, 2, 7, 6], [1, 3, 6, 8, 2, 4, 5, 7], [3, 2, 1, 4, 5, 6, 7, 8], [2, 3, 4, 1, 5, 6, 7, 8], [2, 1, 3, 6, 4, 5, 8, 7]
满足条件的个体为:
[4, 1, 5, 8, 6, 3, 7, 2], [2, 7, 5, 8, 1, 4, 6, 3]

如果你在自己电脑上运行,得到的结果大部分应该是各代都不行,多运行几次以后才可能有解。我上面这个也是运行了好几次才得出有满足条件的个体。可能是我自己设定的遗传算法的选择、交叉、变异操作机制不好导致的。这种结果与迭代次数无关,我试了很多次,同样的种群规模,往往如果在前10代还未出现解,那后面基本不可能出现了,因此我在主程序中设定迭代次数为10。至于种群规模,在迭代次数相同的情况下,种群规模在100到150之间效果较好,大于150的话,基本上都是在第0代即初始种群就有解,这样使用遗传算法就失去了作用,毕竟刚开始就有解的话,就没必要往后算了。


5 评价

自己并不满意。性能差的原因可能就是上面说的,我觉得主要可能是我的变异操作不好的原因,其次是交叉操作不优,应该和选择操作关系不大,因为轮盘赌法还是比较合理的。


END

  • 18
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值