【人工智能】遗传算法解决N皇后问题之Java实现

本文深入探讨遗传算法的基本原理及其在解决N皇后问题中的应用。通过编码、种群生成、适应度计算、交叉与变异等步骤,展示遗传算法如何高效寻优。实验结果表明,种群规模对算法性能有显著影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.原理     

 遗传算法(Genetic Algorithm)类似于爬山法(Hill-climbing Search),是一种局部束搜索算法(Local Beam Search)。GA模拟自然界的生成法则,实行优胜劣汰,偶然变异。爬山法可以看成是无性繁殖,而遗传算法可以看成是有性繁殖(两个体繁殖2新的2个体)。

遗传算法包括以下几个步骤:

(1)种群(Population):随机生成种群。

(2)计算每个个体(Individual,有时也称为染色体Chromosome)的适应度(Fitness)。

(3)根据适应度,计算每个个体占整个种群的适应度百分比(即:个体适应度*100/种群所有个体适应度之和),我的理解是这个百分比可以根据种群大小适当放大。

(4)繁殖或交叉(Crossover):根据上面的百分比,使用一种随机选择算法如轮盘赌法选择一对个体进行等量繁殖(Reproduction),这里面很重要的一条是要进行优胜劣汰,对于适应度最好的个体可以直接进入下一代种群,而对最差的个体直接使用最好的个体进行替代,以保证种群大小不变(个人理解)。

(5)变异(mulation):繁殖后的个体以极小的概率进行变异

总之,遗传算法应经历以下阶段:编码(对问题进行分析,使用二进制、浮点数、十进制等方法进行编码,每一个码就是一个基因)、生成种群、计算适应度、繁殖、变异,直到找到目标状态,最后进行解码。

2.N皇后问题之遗传算法

可以将N皇后问题编码为二进制或十进制,这里以十进制为例,如8皇后的一种状态可表示为一个数组:24748552,其适应度可以用不冲突的皇后对数来度量,该个体的适应度为24,显然对于8皇后问题,如果其适应度为$(8-1)*8/2=28$就表示找到一个解。在交叉的时候可以随机选择一个位置来交叉前后部分,假设有两个个体24748552、32752411,若以第4位来交叉,则新生成的个体为:32758552、24742411,当然也可以选择某个范围或某几个范围的交叉(一般应设置变异率,值为0.7-0.99)。在变异时,随机生成一个概率,若小于预设的变异概率(一般为0.001-0.01),则进行变异,如个体24742411的第7位变异为3,则突变为24742431。

3.N皇后问题的表示

数据结构包括:种群数组(d[DSIZE][SIZE])、列线数组(col[DSIZE][SIZE])、主对角线数组(pdiag[DSIZE][SIZE])、副对角线数组(cdiag[DSIZE][SIZE])、适应度数组(f[DSIZE])、适应度百分比数组(fp[DSIZE]),其中DSIZE表示种群大小,而SIZE表示皇后个数。

import java.util.Random;
import java.util.Date;
import java.util.Queue;
//import java.util.LinkedList;
//import java.io.FileWriter;
//import java.io.BufferedWriter;
//import java.io.IOException;

