65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中使用遗传算法解决八皇后问题

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (28投票s)

2011 年 6 月 25 日

CPOL

5分钟阅读

viewsIcon

101784

downloadIcon

10059

使用遗传算法解决八皇后问题的 C# 代码

引言

解决 NP-hard 问题有多种方法。遗传算法是一种解决此类问题的简单方法。本文将介绍如何使用 C# 中的遗传算法解决八皇后问题。

mainView.JPG

八皇后问题是在一个 8×8 的棋盘上放置八个国际象棋皇后,使得任意两个皇后都不能互相攻击。因此,解决方案要求任意两个皇后都不能在同一行、同一列或同一对角线上。

要使用遗传算法,必须定义交叉算子、变异算子、染色体和基因。

问题表示

基因:基因是一个从 0 到 7 的数字,表示皇后所在的行号。基因在染色体中的位置表示皇后的列号。由于棋盘上的皇后数量与列/行数量相同,因此必须保证每列每行都有一个皇后。

染色体:染色体是一组 8 个基因。假设染色体中没有重复的基因。

示例

0|1|4|2|3|6|7|5 八个皇后位于以下单元格。

(0,0), (1,1), (2,4), (3,2), (4,3), (5,6), (6,7), (7,5)

变异:变异是通过将要变异的基因与同一染色体中随机选择的一个基因(当前要变异的基因除外)进行交换来完成的。

交叉:以 0.5 的概率从两个父代染色体中抽取基因。从一个父代抽取一个基因,并将其附加到子代染色体。在另一个父代中删除相应的基因。重复此步骤,直到两个父代染色体都为空,并且子代包含所有涉及的基因。

适应度函数:碰撞是指两个皇后能够互相攻击。这意味着它们在同一对角线、同一行或同一列。由于染色体没有重复基因,并且保证了 2 个皇后不会在同一列,因此只需计算对角线碰撞。因此,可能发生的最多碰撞次数为 28。适应度函数是一个“越高越好”的函数,因此可以通过从 28 中减去当前状态下发生的碰撞次数来计算。解决方案将有 0 次碰撞,适应度为 28。

Using the Code

类和结构

  • class GeneticAlgo:负责遗传算法所有操作的类。
  • class FitnessComparator:一个比较器类,用于按适应度值对染色体进行排序,以便在表中显示最终种群。
  • struct Chromosome:表示染色体的结构,包括基因、适应度和其平均适应度累积。
  • class MainFrame:负责处理用户界面并创建初始种群以传递给遗传算法的类。
  • class Board:负责棋盘的图形视图和操作的类。

变量

  • private const int MAX_FIT = 28; 存储最大适应度。

函数

private List<chromosome> GetInitialPopulation(int population)
{
	List<chromosome> initPop = new List<chromosome>();
	GeneticAlgo RandomGen = new GeneticAlgo();
	for (int i = 0; i < population; i++)
	{
		List<int> genes = new List<int>(new int[] {0, 1, 2, 3, 4, 5, 6, 7});
		Chromosome chromosome = new Chromosome();
		chromosome.genes = new int[8];
		for (int j = 0; j < 8; j++)
		{
			int geneIndex = (int)(RandomGen.GetRandomVal
				(0,genes.Count-1)+0.5);//randomly select a gene
			chromosome.genes[j] = genes[geneIndex];
			genes.RemoveAt(geneIndex);//remove selected gene
		}

		initPop.Add(chromosome);
	}
	return initPop;
} 

此函数以种群大小作为参数,并返回一个包含随机生成的基因的染色体列表。基因的值是从包含 0 到 7 的列表中随机选择的。在从列表中选择值时,会删除选定的值,以避免染色体中出现重复基因。列表的大小等于种群的大小。使用此函数创建初始种群后,将返回的染色体列表发送到 DoMating 函数。

public void DoMating(ref List<Chromosome> initPopulation, 
	int generations, double probCrossver, double probMutation)
{
	int totalFitness = 0;
	CalcFitness(ref initPopulation, ref totalFitness);            

	for (int generation = 0; generation < generations; generation++)
	{                
		PrepareRuletteWheel(ref initPopulation,totalFitness);
		Crossover(ref initPopulation, probCrossver);
		Mutate(ref initPopulation, probMutation);
		CalcFitness(ref initPopulation, ref totalFitness);                
		if (initPopulation[initPopulation.Count - 1].fitness == 28)
			break;
		if (progress != null)
		{
			progress(generation + 1);
		}
	}
	initPopulation.Sort(new FitnessComparator());
}  

此函数以染色体列表作为初始种群,以我们愿意在算法中进行的代数为参数,以及交叉概率和变异概率。此函数的负责是调用 CalcFitnessPrepareRuletteWheelCrossover Mutate 函数来处理传播到所需代数。

public void CalcFitness(ref List<Chromosome> chromosome, ref int totalFitness)
{
	int collisions = 0;
	totalFitness = 0;
	for (int k = 0; k < chromosome.Count; k++)
	{
		for (int i = 0; i < chromosome[k].genes.Length - 1; i++)
		{
			int x = i;
			int y = chromosome[k].genes[i];
			for (int j = i + 1; j < chromosome[k].genes.Length; j++)
			{
				if (Math.Abs(j - x) == Math.Abs
					(chromosome[k].genes[j] - y))
					collisions++;                        
			}
		}
		
		Chromosome temp = chromosome[k];
		temp.fitness = MAX_FIT - collisions;
		chromosome[k] = temp;
		totalFitness += chromosome[k].fitness;
		collisions = 0;
	}
}

