遗传算法入门笔记

目录

一、大体实现过程

二、开始我们的进化(具体实现细节)

2.1 先从编码说起

2.1.1 二进制编码法

2.1.2 浮点编码法

2.1.3 符号编码法

2.2 为我们的袋鼠染色体编码

2.3 评价个体的适应度

2.4 射杀一些袋鼠

2.5 遗传--染色体交叉(crossover)

2.6 变异--基因突变(Mutation)

三、代码实现环节

3.1 基础概念

3.2 代码尝试

3.2.1 遗传算法算子

3.2.2 变异算子(mutation)

3.3 遗传算法策略

3.4 小刷例题

3.4.1 例题1:牛刀小试

3.4.2 例题2:糖果


一、大体实现过程

遗传算法中每一条染色体,对应着遗传算法的一个解决方案,一般我们用适应性函数(fitness function)来衡量这个解决方案的优劣。所以从一个基因组到其解的适应度形成一个映射。遗传算法的实现过程实际上就像自然界的进化过程那样。

我们用袋鼠跳中的步骤一一对应解释:

1) 首先寻找一种对问题潜在解进行“数字化”编码的方案。(建立表现型和基因型的映射关系)

2) 随机初始化一个种群(那么第一批袋鼠就被随意地分散在山脉上),种群里面的个体就是这些数字化的编码。

3) 接下来,通过适当的解码过程之后(得到袋鼠的位置坐标)。

4) 用适应性函数对每一个基因个体作一次适应度评估(袋鼠爬得越高当然就越好,所以适应度相应越高)。

5) 用选择函数按照某种规定择优选择(每隔一段时间,射杀一些所在海拔较低的袋鼠,以保证袋鼠总体数目持平。)

6) 让个体基因变异(让袋鼠随机地跳一跳)。

7) 然后产生子代(希望存活下来的袋鼠是多产的,并在那里生儿育女)。

遗传算法并不保证你能获得问题的最优解,但是使用遗传算法的最大优点在于你不必去了解和操心如何去“找”最优解。(你不必去指导袋鼠向那边跳,跳多远。)而只要简单的“否定”一些表现不好的个体就行了。(把那些总是爱走下坡路的袋鼠射杀,这就是遗传算法的精粹!)

由此我们可以得出遗传算法的一般步骤:

1) 随机产生种群。

2) 根据策略判断个体的适应度,是否符合优化准则,若符合,输出最佳个体及其最优解,结束。否则,进行下一步。

3) 依据适应度选择父母,适应度高的个体被选中的概率高,适应度低的个体被淘汰。

4) 用父母的染色体按照一定的方法进行交叉,生成子代。

5) 对子代染色体进行变异。

由交叉和变异产生新一代种群,返回步骤2,直到最优解产生。

二、开始我们的进化(具体实现细节)

2.1 先从编码说起

编码是应用遗传算法时要解决的首要问题,也是设计遗传算法时的一个关键步骤。编码方法影响到交叉算子、变异算子等遗传算子的运算方法,很大程度上决定了遗传进化的效率。

迄今为止人们已经提出了许多种不同的编码方法。总的来说,这些编码方法可以分为三大类:二进制编码法、浮点编码法、符号编码法。下面分别进行介绍。

2.1.1 二进制编码法

就像人类的基因有AGCT 4种碱基序列一样。不过在这里我们只用了0和1两种碱基,然后将它们串成一条链形成染色体。一个位能表示出2种状态的信息量,因此足够长的二进制染色体便能表示所有的特征。这便是二进制编码。如下:

1110001010111

它由二进制符号0和1所组成的二值符号集。它有以下一些优点

1) 编码、解码操作简单易行

2) 交叉、变异等遗传操作便于实现

3) 符合最小字符集编码原则

4) 利用模式定理对算法进行理论分析。

二进制编码的缺点是:对于一些连续函数的优化问题,由于其随机性使得其局部搜索能力较差,如对于一些高精度的问题,当解迫近于最优解后,由于其变异后表现型变化很大,不连续,所以会远离最优解,达不到稳定。

2.1.2 浮点编码法

二进制编码虽然简单直观,一目了然。但是存在着连续函数离散化时的映射误差。个体长度较短时,可能达不到精度要求,而个体编码长度较长时,虽然能提高精度,但增加了解码的难度,使遗传算法的搜索空间急剧扩大。

