一篇文章让你在根本上理解遗传算法,用牛刀杀鸡-使用遗传撕力扣_遗传算法题目实例
前言
遗传算法是大数据科学和AI的核心算法之一,是用来分析这个复杂世界不可缺少的一个算法,它听着很高大上,但是原理很简单,我希望用一篇文章让大家在概念上深入理解遗传算法的本质。
一 遗传算法的理解
需要的知识
因为遗传算法本身是模仿自然界生物的遗传特性,所以想理解遗传算法你可能并不需要太多的计算机知识,反而需要一些生物遗传学知识,但是一般高中生物能及格的同学应该都是知道这部分知识的,
遗传算法的定义(可以扫一遍,不需要硬理解)
首先让我们看一下遗传算法的定义:遗传算法(英语:Genetic Algorithm,GA)是计算数学中用于解决最优化的搜索算法,是进化算法的一种。进化算法最初是借鉴了进化生物学中的一些现象而发展起来的,这些现象包括遗传、突变、自然选择以及杂交等等。
遗传算法通常实现方式为一种计算机模拟。对于一个最优化问题,一定数量的候选解(称为个体)可抽象表示为染色体,使种群向更好的解进化。传统上,解用二进制表示(即0和1的串),但也可以用其他表示方法。进化从完全随机个体的种群开始,之后一代一代发生。在每一代中评价整个种群的适应度,从当前种群中随机地选择多个个体(基于它们的适应度),通过自然选择和突变产生新的生命种群,该种群在算法的下一次迭代中成为当前种群。
我这里尝试把官方解释翻译成人话-大白话描述并不严谨,但容易理解
作为成年人,大家都应该知道适者生存的道理,自然选择不断淘汰落后的基因,保留好的基因,基因也会不断突变,如果突变的方向是好的,它就能活下来,如果突变是坏的,他就更可能被淘汰,每种生物或者人大多都不是完美的,但是能延续下来的群体,身上至少有一些优势基因在起作用,同时优势基因也会通过遗传累加起来获得更大的优势从而淘汰那些并不那么优秀的基因,或者某些垃圾基因联合起来的时候就突然有了1+1>2的效果,也有些垃圾基因通过突变直接翻身了。就是这样一代一代的繁殖,不断淘汰落后的个体,整个群体就会越来越niubility,
如果把生物学对应到算法题中,算法题本身就是环境,适应环境的基因序列才是好基因序列,算法题的解就是王者基因序列,王者基因序列可能每个基因都是最好的(贪婪算法为解的情况),也可能他每个基因都不是很优秀,但是这些基因合起来就出现了意想不到的效果(贪婪算法不是最优解的情况)
二 遗传算法实践
这里可以通过使用遗传算法解一道算法题让大家更加具体地理解遗传算法
题目(来自力扣)
假如有一排房子,共
n
个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个
n x 3
的正整数矩阵costs
来表示的。例如,
costs[0][0]
表示第 0 号房子粉刷成红色的成本花费;costs[1][2]
表示第 1 号房子粉刷成绿色的花费,以此类推。请计算出粉刷完所有房子最少的花费成本。
示例 1:
输入: costs = [[17,2,17],[16,16,5],[14,3,19]]输出: 10解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。 最少花费: 2 + 5 + 3 = 10。示例 2:
输入: costs = [[7,6,2]]输出: 2
常规解法
大家能想到最多地应该就是枚举法和动态规划了,枚举法就是把所有地可能都列举出来,时间复杂度是3的n次方到2的n次方,非常慢,动态规划就很快,我这里象征性贴一下动态规划代码,对于没有学算法的人可能不太容易理解动态规划,其他也可以跳过这一部分,因为介绍动态规划要花费大量篇幅和读者的时间,而且动态规划不是文章重点,大家可以粗略阅读甚至跳过动态规划代码
动态规划源码
class Solution {
//re[x][y] 表示如果只有前面(x + 1)个房子,且最后的房子(x+1)涂为y色时的最小花费, max(re[n - 1][0], re[n - 1][1], re[n - 1][2])就是我们要的解
int[][] re;
public int minCost(int[][] costs) {
int n = costs.length;
re = new int[n][3];
//赋初值
re[0][0] = costs[0][0];
re[0][1] = costs[0][1];
re[0][2] = costs[0][2];
for(int i = 1; i < n; i ++){
re[i][0] = costs[i][0] + Math.min(re[i - 1][1], re[i - 1][2]);
re[i][1] = costs[i][1] + Math.min(re[i - 1][0], re[i - 1][2]);
re[i][2] = costs[i][2] + Math.min(re[i - 1][0], re[i - 1][1]);
}
int maybeMax = re[n - 1][0] < re[n - 1][1] ? re[n - 1][0] : re[n - 1][1];
return maybeMax < re[n - 1][2] ? maybeMax : re[n - 1][2];
}
}
使用遗传算法解体
重点来了,为了便于大家理解,我不会一次性贴全部代码,而是一边讲解一边贴代码,最后再贴完整代码,我们按步骤来写代码,现在开始想像我们创造了一个世界,这个世界的每个人都是为了解决那道题而生
第一步 定义人(Person)
在这里,每个人都是为了解决这道题而生,每个人都代表一个解(这个解不一定是最优解),很自然我们需要表示这道题的一种解法,我们这里很容易想到用一个数字数组来表示一个解,比如题目的例子中,costs = [[17,2,17],[16,16,5],[14,3,19]], 那么数组[0,2,1]表示第一个房涂第一种颜色,第二个房子涂第三种颜色,第三个房子涂第二种颜色,其花费就是17 + 5 + 3 = 25,[1,2,1]表示第一个房涂第二种颜色,第二个房子涂第三种颜色,第三个房子涂第二种颜色,其花费就是2 + 5 + 3 = 10(比前面那个人优秀),而[0, 0, 1]第一间房子和第二间房子因为涂了一样的颜色,所以因为违法规则被淘汰了,无法存活,
表示人的代码
//自然环境变量,存放题目给的costs static int[][] costs; public static class Person{ //代表解法 int[] solute; //存放这个解法的花费,跳过getCost()方法计算 int cost; //计算这个解法的花费 public int getCost(){ int cost = 0; //累加每个房子的涂鸦成本 for(int i = 0; i < solute.length; i++){ cost += costs[i][solute[i]]; } return cost; } }
当然我们还需要一个方法判断这个人是否违法规则,因为如果违法规则就会被淘汰(夭折),这里是如果相邻的房子不能涂一样的颜色
public boolean isDead(){ for(int i = 1; i < solute.length; i++){ if(solute[i] == solute[i-1]){ return true; } } return false;}
然后我们就需要用代码模拟的交配行为了,即两个人生一个新的人,根据遗传学定理,每个人的每个染色体都会随机来自父亲或者母亲,方便起见这里可以让solute数组的每一个项都作为一条单独的染色体(尽管人的染色体是成对的),在生一个孩子时,这个孩子的每个基因都会随机来自于父或者母,并且有百分之5的概率变异(变异大部分是坏的,过高的变异率可能让一些运气不好的优秀基因全部阵亡,过低又会使得整个群体难以获得突破性进化),为了方便这里就不纠结性别了。
随机方法
private static final Random random = new Random();public static boolean maybe(double probability){ return random.nextDouble() < probability;}
交配函数,
每个染色体(房子的涂鸦颜色)都会在父母中随机选择,之后百分之5的可能变异,然后判断这个个体是否能存活
public static Person makeLove(Person p1, Person p2){ Person person = new Person(); person.solute = new int[p1.solute.length]; for(int i = 0; i < p1.solute.length; i++){ person.solute[i] = maybe(0.5) ? p1.solute[i] : p2.solute[i]; //百分之五的可能基因突变 if(maybe(0.05)){ person.solute[i] = random.nextInt() % 3; } } //夭折 if(person.isDead()){ return null; } person.cost = person.getCost(); return person;}
K值
自然界有个K值的概念,就是环境的最大容纳量,比如某个草原最多没养1000只羊,超过1000的话就会因为资源争夺等原因物种的内部竞争加剧,最菜的个体会被淘汰,使得最终羊的数量维持在1000左右,所以我们也需要设定一个K值,然后用一个数组来存储自然界中活着的人,
//k值,代表环境能容纳的最大人数static int k; //用来存储活着的人,大小最大为kstatic List people;
第二步 让世界开始运转,开始自然选择
首先设置环境的条件(题目中为粉刷房子的费用数组)和环境k值
public static int solute(int[][] costs){ Ycsf.costs = costs; k = 10000; people = new ArrayList(k); ...}
然后我们需要生成初始群体,这里暂时生成2000个初始群体
先写一个生成随机人的方法
public static Person createRandomPerson(int n){ Person re = new Person(); re.solute = new int[n]; //记录前一个值 int pre = -1; for(int i = 0; i < n; i++){ int ran = new Random().nextInt() % 3; //避免因为相邻的房子涂同一种颜色而生成一个死人 while(ran == pre){ ran = new Random().nextInt() % 3; } pre = ran; re.solute[i] = ran; } re.cost = re.getCost(); return re;}
在自然环境中生成2000个人
int n = costs.length;for(int i = 0; i < 2000; i++){ people.add(createRandomPerson(n));}
决定繁殖代数
虽然自然界的生物可以一直迭代到世界毁灭的那天,但是我们毕竟要解决算法题,不可能让他们无限迭代,我们需要在迭代到某个时候的时候从那些人中获取到最优解。我们这里让他们迭代20000次,就退出程序获取解
int time = 20000;for(int i = 0; i < time; i++){ ...迭代逻辑}
编写每次繁殖和淘汰的逻辑
其实主要就是一直重复做两个事情,繁殖和淘汰,我们这里假设生物不会因为年龄死亡,只会被淘汰
繁殖
我们每次让每个人随机和其他两个人交配,如果孩子夭折了,就允许他再找一个补偿一下他,减少一个人因为运气不好绝代的概率
for(int j = 0; j < personNum; j++){ //要繁殖的人 Person person = people.get(j); //选人和交配逻辑 ...}
首选是选人的逻辑,我们这里随机选两个人
//随机获取一个人和他交配,但是不能是他自己int ranId = random.nextInt() % personNum;while(ranId == j){ ranId = random.nextInt() % personNum;}//再随机获取一个人和他交配,但是不能是他自己或者前面的人,尽可能多地增加可能性int ranId2 = random.nextInt() % personNum;while(ranId2 == j || ranId2 == ranId){ ranId2 = random.nextInt() % personNum;}
然后就是交配逻辑了,如果出现过交配失败的情况,额外补充一次
//这个变量用来判断是否失败过boolean fail = false;//开始交配并且把生成的人塞入自然界Person person1 = makeLove(person, people.get(ranId));if(person1 != null){ people.add(person1);}else{ fail = true;}//开始交配并且把生成的人塞入自然界Person person2 = makeLove(person, people.get(ranId2));if(person2 != null){ people.add(person2);}else{ fail = true;}if(fail){ int ranId3 = random.nextInt() % personNum; while(ranId3 == j || ranId3 == ranId || ranId3 == ranId2){ ranId3 = random.nextInt() % personNum; } Person person3 = makeLove(person, people.get(ranId3)); if(person3 != null){ people.add(person3); }}
每一轮迭代后,进行自然选择和淘汰,把人(解决方案)按照花费排序, 然后淘汰k值之后的个体
people = people.stream().sorted((p1, p2) -> p1.cost - p2.cost) .limit(k) .collect(Collectors.toList());
在完成我们设定的所有迭代后,取排在最前面的那个人作为解
return people.get(0).cost;
自此,我们就用遗传算法解了这道题,代码看似复杂,其实时间复杂度只有o(n2logn),(排序算法我们使用最快的nlogn),总体代码如下
import java.util.ArrayList;import java.util.List;import java.util.Random;import java.util.stream.Collectors;public class Ycsf { //自然环境变量,存放题目给的costs static int[][] costs; //k值,代表环境能容纳的最大人数 static int k; //用来存储活着的人,大小最大为k static List people; private static final Random random = new Random(); public static void main(String[] args) { costs = new int[][]{{17,2,17}, {16,16,5}, {14, 3, 19}}; int solute = solute(costs); System.out.println(solute); } public static int solute(int[][] costs){ Ycsf.costs = costs; k = 10000; people = new ArrayList(k); int n = costs.length; for(int i = 0; i < 2000; i++){ people.add(createRandomPerson(n)); } int time = 20000; for(int i = 0; i < time; i++){ int personNum = people.size(); for(int j = 0; j p1.cost - p2.cost) .limit(k) .collect(Collectors.toList()); } return people.get(0).cost; } public static Person createRandomPerson(int n){ Person re = new Person(); re.solute = new int[n]; //记录前一个值 int pre = -1; for(int i = 0; i < n; i++){ int ran = ranInt() % 3; //避免因为相邻的房子涂同一种颜色而生成一个死人 while(ran == pre){ ran = ranInt() % 3; } pre = ran; re.solute[i] = ran; } re.cost = re.getCost(); return re; } public static class Person{ //代表解法 int[] solute; //存放这个解法的花费,跳过getCost()方法计算 int cost; //计算这个解法的花费 public int getCost(){ int cost = 0; //累加每个房子的涂鸦成本 for(int i = 0; i < solute.length; i++){ cost += costs[i][solute[i]]; } return cost; } public boolean isDead(){ for(int i = 1; i < solute.length; i++){ if(solute[i] == solute[i-1]){ return true; } } return false; } } public static Person makeLove(Person p1, Person p2){ Person person = new Person(); person.solute = new int[p1.solute.length]; for(int i = 0; i < p1.solute.length; i++){ person.solute[i] = maybe(0.5) ? p1.solute[i] : p2.solute[i]; //百分之五的可能基因突变 if(maybe(0.05)){ person.solute[i] = ranInt() % 3; } } //夭折 if(person.isDead()){ return null; } person.cost = person.getCost(); return person; } public static boolean maybe(double probability){ return random.nextDouble() < probability; } public static int ranInt(){ int re =random.nextInt(); if(re < 0){ re = -re; } return re; }}
打印结果
虽然运行了比较久,但是确实给出了正确的答案。
遗传算法使用了大量随机理论,不管是选择交配过程,遗传过程等等很多都用到了随机数,但是因为遗传,变异和淘汰的原因,总体会随着更好的方向迭代,大家应该很容易发现
1 遗传算法得到的解不一定是最优解(这个例子总能得到最优解是因为我们设置的k值和迭代次数相对比较大,而题目给的很简单,就三个房子)
2 同样的代码运行多次可能得到的结果不一样。
3 迭代次数,变异几率,环境k值,交配选择规则,遗传规则都是可以根据我们自己的需要调整。
这里用遗传算法解题也是为了让大家更加形象地理解遗传算法。
既然遗传算法这么复杂,又这么慢,得到的结果又不一定准确,为什么还会用得如此广泛呢,因为真实的世界远比这些算法题复杂,不仅问题的量级很高,也很少有确定的解法,很多只能一一遍,历,比如人的46条染色体上DNA总共大约有60亿个碱基对.每个碱基对有四个可能,如果你想要遍历这些可能性,需要执行4的60亿次方次计算,还有天文学上的计算,这是现代计算机想都不敢想的量级。所以便有了这些算法。
遗传算法给我带来的启发
如何取得成功,其实大部分人也是用的遗传算法的方式,,我们在不同的道路上拼搏,有人学医,有人做律师,有人做老师,有人创业..., 其实我们也不一定能明确地知道做什么能成功,但是我们也不可能把所有事情都试一遍,所以我们会找到一些成功的人,这里学一下他们的性格,这里学一下他们的做事方式,那里参考一下他们的选择的道路,试图把他们拼凑起来成就自己。如果你觉得可以在他们的基础上做一些改变从而获得更好的效果,这就类似于遗传学的变异。
环境不像算法题,是会不断变化的,可能今天适合脖子长的人,明天就适合脖子短的人,每个人生存在这个世界上都有自己的方式,我们可能有一些不好的基因,但是我们能在这个世界有一席之地肯定就说明了我们有某些更加优秀的基因抵消这些不好的基因,很可能你的某个基因就是现在或者未来世界的答案,所以我们要保持自信。