public class nQueen {
    final static int SIZE=1000;//皇后数
	final static int DSIZE=100;//种群大小
	final static int MP=512;//变异率
         //种群
	static int[][] d=new int[DSIZE][SIZE];
	//col[i]表示第i列有几个皇后 
	static int[][] col=new int[DSIZE][SIZE];
	//pdiag[i]表示第i根主对角线(principal diagonal)皇后个数(从左上到右下的线) 
	static int[][] pdiag=new int[DSIZE][SIZE*2];
	//cdiag[i]表示第i根副对角线(counter diagonal)皇后个数(从右上到左下的线) 
	static int[][] cdiag=new int[DSIZE][SIZE*2];
	//适应度
	static int[] f=new int[DSIZE];;
	//适应度百分比
	static int[] fp=new int[DSIZE];

4.初始化种群

为了提高种群的质量,每行每列生成唯一的皇后,使用了isHas函数判断是否在某列上已生成皇后(当然也可以采取通用的对角线赋值加随机交换办法)。生成种群后应计算三条线(主副对角线、列线,此时列线当然都为0)上的皇后个数。

public static boolean isHas(int[] t,int k)
{
	for(int i=0;i<t.length;i++)
	   if(t[i]==k) return true;
	return false;
}
//初始化种群
public static void initPopulation()
{   //随机生成种群
    Random r=new Random();
	for(int i=0;i<DSIZE;i++)
	{
		for(int j=0;j<SIZE/2;j++)
		{
			int x;
			do
			{ x=r.nextInt(SIZE); }
			while(isHas(d[i],x));
			d[i][j]=x;
			d[i][SIZE-j-1]=SIZE-x-1;
		}
	}
	//初始化三条线
	for(int i=0;i<DSIZE;i++)
	{
		for(int j=0;j<SIZE;j++)
		{
			col[i][d[i][j]]++;
			pdiag[i][j-d[i][j]+SIZE-1]++;
			cdiag[i][j+d[i][j]]++;
		}
	}
	//计算适应度f
	int s=(SIZE-1)*SIZE/2;
	for(int i=0;i<DSIZE;i++)
	{
		f[i]=0;
		for(int j=0;j<2*SIZE;j++)
		{
			if(j<SIZE) f[i]+=((col[i][j]-1)*col[i][j]/2);
			f[i]+=((pdiag[i][j]-1)*pdiag[i][j]/2);
			f[i]+=((cdiag[i][j]-1)*cdiag[i][j]/2);
		}
		f[i]=s-f[i];
	}
    //找到最大适应度、最小适应度
	int min=f[0],max=f[0];
	for(int i=0;i<DSIZE;i++)
	{
		if(f[i]<min) min=f[i];
		if(f[i]>max) max=f[i];
 	}
	//采用一种办法计算适应度百分比:fi=s-max+fi-min
	//可以稍微放大每个范围,不至于拥堵在一处
	int sum=0;
	//System.out.println("max="+max+",min=,"+min);
	//System.out.print("min="+min+"fp1:[");
	for(int i=0;i<DSIZE;i++)
	{
        sum+=(s-max+f[i]-min);
        fp[i]=sum;
		//System.out.print(fp[i]+",");
	}
	//System.out.println("]");
	//System.out.print("sum="+sum+",fp2:[ ");
	for(int i=0;i<DSIZE;i++)
	{
	        //放大百分比数
		fp[i]=(int)(10000.0*fp[i]/sum);
		//System.out.print(fp[i]+",");
	}
	//System.out.println("]");
	System.out.println("初始化种群完成,种群大小:"+DSIZE);
}

5.交叉或变异时的调整个体

交叉或变异时,需要调整个体,这时不宜重新去生成三条线,适当调整即可。

//调整第d1个体(个体,即一种N皇后摆法)
//将其第row行皇后调整到第newcol列
public static void adjust(int d1,int row,int newCol)
{	//原第row行皇后所在列位置
	int oldCol=d[d1][row];
	if(oldCol==newCol) return;
	//列上增加值,下面三条语句可以进行相应推导而得
	f[d1]+=(col[d1][oldCol]-col[d1][newCol]-1);
	//主对角线上增加值 
    f[d1]+=(pdiag[d1][row-oldCol+SIZE-1]-pdiag[d1][row-newCol+SIZE-1]-1);
	//副对角线上增加值  
	f[d1]+=(cdiag[d1][row+oldCol]-cdiag[d1][row+newCol]-1); 
	//原列线,主副对角线都减1
	col[d1][oldCol]--;
	pdiag[d1][row-oldCol+SIZE-1]--;
	cdiag[d1][row+oldCol]--;
	//现列线,主副对角线都加1
	col[d1][newCol]++;
	pdiag[d1][row-newCol+SIZE-1]++;
	cdiag[d1][row+newCol]++;
}

6.轮盘赌算法

如下图所示,假设根据适应度随机选择生成了一个数为63,这时所处的范围为49.5-75.2(这个百分比是累加的),根据预设的规则应选择4。对于皇后个数很多的情况,如超过10000,建议使用二分法查找以加快查找速度。

图1 轮盘赌选择法
//轮盘赌选择一个个体(个体,即一种N皇后摆法)
public static int roulette()
{
    Date dd=new Date();
	Random r1=new Random();
	//增强随机数生成,以免一个时间点生成的都是一个数
	Random r=new Random(dd.getTime()+r1.nextLong());
	//随机生成一个概率,放大的...
	int p=r.nextInt(10000);
	if(p==0 || p<fp[0]) return 0;
	for(int i=1;i<DSIZE;i++)
	{
		if(p<fp[i] && p>=fp[i-1]) return i;
	}
	return 0;
}

7.繁殖或杂交

//杂交,第d1个与第d2个杂交
public static void crossover(int d1,int d2)
{
    Date dd=new Date();
    Random r1=new Random(dd.getTime());
    Random r=new Random(r1.nextLong()+dd.getTime());
    //随机生成一个杂交起始点与终止点
    //这对皇后个数很多比较好
    int cp1=r.nextInt(SIZE);
    int cp2=cp1+r.nextInt(SIZE-cp1);
    for(int k=cp1;k<cp2;k++)
    {
    	int temp=d[d1][k];
    	//adjust负责调整列线col、主副对角线(pdiag、cdiag)皇后冲突个数
    	//还负责调整适应值表f
    	adjust(d1,k,d[d2][k]);
    	d[d1][k]=d[d2][k];
    	adjust(d2,k,temp);
    	d[d2][k]=temp;
    }
}

8.变异

//变异,第d1个可能的变异
public static void mulation(int d1)
{
    Random r=new Random();
    //随机生成一个数与变异率MP(变异率)比较
    int p=r.nextInt(10000);
    if(p<=MP)
    {
    	//随机生成1个变异点
    	int mp=r.nextInt(SIZE);
    	//随机生成1个变异值
    	int mv=r.nextInt(SIZE);
        //调整三条线
    	adjust(d1,mp,mv);
    	d[d1][mp]=mv;
    }
}

9.重新计算适应度百分比

//为轮盘赌选择法计算适应度百分比
public static void perFitness()
{
    int min=f[0],max=f[0];
    int sum=0;
    int s=SIZE*(SIZE-1)/2;
    //找到最大、最小
    for(int i=0;i<DSIZE;i++)
    {
    	if(f[i]<min) min=f[i];
    	if(f[i]>max) max=f[i];
    }
    //放大范围
    for(int i=0;i<DSIZE;i++)
    {
    	sum+=(s-max+f[i]-min);
    	fp[i]=sum;
    }
    //累加百分比,放大
    for(int i=0;i<DSIZE;i++)
    	fp[i]=(int)(10000.0*fp[i]/sum);
}

10.优胜劣汰

//最好的个体直接进入下一代,最差的淘汰
public static void bestBadOperation(int c)
{
    int best=0,bad=0;
    //int maxFitness=SIZE*(SIZE-1)/2;
    int sum=0;
    for(int i=0;i<DSIZE;i++)
    {
    	if(f[i]>f[best]) best=i;
    	if(f[i]<f[bad]) bad=i;
    	sum+=f[i];
    } 
    //批量替换差的为好的
    //问题:最后全都一样了
    for(int i=0;i<DSIZE;i++)
    {
    	if(f[i]==f[bad]) 
    	{       //arraycopy效率一般还可以
    		System.arraycopy(d[best],0, d[i], 0, SIZE);
    		System.arraycopy(col[best],0, col[i], 0, SIZE);
        	System.arraycopy(pdiag[best],0, pdiag[i], 0, 2*SIZE);
        	System.arraycopy(cdiag[best],0, cdiag[i], 0, 2*SIZE);
            f[i]=f[best];
    	}
    }
    //重新计算适应度百分比
    perFitness();
}

11.遗传算法的实现

//N皇后遗传算法
//不考虑迭代次数限制!
public static int geneticAlgorithms()
{
    int c=0;//迭代次数
    //N皇后最大适应度
    int maxFitness=SIZE*(SIZE-1)/2;
    while(true)
    {
    	c++;
    	int k=0;//个体计数器
    	while(k<DSIZE)
    	{
    		int d1,d2,kk1=0,kk2=0;
    		//轮盘赌选择2个杂交个体
    		while(true)
    		{
    			d1=roulette();//轮盘赌选择个体
    			d2=roulette();
    			//System.out.println("d1="+d1+",d2="+d2+",kk1="+kk1+",kk2="+kk2);
    			//if(d1>=DSIZE || d2>=DSIZE)
    			{
    				kk1++;
    			}
    			if(d1==d2)  kk2++;
		        //轮盘赌二分法查找有问题时加的下面一行
    			if(d1!=d2 && d1<DSIZE && d2<DSIZE) break;
    		}
    		k+=2;		
    		//杂交
    		crossover(d1,d2);
    		//变异
    		mulation(d1);
    		//找到N皇后解
    		if(f[d1]==maxFitness) 
    		{
    			System.out.println("\n迭代次数:"+c);
    			return c;
    		}
    		mulation(d2);
    		if(f[d2]==maxFitness) 
    		{
    			System.out.println("\n迭代次数:"+c);
    			return c;
    		}
    	}
    	//优胜劣汰,重新计算适应度百分比
    	bestBadOperation(c);
    }
}

12.测试、结果