所谓浮点法,是指个体的每个基因值用某一范围内的一个浮点数来表示。在浮点数编码方法中,必须保证基因值在给定的区间限制范围内,遗传算法中所使用的交叉、变异等遗传算子也必须保证其运算结果所产生的新个体的基因值也在这个区间限制范围内。如下所示:

1.2-3.2-5.3-7.2-1.4-9.7

浮点数编码方法有下面几个优点:

1) 适用于在遗传算法中表示范围较大的数。

2) 适用于精度要求较高的遗传算法。

3) 便于较大空间的遗传搜索。

4) 改善了遗传算法的计算复杂性,提高了运算效率。

5) 便于遗传算法与经典优化方法的混合使用。

6) 便于设计针对问题的专门知识的知识型遗传算子。

7) 便于处理复杂的决策变量约束条件。

2.1.3 符号编码法

符号编码法是指个体染色体编码串中的基因值取自一个无数值含义、而只有代码含义的符号集如{A,B,C…}。

符号编码的主要优点是:

1) 符合有意义积术块编码原则。

2) 便于在遗传算法中利用所求解问题的专门知识。

3) 便于遗传算法与相关近似算法之间的混合使用。

2.2 为我们的袋鼠染色体编码

在上面介绍了一系列编码方式以后,那么,如何利用上面的编码来为我们的袋鼠染色体编码呢?首先我们要明确一点:编码无非就是建立从基因型到表现型的映射关系。这里的表现型可以理解为个体特征(比如身高、体重、毛色等等)。那么,在此问题下,我们关心的个体特征就是:袋鼠的位置坐标(因为我们要把海拔低的袋鼠给杀掉)。无论袋鼠长什么样,爱吃什么。我们关心的始终是袋鼠在哪里,并且只要知道了袋鼠的位置坐标(位置坐标就是相应的染色体编码,可以通过解码得出),我们就可以:

1) 在喜马拉雅山脉的地图上找到相应的位置坐标,算出海拔高度。(相当于通过自变量求得适应函数的值)然后判读该不该射杀该袋鼠

2) 可以知道染色体交叉和变异后袋鼠新的位置坐标。

回到一元函数最大值的问题。在上面我们把极大值比喻为山峰,那么,袋鼠的位置坐标可以比喻为区间[-1, 2]的某一个x坐标(有了x坐标,再通过函数表达式可以算出函数值 <==> 得到了袋鼠染色体编码,解码得到位置坐标,在喜马拉雅山脉地图查询位置坐标算出海拔高度)。这个x坐标是一个实数,现在,说白了就是怎么对这个x坐标进行编码。下面我们以二进制编码为例讲解,不过这种情况下以二进制编码比较复杂就是了。(如果以浮点数编码,其实就很简洁了,就一浮点数而已。)

我们说过,一定长度的二进制编码序列,只能表示一定精度的浮点数。在这里假如我们要求解精确到六位小数,由于区间长度为2 - (-1) = 3 ,为了保证精度要求,至少把区间[-1,2]分为3 × 10^6等份。又因为

2^21 = 2097152 < 3*10^6 < 2^22 = 4194304

所以编码的二进制串至少需要22位。

把一个二进制串 (b0,b1,....bn) 转化为区间里面对应的实数值可以通过下面两个步骤:

1) 将一个二进制串代表的二进制数转化为10进制数:

$(b_{0}...b_{20}b_{21})_{2}=(\sum_{i=0}^{21} b_{i}*2^i)_{10}=x^t$

2) 对应区间内的实数:

$x=-1+x^t\frac{(2-(-1))}{2^{22}-1}$

例如一个二进制串(1000101110110101000111)通过上面换算以后,表示实数值0.637197。

好了,上面的编码方式只是举个例子让大家更好理解而已,编码的方式千奇百怪,层出不穷,每个问题可能采用的编码方式都不一样。在这一点上我们要注意。

2.3 评价个体的适应度

前面说了,适应度函数主要是通过个体特征从而判断个体的适应度。在本例的袋鼠跳中,我们只关心袋鼠的海拔高度,以此来判断是否该射杀该袋鼠。这样一来,该函数就非常简单了。只要输入袋鼠的位置坐标,在通过相应查找运算,返回袋鼠当前位置的海拔高度就行。

