从贪心到低配版遗传算法

”明月如霜,好风如水,清景无限 “

今天做了一个简单的leetcode题。看介绍是用贪心做。题目是:

605. 种花问题
假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。
可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组  flowerbed 表示花坛,由若干 01 组成,
其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不
打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。

示例 1:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true
示例 2:
输入:flowerbed = [1,0,0,0,1], n = 2
输出:false
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/can-place-flowers
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析一下题目,用贪心的话,还是贪心策略的确定。很明显不需要排序,也没法排。按题目要求转化为比较能种植数和n大小。之后是需要确定已有花朵的位置。问题转化为:如何根据已经种花的位置确定还能种几朵花。看起来是个简单的数学问题。代码如下:

int n_size = flowerbed.size();
    vector<int> position;
    for (int i = 0; i < n_size; ++i) {
      if (flowerbed.at(i) == 1) {
        position.push_back(i);
      }
    }
int num = 0;
    int position_size = position.size();
    for (auto i = 0; i < position_size; ++i) {
      //cout << position[i] << " ";
      if (i == 0) {
        num += (position[i]) / 2;
      }
      else {
        num += (position[i]- position[i-1]-1) / 2;
      }
    }
    
    num += (n_size - 1 - position[position_size-1]) / 2;
  

    return num;

思路上清晰,分三断。第一朵花往左是一段,中间一段,最后一朵花往右为一段。

效果很不好,还是思考问题的全面性有待提高。因为没考虑到没有花的时候就只有一段,一朵花时是两段(即if的层次问题,还有就是具体公式计算的细节,eg,三个间隔是只能种1朵花)

 if(position_size==0){
    bool Flag=(n_size+1)/2>=n ? true:false;
    return Flag;
  }

比较逗的是在调试时,文远陷入了对原数组情况的分类,因为未通过的栗子是:flowerbed=[1],n=0。这个栗子,我怀疑烂了,,,

 if (n_size==1){
      if(n<=1&&flowerbed[0]==0||n==0&&flowerbed[0]==1) return true;
      else return false;
  }

确实折腾了很久,关键时间和空间复杂度好菜啊。

看到这个渣渣后95%的代码。文远决定用de一下GA,也算是回顾一下。先是提供一个GA的模板。

莫烦大佬GA:

源码:https://gitee.com/AttackerGry/Evolutionary-Algorithm/blob/master/tutorial-contents/Genetic%20Algorithm/Find%20Path.py
b站:https://www.bilibili.com/video/av16926245?zw

好了,之后讲讲GA(Genetic Algorithm)的几个关键点。

整体上看,GA就是在模仿生物学的优胜劣汰多目标优化上,效果很好,由于加入概率,陷入局部最优的可能性不大。用种群理解的话。种群中的每一个个体都是一个解,或者说是一种方案,答案。计算机模拟自然界优胜劣汰,种群一代一代更替后,种群中的个体都收敛到适应度最高的状态。也就是收敛后的种群中的个体对应的结果,方案是最优的。而且所有个体的结果都比较好。
实践关键注意点:

  • 初始种群,尽量随机。这样陷入局部最优的解只是一部分,不影响种群的整体进化。

  • 我们需要解决的问题,或者说我们定义的优胜略汰的规则,就是GA最关键的地方。习惯上称之为:fitness(适应度)。也就是每一个个体适应环境的能力。

  • 常见操作:将一些硬约束的惩罚力度加到最大(也就是让其适应度无穷小)软约束:(某些情况只是局部不优,可能后面又全局最优了)惩罚适应度的力度会小一些。一般都是得分制设计。!!!!要体现出你想要的优化!!!!

这一点和数学相关性很大,但是你可以仔细地学习了解激活函数(如何建立映射),还是很有启发的。一些取指数的技巧啊,有时像ReLu,TanH,这个还是自己好好把握。

下面是常见的三种操作了。

  • 复制(copy)

复制就涉及到选择了,选择出本代较优的个体。可以用进化策略,选前10%适应度的个体进入下一代,再复制回100%的个体数。也可以用fitness做抽样概率,放回抽样的情况下,适应度高的个体可能被连续抽到下一代。感觉效果差不多。

  • 交叉(crossover)

交叉的操作从DNA层面比较容易理解。最简单直接的情况是一半父代基因一半母亲的基因。这个在某些情况下,极其不适用,也好理解,两个好方案组合一下,很可能就四不像了。(这里必须要提一下编码解码,一般是要把方案编码为数字的,计算机好识别。参考one-hot编码解码),有双亲(两个个体)注意用copy()函数复制一下种群。

  • 变异(mutation)

变异是当之无愧的关键一步,从生物演化来说也是如此。一开始很垃圾的种群,经过变异产生一些种群里完全没有的基因。也就是变异让全局最优搜索的能力又加强了。可以因为变异从局部最优的解空间中跳出来。(设计变异多种多样,可以从变异范围,变异数量等多重考虑,但记住是每一个DNA都有可能变异,需要考虑具体的情况)

最后就是经典何时收敛。收敛条件一般设定为本代最高适应度与上代,上上代一样。有些学习幅度大的,不是很稳定时。可以:

abs(fitness[i]-fitness[i-1])/fitness[i] < 1e-2

种花-605实践代码:

import numpy as np
import re
# import matplotlib.pyplot as plt
flowerbed=[]
# flowerbed = [1,0,0,0,1]
print("输入初始土地栽花情况flowerbed数组:")
# flowerbed = [1,0,0,0,1]
flowerbed = [0,0,1,0,1,0,0,1,0,0,0]
# flowerbed = [0,0,0,1,0,0,1,0,0,0,1]
n=int(input('输入栽入花的数量'))
TARGET_PLAN = flowerbed      # target DNA

POP_SIZE = 300                      # population size
CROSS_RATE = 0.5                    # mating probability (DNA crossover)
MUTATION_RATE = 0.1                # mutation probability
N_GENERATIONS = 500

DNA_SIZE = len(TARGET_PLAN)

origin_pop=flowerbed*POP_SIZE
origin_pop=np.array(origin_pop)
origin_pop=origin_pop.reshape(POP_SIZE,-1)

class GA(object):
    def __init__(self, DNA_size,  mutation_rate, pop_size):
        self.DNA_size = DNA_size
        # self.cross_rate = cross_rate
        self.mutate_rate = mutation_rate
        self.pop_size = pop_size
        self.pop = origin_pop

    def get_fitness(self):                      # 最重点的计算适应度
        ###首先是体现出相邻的1是不行的,即硬约束
        ### 惩罚为   *-1000000
        fitness_list=[]
        ex='1 1'
        for individual in self.pop:
            # print(individual)    
            error_result=re.findall(ex,str(individual),re.S)
            loss_num=len(error_result)
            # print(loss_num)
            ###还是选择不对就适应度归0,比较好,便于概率处理
            # loss_fit=-1000000*loss_num
            ###接着是体现出种的越多越好,即硬约束
            ### self.pop 为300个体
            fit_count=sum(individual)
            if loss_num>0:
                fit_count=0
            # fit_count=sum(individual)+loss_fit
            fitness_list.append(fit_count)
        return np.array(fitness_list)

    def select(self):
        # fitness = self.get_fitness() + 1e-4     # add a small amount to avoid all zero fitness
        
        ###如何挑选出得分前10%的个体,传到下一代
        ##更新fitness,保证概率为正
        # fitness=[i for i in fitness if i<0: i=0]
        # print(fitness)
        idx = np.random.choice(np.arange(self.pop_size), size=self.pop_size, replace=True, p=fitness/fitness.sum())###即从编号range(pop_size)的个体中有放回的抽样,抽出pop_size个
        return self.pop[idx]

    # def crossover(self, parent, pop):###求简单,不需要
    #     if np.random.rand() < self.cross_rate:
    #         i_ = np.random.randint(0, self.pop_size, size=1)                        # select another individual from pop
    #         cross_points = np.random.randint(0, 2, self.DNA_size).astype(np.bool)   # choose crossover points
    #         parent[cross_points] = pop[i_, cross_points]                            # mating and produce one child
    #     return parent

    def mutate(self, child):###要保证的是只能由0变1,也就是再种树,不能1变0。不改变原来结果
        for point in range(self.DNA_size):##每一个点都可能变为1,即原来为1则没突变
            if np.random.rand() < self.mutate_rate:
                child[point] = 1  # 即此节点变为种树
        return child

    def evolve(self):
        pop = self.select()
        # pop_copy = pop.copy()###要保持原样本???
        # print('前',pop)
        for parent in pop:  # for every parent
            # child = self.crossover(parent, pop_copy)
            child = self.mutate(parent)
            parent[:] = child
        # print('后',pop)
        self.pop = pop
###实例化对象
ga = GA(DNA_size=DNA_SIZE, mutation_rate=MUTATION_RATE, pop_size=POP_SIZE)  ###初始化
best_fit=[]
# best_fit=[0,1e-5]

for generation in range(N_GENERATIONS):
    fitness = ga.get_fitness()
    # print(fitness)
    # print(np.argmax(fitness))
    best_index = np.argmax(fitness)
    best_fit.append( fitness[best_index] )
    best_result = ga.pop[np.argmax(fitness)]###返回本代适应度最高个体
    print('Gen', generation, ': ', best_result ,'-'*10,'fitness:' , best_fit[generation])
    # print('Gen', generation, ': ', best_result)
        ###终止的指标,很重要很重要,三代没有变化则停止
    if generation>=2:
        if best_fit[generation] == best_fit[generation-1] and best_fit[generation-1] == best_fit[generation-2]:
            break  
    ga.evolve()
# print(best_fit)
# print(sum(flowerbed))
# print(n)
if best_fit[-1]-sum(flowerbed)<n:
    print("False")
else:
    print("True")

可以看看注释,记住GA类面向对象的操作,在种群迭代时还是很方便的,这种形式最好保持。因为种花情况的情况的原始值确定了,而且只能由0变1,即此处种花。所以变异这个操作大大简化了。而花不能相邻很明显是硬约束,你可以选择一个很大的负适应度。但因为没有用前10%复制,有放回抽样函数:

np.random.choice(np.arange(self.pop_size),size=self.pop_size,replace=True, p=fitness/fitness.sum())

其中的p对应每个个体抽到下轮的机会。所以将相邻种花的情况适应度定义为0。注意收敛条件导致的generation>=2。

举个好吃的栗子:

需要源码点击阅读原文,如果对你有所帮助的话,记得为公众号添加星标啊。

END

作者:不爱跑马的影迷不是好程序猿

   喜欢的话请关注点赞👇 👇👇 👇                     

图片

壹句: 欲买桂花同载酒

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值