	public static void main(String[] args) {
		int c=0;
		int avg=0;
		int k=0;
        //测试10次取平均
		while(c<10)
		{
			c++;
			d=new int[DSIZE][SIZE];
			col=new int[DSIZE][SIZE];
			pdiag=new int[DSIZE][2*SIZE];
			cdiag=new int[DSIZE][2*SIZE];
			f=new int[DSIZE];
			fp=new int[DSIZE];
			initPopulation();
			long t1=new Date().getTime();
		    int x=geneticAlgorithms();
		    long t2=new Date().getTime();
		    avg+=(t2-t1);
		    k+=x;
		    System.out.println(c+". 耗时:"+(t2-t1)+",迭代次数:"+x);
		}
		System.out.println("平均耗时:"+avg/10+",平均迭代次数:"+k/10);
          //下面代码主要是将数据写到文件里,以便可以使用excel作图
		//long t1=new Date().getTime();
	    //int k=geneticAlgorithms();
	    //long t2=new Date().getTime();
	    //System.out.println(k+":");
	    
	    /*Iterator<Integer> it=itQueue.iterator();
	    Iterator<Integer> itBest=bestQueue.iterator();
	    Iterator<Integer> itBad=badQueue.iterator();
	    Iterator<Integer> itAvg=avgQueue.iterator();*/
	    /*try {
	       FileWriter fw=new FileWriter("100.txt",false); 
	       BufferedWriter bw=new BufferedWriter(fw);
	    
	       while(!itQueue.isEmpty())
	       {
	    	   bw.newLine();
	    	   bw.write(itQueue.poll()+","+bestQueue.poll()+","+badQueue.peek()+","+avgQueue.poll());
	       }
	       bw.close();
	       fw.close();
	    }
	    catch(IOException e)
	    {
	    	System.out.println(e.getMessage());
	    }*/
	   // for(int j=0;j<DSIZE;j++)
	   // {
			 // for(int i=0;i<SIZE;i++)
				//    System.out.print(d[k][i]+",");
			  //System.out.println();
	    //}

		//System.out.println("\n消耗时间:"+(t2-t1));
	}

}