适应度函数也称评价函数,是根据目标函数确定的用于区分群体中个体好坏的标准。适应度函数总是非负的,而目标函数可能有正有负,故需要在目标函数与适应度函数之间进行变换。

评价个体适应度的一般过程为

  1. 对个体编码串进行解码处理后,可得到个体的表现型。

  2. 由个体的表现型可计算出对应个体的目标函数值。

  3. 根据最优化问题的类型,由目标函数值按一定的转换规则求出个体的适应度。

2.4 射杀一些袋鼠

遗传算法中的选择操作就是用来确定如何从父代群体中按某种方法选取那些个体,以便遗传到下一代群体。选择操作用来确定重组或交叉个体,以及被选个体将产生多少个子代个体。前面说了,我们希望海拔高的袋鼠存活下来,并尽可能繁衍更多的后代。但我们都知道,在自然界中,适应度高的袋鼠越能繁衍后代,但这也是从概率上说的而已。毕竟有些适应度低的袋鼠也可能逃过我们的眼睛。

那么,怎么建立这种概率关系呢?

下面介绍几种常用的选择算子:

  1. 轮盘赌选择(Roulette Wheel Selection):是一种回放式随机采样方法。每个个体进入下一代的概率等于它的适应度值与整个种群中个体适应度值和的比例。选择误差较大。

  2. 随机竞争选择(Stochastic Tournament):每次按轮盘赌选择一对个体,然后让这两个个体进行竞争,适应度高的被选中,如此反复,直到选满为止。

  3. 最佳保留选择:首先按轮盘赌选择方法执行遗传算法的选择操作,然后将当前群体中适应度最高的个体结构完整地复制到下一代群体中。

  4. 无回放随机选择(也叫期望值选择Excepted Value Selection):根据每个个体在下一代群体中的生存期望来进行随机选择运算。方法如下:

    1. 计算群体中每个个体在下一代群体中的生存期望数目N。

    2. 若某一个体被选中参与交叉运算,则它在下一代中的生存期望数目减去 0.5,若某一个体未被选中参与交叉运算,则它在下一代中的生存期望数目减去1.0。

    3. 随着选择过程的进行,若某一个体的生存期望数目小于0时,则该个体就不再有机会被选中。

  5. 确定式选择:按照一种确定的方式来进行选择操作。具体操作过程如下:

    1. 计算群体中各个个体在下一代群体中的期望生存数目N。

    2. 用N的整数部分确定各个对应个体在下一代群体中的生存数目。

    3. 用N的小数部分对个体进行降序排列,顺序取前M个个体加入到下一代群体中。至此可完全确定出下一代群体中M个个体。

  6. 无回放余数随机选择:可确保适应度比平均适应度大的一些个体能够被遗传到下一代群体中,因而选择误差比较小。

  7. 均匀排序:对群体中的所有个体按其适应度大小进行排序,基于这个排序来分配各个个体被选中的概率。

  8. 最佳保存策略:当前群体中适应度最高的个体不参与交叉运算和变异运算,而是用它来代替掉本代群体中经过交叉、变异等操作后所产生的适应度最低的个体。

  9. 随机联赛选择:每次选取几个个体中适应度最高的一个个体遗传到下一代群体中。

  10. 排挤选择:新生成的子代将代替或排挤相似的旧父代个体,提高群体的多样性。

下面以轮盘赌选择为例给大家讲解一下:

假如有5条染色体,他们的适应度分别为5、8、3、7、2。

那么总的适应度为:F = 5 + 8 + 3 + 7 + 2 = 25。

那么各个个体的被选中的概率为:

α1 = ( 5 / 25 ) * 100% = 20%

α2 = ( 8 / 25 ) * 100% = 32%

α3 = ( 3 / 25 ) * 100% = 12%

α4 = ( 7 / 25 ) * 100% = 28%

α5 = ( 2 / 25 ) * 100% = 8%

所以转盘如下:

当指针在这个转盘上转动,停止下来时指向的个体就是天选之人啦。可以看出,适应性越高的个体被选中的概率就越大。

2.5 遗传--染色体交叉(crossover)