此函数计算每个染色体的适应度,并将适应度值分配给每个染色体的 fitness 属性。通过计算碰撞次数并从最大碰撞次数中减去它来计算适应度,因为在此代码中,适应度是一个“越高越好”的函数。在为每个染色体计算适应度的同时,此函数还计算种群的总适应度,因为我们在下一步需要它来计算每个染色体的适应度比例。

private void PrepareRuletteWheel(ref List<Chromosome> parents,int total)
{
	int currentTotalFitness=0;
	for (int i = 0; i < parents.Count; i++)
	{
		currentTotalFitness += parents[i].fitness;
		Chromosome temp = parents[i];
		temp.cumAvgFitness = currentTotalFitness / (double)total;
		parents[i] = temp;
	}
}

使用基于染色体适应度的轮盘赌来选择要配对的父代以生成新一代。此函数负责准备轮盘赌,它以染色体列表作为当前种群,以种群总适应度作为参数。该函数计算每个染色体适应度与总适应度的比例,然后计算其累积值,并将其分配给染色体的 cumAvgFitness 属性。

public void Crossover(ref List<Chromosome> parents, double probability)
{
	List<Chromosome> offspring = new List<Chromosome>();

	for (int i = 0; i < parents.Count; i++)
	{                
		if (Assay(probability)) //if the chance is to crossover
		{
			Chromosome parentX = AssayRuletteWheel(parents);
			Chromosome parentY = AssayRuletteWheel(parents);

			List<int> child = new List<int>();
			for (int j = 0; j < 8; j++)
			{
				if (Assay(0.5)) //select from parentX
				{
					for (int k = 0; k < parentX.genes.Length; k++) 
					{
						if (!child.Contains
						(parentX.genes[k]))//instead of 
							//deleting the similar genes
							//from parents select the 
							//next non-contained number
						{
							child.Add(parentX.genes[k]);
							break;
						}
					}
				}
				else //select from parentY
				{
					for (int k = 0; k < parentY.genes.Length; k++)
					{
						if (!child.Contains
						(parentY.genes[k]))//instead of 
						//deleting the similar genes from 
						//parents select the next 
						//non-contained number
						{
							child.Add(parentY.genes[k]);
							break;
						}
					}
				}
			}
			Chromosome offSpr = new Chromosome();
			offSpr.genes = child.ToArray();
			offspring.Add(offSpr);

		}
		else //else the chance is to clone
		{
			Chromosome parentX = AssayRuletteWheel(parents);
			offspring.Add(parentX);
		}
	}

	while (offspring.Count > parents.Count)
	{
		offspring.RemoveAt((int)GetRandomVal(0, offspring.Count - 1));
	}

	parents = offspring;
}

此函数负责交叉和克隆操作。该函数以染色体列表作为当前种群,以交叉概率作为参数。Assay(int probability) 函数以给定的概率返回 true,因此它与交叉概率一起用于确定操作是交叉还是克隆。

if (Assay(probability)) //if the chance is to crossover
{
	Chromosome parentX = AssayRuletteWheel(parents);
	Chromosome parentY = AssayRuletteWheel(parents);

	List<int> child = new List<int>();
	for (int j = 0; j < 8; j++)
	{
		if (Assay(0.5)) //select from parentX
		{
			for (int k = 0; k < parentX.genes.Length; k++) 
			{
				if (!child.Contains(parentX.genes[k]))//instead of 
					//deleting the similar genes from parents 
					//select the next non-contained number
				{
					child.Add(parentX.genes[k]);
					break;
				}
			}
		}
		else //select from parentY
		{
			for (int k = 0; k < parentY.genes.Length; k++)
			{
				if (!child.Contains(parentY.genes[k]))//instead of 
					//deleting the similar genes from parents 
					//select the next non-contained number
				{
					child.Add(parentY.genes[k]);
					break;
				}
			}
		}
	}
	Chromosome offSpr = new Chromosome();
	offSpr.genes = child.ToArray();
	offspring.Add(offSpr);
}

这部分代码来自上面的函数,负责交叉两个父代 parentX parentY。为了创建子代,以 0.5 的概率从两个父代中选择基因,同时避免染色体中的基因重复。

在克隆操作中,一个父代直接被带到下一代。

public void Mutate(ref List<Chromosome> parents, double probability)
{
	List<Chromosome> offsprings = new List<Chromosome>();
	for (int i = 0; i < parents.Count; i++)
	{
		Chromosome offspring = parents[i];
		for (int mutatePosition = 0; mutatePosition < 8; mutatePosition++)
		{
			if (Assay(probability)) //if the chance is to mutate
			{
				int newGeneIndex = (int)(GetRandomVal(0,6)+0.5);
				if (newGeneIndex>=mutatePosition)
				{
					newGeneIndex += 1;
				}
				int swapTemp = offspring.genes[mutatePosition];
				offspring.genes[mutatePosition] = 
						offspring.genes[newGeneIndex];
				offspring.genes[newGeneIndex] = swapTemp;                       
			}
		}                                             		
		offsprings.Add(offspring);                
	}
	parents = offsprings;
}

此函数以给定的概率应用变异算子。它在遍历当前种群的染色体基因时评估变异的机会。如果必须对基因应用变异,则其值与同一染色体中随机选择的一个基因(要变异的基因除外)进行交换。

当找到解决方案时,包含解决方案的染色体中的基因数组可以设置为 Board 类中名为 Genes 的属性。

如何使用

该程序允许您指定种群大小、代数、交叉概率和变异概率。可以使用开始按钮执行算法。如果种群大小和代数过大,请使用进度条指示当前代数。但这会显著减慢算法速度。

最后一代表的染色体都显示在表中,图形化的棋盘显示最佳结果。

源代码可供下载。

© . All rights reserved.