什么是遗传算法
遗传算法是模拟达尔文生物进化伦的自然选择和遗传学激励的生物进化古欧成的计算模型,它通过模拟自然进化——适者生存,不适者淘汰,的过程搜索对于问题的最优解。
下面描述遗传算法的基本框架:初始化一个随机种群作为第一代,种群中的个体通过基因实现唯一性,针对我们要解决的问题,不同基因的个体存在不同的适应性,适者生存,不适者被淘汰,通过大自然的选择,存活下来的所有个体进行遗传变异操作,产生第二代的个体。重复前面的选择,遗传,变异等过程,直到找到完美解决问题,即适应性达到完美的个体,此时该个体的基因就是解决问题的方案。
基因:将问题中对象可以进行的所有操作,通过编码的方式,形成基因序列;
初始化:设置第一代种群中所有个体的基因序列,此过程为初始化,为了后续进行选择,遗传,以及编译过程做准备;
适应性:即个体对于环境的适应性,或者称为方案解决问题的能力,通过设置适应性函数计算个体的适应性;
选择:大自然将适应性高的个体选择出来并使得存活,同时部分不适应的个体也幸运地存活下来,此过程称为选择;
遗传:两个个体将自身优秀的基因传承给后代的过程,即构成下一代基因序列过程的一部分;
变异:两个个体在遗传的过程中产生一定不可控变化的过程,即构成下一代基因序列过程的一部分。
什么是8数码难题
在一个3×3的棋盘中,摆有8个棋子,每个棋子上标有1-8的8个数字,且各不相同,棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。给出一个初始状态和一个终止状态,求初始状态到终止状态的转变过程。
解决8数码难题——Java
个体定义的代码:
public class Self{
public static boolean keyAns;
public int[] gene; //基因序列
public int location; //记录满足条件的最终步数所在的位置
public int fitness; //个体对环境的适应度
public int rf; //个体适应度占群体总适应度的比例
public int cf; //个体i的累积适应度
public Self(){
Random ran=new Random();
gene=new int[140]; //假定基因的长度为140
for(int i=0;i<gene.length;i++){
gene[i]=ran.nextInt(4)+1;
//对应空白格的上下左右移动
}
location=-1; //记住这个“-1”
keyAns=false;
rf=0;
cf=0;
}
}
空白格进行移动:
public static boolean move(int sta,int[] nowSta){
int space=0; //空格的位置
int block=0; //移动的格子的位置
//查找空格的位置
for(int i=0;i<nowSta.length;i++){
if(nowSta[i]==0){
space=i;
block=space;
break;
}
}
//九宫格的架构为
// 0 1 2
// 3 4 5
// 6 7 8
//确定传入的sta所代表的操作,所代表的需要移动的格子的位置
//例如,空格在“4”位置,传入sta为1,确认移动格子的位置为“1”
if(sta==1){
if(space>=3){
block=block-3;
}
}//空格向上移动
else if(sta==2){
if(space<=5){
block=block+3;
}
}//空格向下移动
else if(sta==3){
if(space%3>=1){
block=block-1;
}
}//空格向左移动
else if(sta==4){
if(space%3<=1){
block=block+1;
}
}//空格向右移动
int temp=nowSta[space];
nowSta[space]=nowSta[block];
nowSta[block]=temp;
//交换两个格子中的值
if(space==block)
return false;
else
return true; //sta代表的操作可以被执行,用于打印变化过程
}
计算单个基因实施的操作(即一次移动)对于适应度的改变:
public static int count(int[] nowSta,int[] goalSta){
int Fit=0;
for(int i=0;i<nowSta.length;i++){
if(nowSta[i]==goalSta[i]){
Fit=Fit+100-(nowSta[i]*10);
}
//适应度计算的具体方法,可根据个人需要进行修改
}
return Fit;
}
计算个体的适应度:
public void countFitness(int[] nowSta,int[] goalSta){
for(int i=0;i<gene.length;i++){
move(gene[i],nowSta);
fitness=count(nowSta,goalSta);
if(fitness==540){
//找到解决方案时退出循环,将静态变量kayAns改为true作为判断条件
location=i;
keyAns=true;
break;
}
}
if(fitness==0){
fitness=1;
}//即使适应度为0,也有被选择的可能性(具体见选择算法和轮盘赌算法)
}
基因变异:
public void varyWay(){
Random ran=new Random();
int num=ran.nextInt(140)+1; //有几个基因发生变异
for(int i=0;i<num;i++){
int loca=ran.nextInt(140); //随机定位
gene[loca]=5-gene[loca]; //变异对于基因变异的影响方式
//可以根据需要自行修改
}
}
public void vary(){
Random ran=new Random();
//有一定的概率fvary发生基因变异
//double fvary=0.30;
//double freal=ran.nextDouble();
//一开始用的是double比较,重新看了一遍觉得还是改一下,可不用double比较就不用
int fvary=3;
int freal=ran.nextInt(10+1);
if(freal<=fvary){
varyWay();
}
}
因为定义的是Self类,即个体类,剩下还有选择(运用了轮盘赌的思想,针对群体)以及基因交叉(需要两个个体)的操作写在了main方法中,如下:
public static void main(String[] args){
int[] firstState={7,2,4,5,0,6,8,3,1};
int[] goalState={0,1,2,3,4,5,6,7,8};
int[] nowState={7,2,4,5,0,6,8,3,1};
int[][] prea=new int[20][140];
Self[] a=new Self[20]; //假设种群的个体数目为20
for(int i=0;i<a.length;i++){
a[i]=new Self();
}
//假设最大的种群代数是9999
for(int ration=0;ration<9999;ration++){
int totalFitness=0;
//计算个体的适应度
for(int i=0;i<a.length;i++){
//对于一个个体的适应度计算完毕之后,需要将nowState重新初始化
for(int k=0;k<nowState.length;k++){
nowState[k]=firstState[k];
}
a[i].countFitness(nowState,goalState);
totalFitness=totalFitness+a[i].fitness;
}
if(a[0].keyAns){ //如果种群中出现完美个体,退出循环
break;
}
int lastCf=0; //第i个体的累积适应度
for(int i=0;i<a.length;i++){
a[i].rf=a[i].fitness*1000000/totalFitness;
a[i].cf=lastCf+a[i].rf;
lastCf=a[i].cf;
}
//至此完成种群的初始化,下面开始实现种群的选择
//采用轮盘赌算法的思想,将适应环境的个体选到二维数组prea中
/*因为使用double类型的变量做轮盘赌算法的运算时,会因为精度丢失的原因使得比较结果出现异常,导致被选择的基因为空,
prea数组自动填0补充。使用int类型放大1000000倍,会出现个体的累积选择概率总和不为1的情况,所以当出现了这种情况的
时候,默认将数组最后的一个个体选择为下一代的父体(母体)。*/
Random ran=new Random();
for(int i=0;i<a.length;i++){
int f=ran.nextInt(1000000);
if(f<a[i].rf){
for(int k=0;k<a[i].gene.length;k++){
prea[i][k]=a[i].gene[k];
}
}
else{
if(f>a[19].cf){
for(int k=0;k<a[i].gene.length;k++){
prea[i][k]=a[19].gene[k];
}
}
else{
for(int j=0;j<19;j++){
if(f>=a[j].cf && f<a[j+1].cf){
for(int k=0;k<a[i].gene.length;k++){
prea[i][k]=a[j+1].gene[k];
}
break;
}
}
}
}
}
for(int i=0;i<a.length;i++){
for(int k=0;k<a[i].gene.length;k++){
a[i].gene[k]=prea[i][k];
}
}
//至此完成选择函数,下面开始实现遗传
//遗传需要两个个体进行基因交叉,当first不为-1时发生
int first=-1;
int fChange=8; //基因交换的概率
for(int i=0;i<a.length;i++){
int f=ran.nextInt(10)+1;
if(f<=fChange){
if(first=-1){
first=i;
}
else{
int num=ran.nextInt(140)+1;
for(int j=0;j<num;j++){
int loca=ran.nextInt(140)+1;
int temp;
temp=a[i].gene[loca];
a[i].gene[loca]=a[first].gene[loca];
a[first].gene[loca]=temp;
}
}
}
}
//至此完成基因交叉,下面实现基因变异
//调用之前定义的基因突变方法
for(int i=0;i<a.length;i++){
a[i].vary();
}
}
//基因突变结束后形成了新的种群,回归for循环的开头执行自然选择的过程,直到出现完美个体,找到keyAns后通过break跳出循环
//跳出循环后,根据Self的属性location(定位结束动作,初始值为-1)不为-1定位完美个体在种群中的位置
int thenum=0;
for(int i=0;i<a.length;i++){
if(a[i].location!=-1){
thenum=i;
break;
}
}
for(int i=0;i<a[thenum].location+1;i++){
if(move(a[thenum].gene[i],firstState)){
//move(int sta,int[] nowSta)返回boolean类型变量
//打印变化过程
System.out.print(……);
System.out.print(……);
System.out.print(……);
System.out.println(……);
}
}
}
轮盘赌算法思想
轮盘赌在代码中的应用体现在选择算法中,具体应用过程大概如下:
-
对于种群中的第一个个体,产生一个概率p,如果该个体在种群中的适应度贡献rf(个体适应度/总适应度)大于概率p,则此个体被选择;
-
如果rf小于p,则利用累积概率cf选出个体,规则如下,按照数组的顺序计算每个个体的累积概率cf,如a[0]的累积概率为a[0].rf,a[1]的累积概率为a[0].rf+a[1].rf,……然后使得a[i].rf<=p<a[i+1].rf,选择个体a[i];
-
因为使用double类型的变量做轮盘赌算法的运算时,会因为精度丢失的原因使得比较结果出现异常,导致被选择的基因为空,prea数组自动填0补充。使用int类型放大1000000倍,会出现个体的累积选择概率总和不为1的情况,所以当出现了这种情况的时候,默认将数组最后的一个个体选择为下一代的父体(母体)。
对于轮盘赌算法如果想要了解更详细建议查找相关文章!!!
代码优化
-
一开始将种群代数设置为999,但是得到false(找不到解决方案)的结果,将种群最大代数设置为9999之后则得到了结果。同时,上调基因突变的概率以及基因交叉的概率可加快得到解决方案的速度(减少代数)。
-
一开始设想是在Self类外设置一个类Race用来存放Self,这样就可以在main方法外写选择以及基因交叉等操作的方法,优化代码结构。但是当时刚学Java,对于面向对象还学得不是很理解,尝试了之后发现代码反复报错就放弃了,因此写在了一个文件中,main方法中显得结构混乱。