遗传算法的交叉操作,是指对两个相互配对的染色体按某种方式相互交换其部分基因,从而形成两个新的个体。

适用于二进制编码个体或浮点数编码个体的交叉算子:

  1. 单点交叉(One-point Crossover):指在个体编码串中只随机设置一个交叉点,然后再该点相互交换两个配对个体的部分染色体。

  2. 两点交叉与多点交叉:

    1. 两点交叉(Two-point Crossover):在个体编码串中随机设置了两个交叉点,然后再进行部分基因交换。

    2. 多点交叉(Multi-point Crossover)

  3. 均匀交叉(也称一致交叉,Uniform Crossover):两个配对个体的每个基因座上的基因都以相同的交叉概率进行交换,从而形成两个新个体。

  4. 算术交叉(Arithmetic Crossover):由两个个体的线性组合而产生出两个新的个体。该操作对象一般是由浮点数编码表示的个体。

咳咳,根据国际惯例。还是抓一个最简单的二进制单点交叉为例来给大家讲解讲解。

二进制编码的染色体交叉过程非常类似高中生物中所讲的同源染色体的联会过程――随机把其中几个位于同一位置的编码进行交换,产生新的个体。

对应的二进制交叉:

2.6 变异--基因突变(Mutation)

遗传算法中的变异运算,是指将个体染色体编码串中的某些基因座上的基因值用该基因座上的其它等位基因来替换,从而形成新的个体。

例如下面这串二进制编码:

101101001011001

经过基因突变后,可能变成以下这串新的编码:

001101011011001

以下变异算子适用于二进制编码和浮点数编码的个体:

  1. 基本位变异(Simple Mutation):对个体编码串中以变异概率、随机指定的某一位或某几位仅因座上的值做变异运算。

  2. 均匀变异(Uniform Mutation):分别用符合某一范围内均匀分布的随机数,以某一较小的概率来替换个体编码串中各个基因座上的原有基因值。(特别适用于在算法的初级运行阶段)

  3. 边界变异(Boundary Mutation):随机的取基因座上的两个对应边界基因值之一去替代原有基因值。特别适用于最优点位于或接近于可行解的边界时的一类问题。

  4. 非均匀变异:对原有的基因值做一随机扰动,以扰动后的结果作为变异后的新基因值。对每个基因座都以相同的概率进行变异运算之后,相当于整个解向量在解空间中作了一次轻微的变动。

  5. 高斯近似变异:进行变异操作时用符号均值为P的平均值,方差为P**2的正态分布的一个随机数来替换原有的基因值。

三、代码实现环节

3.1 基础概念

拿古代人类来举例子:

  • 个体(Individual):每个生物。即每个古人类个体。
  • 种群(population):一个系统里所有个体的总称。比如一个部落。
  • 种群个体数(POPULATION):一个系统里个体的数量。比如一个部落里的人数。
  • 染色体(chromosom):每个个体均携带,用来承载基因。比如一条人类染色体。
  • 基因(Gene):用来控制生物的性状(表现)。
  • 适应度(fitness):对某个生物是否适应环境的定量评分。比如对某个古人类是否强壮进行 [1,100] 的评分。
  • 迭代次数(TIMES):该生物种群繁衍的次数。比如古人类繁殖了 100 万年。

一定要记住这些英文名字!,后面会经常用到

在算法中,我们对每个个体计算其染色体的适应度(fitness)来决定它是否优秀。

3.2 代码尝试

尝试构建一个名字叫做 Individual 的结构体,里面存储一个个体的信息。

struct Individual{
	string chromosome;  //染色体
	int fitness;  //个体的适应度
	int calc_fitness();  //计算适应度 
	
	Individual(string chromosome);  //初始化 
	Individual mate();  //交叉算子(CrossOver) 
	Individual mutation();  //变异算子(Mutation) 
};

Individual(string chromosome){
	this->chromosome=chromosome;
	this->fitness=calc_fitness();
}

3.2.1 遗传算法算子

交叉算子(CrossOver)

也有将该算子称为 mate 的。我更倾向于第二种叫法,因为第二种字数更少。

交叉算子就是模拟父母双方交配过程。想一想人类交配时,每个基因会随机的来自父亲或者母亲。我们可以模拟这个过程。假设我们的染色体用 string 存储,可以实现下面的交配代码:

// par 代表母亲,chromosome 代表父亲(即本身)的染色体,par.chromosome 则代表母亲染色体。
Individual Individual::mate(Individual par) { // 交叉
	string child = ""; // 子代染色体
	int len = chromosome.size();
	for (int i = 0; i < len; i ++ ) {
		double p = random(0, 100) / 100; // 计算来自父母的概率
		if (p <= 0.5) child += chromosome[i]; // 一半概率来自父亲
		else child += par.chromosome[i]; // 另一半来自母亲
	}
	return Individual(child);
}

当然,我们也可以思考一些其他的交叉思路,比如随机抽取某些段进行交换。如下图所示:

这种算法通常在二进制条件下更加实用。

3.2.2 变异算子(mutation)

即低概率地随机地改变某个基因。这样可以有效避免程序陷入局部最优或者过慢收敛。例如:

一般来说,我们可以设计一个变异概率。变异率大概在 0.01∼0.050.01∼0.05 之间最优。变异率太高会导致收敛过慢,变异率太低则会导致陷入局部最优。

3.3 遗传算法策略

  • 精英保留策略

还是拿古人类举例。假设我们是上帝,我们想要古人类实现长久发展,最好的办法就是尽可能的将那些头脑敏捷,肢体强壮的个体保留下来,淘汰那些老弱病残的个体。

在程序中,我们将个体按照适应度排序,把适应度最好前 k% 的保留下来,剩下的随机交配。通常,k 可以设成 1∼20。设置太高则会局部最优,太低则会收敛过慢。

  • 概率保留策略

学名好像是 Stoffa改进方法,这不重要。总之,就是为了避免父母生出傻孩子浪费时间,把傻孩子(适应度低的后代)直接抛弃。

假设我们要求收敛到最低适应度,后代适应度为 y,父代适应度为 x,有 Δ=y−x。若 Δ<0,证明子代比父代更好,我们一定接受。对于 Δ>0,证明子代不如父代好,我们以一定概率接受。之所以要有一定概率接受,是为了避免出现局部最优解。

这个概率我们怎么来算呢?有一种方法给出了概率的计算函数:

$P(\bigtriangleup)=e^{-\frac{\bigtriangleup}{t}}$

其中 t 是我们设定的参数值,一般随着迭代次数增大而减小。

如果 $P(\bigtriangleup)$  > rand(0,1),那么我们就接受它。

为什么是 −Δ 呢?因为 Δ>0,我们要保证 e^x 小于 1 才能保证部分接受。因此要用 −Δ。

为什么要用 e^x 呢?我不知道,大概是因为它的积分等于原函数吧。。。

下面放一张遗传算法求最短哈密尔顿路径的收敛图像。其中绿色实线是加了概率函数的,蓝色虚线则没有加。可以看出,绿色实现收敛的比较快,侧面证明了 Stoffa改进方法 的正确性。

思考题

如果我们要求最大适应值,那么概率的计算函数应当怎么计算呢?

答案:P(Delta)=e^{Delta/t},Delta<0。

3.4 小刷例题

3.4.1 例题1:牛刀小试

给定一个目标字符串(target),目标是从相同长度的随机字符串开始生成目标字符串。

比如从 114514QAQ 生成 123456789。

这里先讲一下适应度函数的构造方法:比较当前染色体与目标字符串之间不同的字符个数即为适应度。显然,适应度越低越好。

样例代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<ctime>
#include<vector>
#define POPULATION 1000
using namespace std;

typedef long long LL;
//using LL=long long;
typedef pair<int,int> PII;
//using PII=pair<int,int>;
typedef pair<LL,LL> PLL;
//using PLL=pair<LL,LL>;

const string target="I've got it!";
const string GeneBase="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP"\
					"QRSTUVWXYZ 1234567890, .-;:_!\"#%&/()=?@${[]}'"; // 基因库

int random(int l,int r){     //随机一个位于[l,r]之间的整数 
	return rand()%(r-l+1)+l;
}

char mutate(){
	int len=GeneBase.size();
	return GeneBase[random(0,len-1)]; 
} 

string create(){ 	//产生新的染色体 
	string chromosome="";
	int len=target.size();
	for(int i=0;i<len;i++){
		chromosome+=mutate();
	}
	return chromosome;
}