遗传算法是随机算法,每次执行的性能都不一样,以10次计算其平均得到以下数据。可以看出,随着种群规模的增加,所花费的时间代价变大,但同时迭代次数降低。

表1 不同皇后数不同种群数遗传算法代价
皇后个数种群大小迭代次数平均耗时变异率
10082817591185512
100321351291992512
100641039853155512
100100570966015512
100200831718937512
皇后个数种群大小迭代次数平均耗时变异率
200810898785597512
2003268230812140512
2006436428311967512
20010032867619302512
20020025306729309512
皇后个数种群大小迭代次数平均耗时变异率
5008432836836747512
50032233711663851512
50064159355869508512
5001001574437112037512
5002002016290312787512
皇后个数种群大小迭代次数平均耗时变异率
10001004248935447166512
2000100175721712762050512

下图是100皇后迭代10000次的结果,其中横轴表示迭代次数,纵轴表示适应度值。可以看到,刚开始时,遗传算法迅速变好,最后缓慢变化并趋于稳定(或收敛)。最差个体几乎没什么变化,而最佳个体与平均适应度变化趋势趋同。

图2 100皇后、种群100个,迭代10000次

13.结论及问题

传算法应该比爬山法慢,尤其是在种群数量比较大的情况下,但是遗传算法可以使用模式(Schemata)来加速目标个体的形成。模式就是一种匹配方式,对于一些明显的不符合要求的可以过滤掉,如对N皇后问题可以使用构造法生成一些匹配模式。由于遗传算法加入了变异功能,一般来说,对于一个问题总能找到其解。但是在本程序里,由于优胜劣汰算法可能不够好,最后除了那些变异的外,其余全部都是一样的。之所以都能找到解,是因为在收敛成一样后进行了变异,这相当于在进行随机全局搜索,因此迭代的次数很多。像100皇后的100种群,基本在4000次迭代后就只是变异在起作用。另外一个问题是,遗传算法必须要有优胜劣汰机制,否则无法快速收敛到一个较好的水平。正如前面所述,优胜劣汰机制也导致最终全部个体都一样。这显然不符合自然法则,个体的多样性没有得到保证。期待有时间更深入去探索遗传算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值