struct Individual{
	string chromosome;  		//染色体
	int fitness;  				//个体的适应度
	
	int calc_fitness();  		//计算适应度 
	
	Individual(string chromosome);  	 //初始化 
	Individual mate(Individual parent);  //交配 
	void mutation();  //随机变异 
	
	bool operator < (const Individual &tmp) const {
		return fitness<tmp.fitness;
	} 
};

Individual::Individual(string chromosome){
	this->chromosome=chromosome;
	this->fitness=calc_fitness();
}

// par 代表母亲,chromosome 代表父亲(即本身)的染色体,par.chromosome 则代表母亲染色体。
Individual Individual::mate(Individual par) { // 交叉
	string child = ""; // 子代染色体/后代 
	int len = chromosome.size();
	for (int i = 0; i < len; i ++ ) {
		double p = random(0, 100) / 100; // 计算来自父母的概率
		if (p <= 0.5) 
			child += chromosome[i]; // 一半概率来自父亲
		else 
			child += par.chromosome[i]; // 另一半来自母亲
	}
	return Individual(child);
}

int Individual::calc_fitness(){		//计算适应度 
	int len=target.size(); 
	int now_fitness=0;
	for(int i=0;i<len;i++){
		now_fitness+=(chromosome[i]!=target[i]);
	}
	return now_fitness;
}

void Individual::mutation(){
	int P=random(1,2),LEN=target.size();
	for(int i=1;i<=P;i++){
		int pos=random(0,LEN-1);
		chromosome[pos]=mutate();
	}
	this->fitness=calc_fitness();
} 

int main(){
	srand(time(0));
    int cnt = 0; // 计算当前迭代到第几代
    vector<Individual> population;
    bool found = false;

    for (int i = 0; i < POPULATION; i ++ ) { // 随机个体
        population.push_back(Individual(create()));
    }
    
    while (!found) {
        sort(population.begin(), population.end()); // 按照适应度排序
        if (population[0].fitness == 0) {
            found = true; break;
        }
        vector<Individual> new_population;
        int s = (10 * POPULATION) / 100; // 保留精英种子
        for (int i = 0; i < s; i ++ )  
            new_population.push_back(population[i]);
        s = POPULATION - s; // 剩下的随机交配
        for (int i = 0; i < s; i ++ ) {
            int len = population.size();
            Individual p1 = population[random(0, 50)];
            Individual p2 = population[random(0, 50)];
            Individual ch = p1.mate(p2);
            int P = random(0, 100);
            if (P <= 20) 
				ch.mutation();
            new_population.push_back(ch);
        }
        population = new_population;
        printf("Generation: %d  ", cnt);
        cout << "String : " << population[0].chromosome << "  ";
        cout << "Fitness : " << population[0].fitness << "\n"; cnt ++ ;
    }
    printf("Generation : %d  ", cnt);
    cout << "String : " << population[0].chromosome << "  ";
    cout << "Fitness : " << population[0].fitness << "\n"; cnt ++ ;
    
	return 0;
} 

运行结果:

Generation: 0  String : !n m fhrd$tI  Fitness : 10
Generation: 1  String : E u" BoiDiyp  Fitness : 9
Generation: 2  String : E u" BoiDiyp  Fitness : 9
Generation: 3  String : sI]&  Pt'"t!  Fitness : 8
Generation: 4  String : I u" BoiDit5  Fitness : 7
Generation: 5  String : I u" BoiDit!  Fitness : 6
Generation: 6  String : I u" BoiDit!  Fitness : 6
Generation: 7  String : I u4 Boi it!  Fitness : 5
Generation: 8  String : I ue BoiDit!  Fitness : 5
Generation: 9  String : I u4 Bot it!  Fitness : 4
Generation: 10  String : I u4 Bot it!  Fitness : 4
Generation: 11  String : I'u4 Bot it!  Fitness : 3
Generation: 12  String : I'u4 Bot it!  Fitness : 3
Generation: 13  String : I'ue Bot it!  Fitness : 2
Generation: 14  String : I'ue eot it!  Fitness : 2
Generation: 15  String : I'ue Bot it!  Fitness : 2
Generation: 16  String : I'3e Bot it!  Fitness : 2
Generation: 17  String : I'ue eot it!  Fitness : 2
Generation: 18  String : I'ue Bot it!  Fitness : 2
Generation: 19  String : I've Bot it!  Fitness : 1
Generation: 20  String : I've Bot it!  Fitness : 1
Generation: 21  String : I've Bot it!  Fitness : 1
Generation : 22  String : I've got it!  Fitness : 0

3.4.2 例题2:糖果

题目描述

糖果店的老板一共有 M 种口味的糖果出售。为了方便描述,我们将 M 种口味编号 1 ∼ M。

小明希望能品尝到所有口味的糖果。遗憾的是老板并不单独出售糖果,而是 K 颗一包整包出售。

幸好糖果包装上注明了其中 K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。

给定 N 包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。

输入格式

第一行包含三个整数 N、M 和 K。

接下来 N 行每行 K 这整数 T1,T2,⋯,TK,代表一包糖果的口味。

输出格式

一个整数表示答案。如果小明无法品尝所有口味,输出 −1。

样例 #1

样例输入 #1

6 5 3
1 1 2
1 2 3
1 1 3
2 3 5
5 4 2
5 1 2

样例输出 #1

2

提示

对于 30% 的评测用例,1≤N≤20。

对于所有评测样例,1≤N≤100,1≤M≤20,1≤K≤20,1≤Ti≤M。

蓝桥杯 2019 年省赛 A 组 I 题。

这一题是不是很熟悉咧,曾经用状态压缩写过,请看链接:

状态压缩DP-CSDN博客

使用遗传算法样例代码:

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <ctime>

#define POPULATION 1000
#define TIMES 10

using namespace std;

const int N = 110, M = 21;
int a[N][M], n, m, K, ans = 0x3f3f3f3f;

int random(int l, int r) {
    return rand() % (r - l + 1) + l;
}
struct Individual {
    vector<int> p;
    int fitness;
    Individual(vector<int> p);
    Individual mate();
    int calc_fitness();

    bool operator < (const Individual& tmp)const {
        return fitness < tmp.fitness;
    }
};

Individual::Individual(vector<int> P) {
    this -> p = P;
    this -> fitness = calc_fitness();
}

Individual Individual::mate() {
    vector<int> _p = this -> p;
    int P = random(0, 3);
    for (int i = 1; i <= P; i ++ ) {
        int pos1 = random(0, n - 1), pos2 = random(0, n - 1);
        swap(_p[pos1], _p[pos2]);
    }
    return _p;
}

int Individual::calc_fitness() {
    int state = 0;
    for (int i = 0; i < n; i ++ ) {
        for (int j = 0; j < K; j ++ )
            state |= (1 << a[p[i]][j] - 1);
        if (state == (1 << m) - 1) return i + 1;
    }
    puts("-1"); 
	exit(0);
}

int main() {
    scanf("%d%d%d", &n, &m, &K);

    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < K; j ++ )
            scanf("%d", &a[i][j]);

    vector<Individual> population; 
	vector<int> P;
    
	for (int i = 0; i < n; i ++ ) 
		P.push_back(i);

    for (int i = 1; i <= POPULATION; i ++ ) {
        random_shuffle(P.begin(), P.end());
        population.push_back(Individual(P));
    }

    for (int i = 0; i < TIMES; i ++ ) {
        sort(population.begin(), population.end());
        ans = min(ans, population[0].fitness);
        vector<Individual> new_population;

        int s = (10 * POPULATION) / 100;
        for (int i = 0; i < s; i ++ )
            new_population.push_back(population[i]);
        s = POPULATION - s;
        for (int i = 0; i < s; i ++ ) {
            Individual p = population[random(0, 50)];
            new_population.push_back(p.mate());
        }
        population = new_population;
    }

    printf("%d\n", ans);
    return 0;
}

当然遗传算法还可以用到寻路问题,8数码问题,囚犯困境,动作控制,找圆心问题(在一个不规则的多边形中,寻找一个包含在该多边形内的最大圆圈的圆心),TSP问题,生产调度问题,人工生命模拟等问题,这里不再展开。

当然,遗传算法的实现还有其他方式,鉴于这是入门篇,不再深入实现其他方法,日后可以再做研究。

以上,遗传算法入门笔记

祝好

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕飞雨的头发不能秃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值