精华内容
下载资源
问答
  • 遗传算法

    万次阅读 多人点赞 2019-04-06 21:41:47
    使用遗传算法求解多峰函数的最大值,是我的项课程作业,做完之后,顺便把文档整理出来做记录。全部内容如下: 1、问题描述 编程实现遗传算法,并求解多峰函数的最大值。多峰函数的表达式如下所示: 用MATLAB...

    使用遗传算法求解多峰函数的最大值,是我的一项课程作业,做完之后,顺便把文档整理出来做个记录。全部内容如下:

    1、问题描述

    编程实现遗传算法,并求解多峰函数的最大值。多峰函数的表达式如下所示:
    在这里插入图片描述
    用MATLAB做出函数的图像如下:
    在这里插入图片描述

    2、算法描述及实现

    2.1、遗传算法概述

    遗传算法(GA,Genetic Algorithm),也称为进化算法。遗传算法是受达尔文的进化论的启发,借鉴生物进化过程而提出的一种启发式搜索算法。其主要特点是直接对结构对象进行操作,因此不同于其他求解最优解的算法,遗传算法不存在求导和对函数连续性的限定,采用概率化的寻优方法,不需要确定的规则就能自动获取和指导优化的搜索空间,自适应地调整搜索方向。

    以上是对遗传算法相对抽象的总结,为了更具体形象的解释遗传算法的一般原理,我们首先介绍一些生物学上的概念

    ①种群:不同生物个体形成的群体,生物的进化以群体的形式进行,这样的一个群体称为种群;

    ②个体:组成种群的单个生物;

    ③基因:带有遗传信息的DNA片段,可以通俗的将基因理解为一段信息,这段信息决定的生物个体的性状;

    ④表现型:根据基因形成的个体的外部表现;

    ⑤适应度:生物个体对于生存环境的适应程度,越适应那么其得以存活和繁衍的概率就越大;

    ⑥遗传:通过繁殖过程,子代将从父母双方各获取一部分基因,形成新的自己的基因,这个过程中,会发生基因的复制、交叉,也会以较低的概率发生基因突变;

    ⑦自然选择:物竞天择,适者生存的自然淘汰机制。具体为对环境适应度高的个体参与繁殖的机会比较多,后代就会越来越多。适应度低的个体参与繁殖的机会比较少,后代就会越来越少;

    ⑧进化:种群通过代际繁衍不断适应生存环境的过程,在这个过程中,以对外界环境的适应度为评判标准,生物的性状不断得到改良。

    了解了这些术语的含义,我们就可以进一步说说生物进化的过程了。由于自然选择是客观存在的,即生物只能改变自己去适应环境,那么在自然选择的过程中,适应度低的个体会被淘汰,适应度高的个体被保留,高适应度的父体与母体又有更高的概率繁衍出适应度高的子代,因此在一代又一代的繁衍之后,高适应度的个体在种群中所占的比例越来越大,种群就这样完成了进化。

    现在我们要参考生物进化的过程来设计算法解决求最优解的问题。对此,遗传算法的思路是,将要解决的问题模拟成一个生物进化的过程,通过进化来寻找最优解。以我们题目中寻找多峰函数的最大值这个问题为例

    将(x, y)这一可能的解作为一个个体;将多峰函数的函数值f(x, y)作为个体的适应度;对(x, y)进行编码作为个体的基因;以适应度为标准不断筛选生物个体;通过遗传算子(如复制、交叉、变异等)不断产生下一代。如此不断循环迭代,完成进化。最终,根据设定的迭代次数,可得到最后一代种群,该种群中的个体适应度都较高,而多峰函数的最大值就有比较大的概率存在于这一群解中,以种群中适应度最高的个体作为问题的解,则可以说该解有比较高的概率就是我们希望求得的最优解。

    文字述说终究还是不如图表好理解,因此还是看图吧(下图将本题与自然遗传联系了起来):
    在这里插入图片描述
    通过以上描述,我们不难看出,遗传算法不能保证一定能求得最优解,而只能以一定的概率求最优解。但是使用遗传算法时,我们可以不用关心具体如何去找最优解,要做的只是简单的否定一些表现不好的个体。这一优点也是遗传算法能够取得广泛应用的原因之一。

    2.2、算法的流程

    通过上文的阐述,对于如何模拟自然进化来求题中多峰函数的最优解已经比较明晰了。这里我将列出遗传算法的主要步骤,并一一解析:

    第一步:随机产生一个种群,作为问题的初代解(通常,初代解可能与最优解相差较大,这是可以容忍的,只要保证初代解是随机产生的,以确保个体基因的多样性即可);

    第二步:寻找一种合适的编码方案对种群中的个体进行编码,可以选择如浮点数编码或二进制编码等常用编码方案(需要指出的是,不同的编码方案直接影响后续遗传算子的实现细节);

    第三步:以多峰函数的函数值 作为个体的适应度,计算种群中每个个体的适应度(算出的适应度将为后续的个体选择提供依据);

    第四步:根据适应度的高低选择参与繁衍的父体与母体,选择的原则是适应度越高的个体越可能被选中(以此不断淘汰适应度低的个体);

    第五步:对被选出的父体与母体执行遗传操作,即复制父体与母体的基因,并采用交叉、变异等算子产生出子代(在较大程度保留优秀基因的基础上,变异增加了基因的多样性,从而提高找到最优解的概率);

    第六步:根据一定的准则判断是继续执行算法,还是找出所有子代中适应度最高个体作为解返回并结束程序(判断的准则可以是设定的解的阈值、指定的迭代次数等)。
    在这里插入图片描述

    2.3、算法的编码实现

    2.3.1、编码

    本文采用的是二进制编码方式,这种编码方式编解码过程简单易行,相应的交叉算子、变异算子等操作用位运算即可实现。当然,它也有一定的缺点,比如连续性不够强。为保证求解的精度,本文使用14个bit为 编码,使用11个bit为 编码,两者组合成25个bit的最终结果。不难算出,该方式的编码精度可达千分位。具体的编码操作可总结为,将 或 的取值区间映射到0~2n-1这一整数范围,其中n表示编码位数, 或 在其取值区间的某点,相应的映射到整数区间中的某点,改点即为 或 的基因编码。程序如下:

    /*
        基因编码
        gene1       输入型参数,待编码的基因段1
        gene2       输入型参数,待编码的基因段2
        gene_code   输出型参数,基因编码
    
        返回值:当输入的基因不符合要求时返回false,否则返回true
    */
    static bool gene_encode(const double gene1, const double gene2, unsigned int *gene_code)
    {
        /* 判断基因是否合法 */
        if (!is_gene_legal(gene1, gene2))
            return false;
    
        /* 若基因合法则对其进行编码 */
        unsigned int gene1_code = (gene1 - GENE1_RANGE_LEFT) * (GENE1_CODE_MAX - 1) / (GENE1_RANGE_RIGHT - GENE1_RANGE_LEFT);
        unsigned int gene2_code = (gene2 - GENE2_RANGE_LEFT) * (GENE2_CODE_MAX - 1) / (GENE2_RANGE_RIGHT - GENE2_RANGE_LEFT);
        
        /* 组合基因片段 */
        *gene_code = (gene1_code << 11) | gene2_code;
    
        return true;
    }
    

    2.3.2、解码

    解码是编码的逆过程,无需赘述,程序如下:

    /*
        基因解码
        gene_code   输入型参数,基因编码
        gene1       输出型参数,解码后的基因段1
        gene2       输出型参数,解码后的基因段2
    
        返回值:当输入的基因编码不符合要求时返回false,否则返回true
    */
    static bool gene_decode(const unsigned int gene_code, double *gene1, double *gene2)
    {
        /* 判断基因编码是否合法 */
        if (!is_gene_code_legal(gene_code))
            return false;
    
        /* 若基因编码合法则对其进行解码 */
        unsigned int gene1_code = GET_GENE1_CODE(gene_code);
        unsigned int gene2_code = GET_GENE2_CODE(gene_code);
    
        *gene1 = (double)gene1_code * (GENE1_RANGE_RIGHT - GENE1_RANGE_LEFT) / (GENE1_CODE_MAX - 1) + GENE1_RANGE_LEFT;
        *gene2 = (double)gene2_code * (GENE2_RANGE_RIGHT - GENE2_RANGE_LEFT) / (GENE2_CODE_MAX - 1) + GENE2_RANGE_LEFT;
    
        return true;
    }
    

    2.3.3、计算适应度

    适应度函数也称评价函数,通常用于区分群体中个体好坏的标准。适应度高的,也就是优秀的个体有更大的几率参与繁衍,遗传自己的基因。一般的,适应度函数根据目标函数来确定,有时候直接将目标函数值作为适应度。这里,考虑到待求解的多峰函数,尖峰分布密集而且峰的直径很窄,这不利于遗传算法的收敛,因此本文不直接将多峰函数值作为适应度,而是利用对数函数将多峰函数进行平缓,并将平缓后的函数值作为目标函数。具体做法是,将多峰函数进行两次求对数,因此,多峰函数与适应度的关系可如下表示:
    在这里插入图片描述
    用MATLAB做出适应度函数图像如下:
    在这里插入图片描述
    对比前文中的图不难看出,图像得到了有效的平缓,同时不同峰之间也保持着一定的高低之别。值得一提的是,这里更主要的是给出优化遗传算法的一个思路,即可以在适应度函数上做文章。本题的适应度函数只是对多峰函数本身做了一个简单的变换,读者不妨思考一下,就本题而言有没有什么非常好的适应度函数。

    据上文所述,适应度求值函数如下:

    /*
        多峰函数:z = 21.5 + x *sin(4 * 3.1415926 * x) + y * sin(20 * 3.1415926 * y)
        适 应 度:log(log(z))
        约    束:-3.0 <= x <= 12.1; 4.1 <= y <= 5.8
        精    度:精确到千分位
    */
    double get_fitness(const double x, const double y)
    {
        return log(log(21.5 + x * sin(4 * PI * x) + y * sin(20 * PI * y)));
    }
    

    2.3.4、选择算子

    本文的选择算法采用了非常常用的“轮盘赌算法”,赌盘算法的原理非常简单明了。创建赌盘时,我们将种群中所有个体的适应度求和,不妨将得到的结果称为总和适应度。然后,将每个个体的适应度除以总和适应度,然后将得到的商逐个累加,每加一次就得到赌盘的一个边界,累加完成后总和为1。如下的饼状图可以更形象的表明赌盘的原理:
    在这里插入图片描述
    由上文所述,赌盘创建函数可如下编写:

    /*
        创建赌盘
        ga      遗传算法器指针
    */
    static void create_roulette(GA *ga)
    {
        /* 计算赌盘中的概率 */
        ga->roulette[0] = ga->fitness[0] / ga->sum_fitness;
    
        for (int num = 1; num < ga->population_num - 1; num++)
        {
            ga->roulette[num] = ga->roulette[num - 1] + ga->fitness[num] / ga->sum_fitness;
        }
    
        ga->roulette[ga->population_num - 1] = 1.0;
    }
    

    再回到选择算子,选择算子需要赌盘作为基础,其运行时,会产生一个0到1的随机数,然后在赌盘中找到该数所在的区间,这个区间对应的个体即为被选中的个体。因此,适应度越高的个体被选中的几率越大,这是合理的。当然,也存在较小的概率选出适应度较低的个体,为了避免这种情况,本文引入了竞争机制,即一次选择的过程选出2个个体,再取其中适应度较高的那个个体,具体的程序如下:

    /*
        基因选择函数
        ga      遗传算法器指针
        返回值:返回使用轮盘赌的方式选出的个体(编号)
        说  明:选择策略为轮盘赌+随机竞争
    */
    static unsigned int select(GA *ga)
    {
        unsigned int index1 = 0, index2 = 0;
    
        /* 产生一个[0.0, 1.0]之间的浮点数 */
        double selector1 = rand() * 1.0 / RAND_MAX;
        double selector2 = rand() * 1.0 / RAND_MAX;
    
        /* 找出被选中的个体的索引 */
        for (; selector1 > ga->roulette[index1]; index1++);
        for (; selector2 > ga->roulette[index2]; index2++);
    
        return (ga->fitness[index1] > ga->fitness[index2] ? index1 : index2);
    }
    

    2.3.5、交叉算子

    遗传算法的交叉操作实质上是按某种方式交换父体和母体的部分基因,常见的交叉算子有单点交叉、两点交叉、多点交叉、均匀交叉及算术交叉等。本文选用两点交叉法,实现过程既不复杂,也有较好的随机性,该方法可由下图示意:
    在这里插入图片描述
    图中虚线指出的两个交叉点是随机产生的。具体程序如下:

    /*
        交叉函数
        ga          遗传算法器指针
        one         输出型参数,待交叉基因
        another     输出型参数,待交叉基因
        说明:
        1.对传入的基因编码执行两点交叉操作
    */
    static void cross(GA *ga, unsigned int *one, unsigned int *another)
    {
        /* 1.随机产生两个交叉点的位置 */
        unsigned char pos1 = rand() % GENE_CODE_LENGTH + 1;
        unsigned char pos2 = rand() % GENE_CODE_LENGTH + 1;
        unsigned char min_pos = min(pos1, pos2);
        unsigned char max_pos = max(pos1, pos2);
    
        /* 2.截出需要交换的基因段 */
        unsigned int one_gene_seg = get_bits(*one, min_pos, max_pos) << (min_pos - 1);
        unsigned int another_gene_seg = get_bits(*another, min_pos, max_pos) << (min_pos - 1);
        unsigned int mask = ~(get_bits(~(0U), min_pos, max_pos) << (min_pos - 1));
    
        /* 3.执行交叉操作 */
        *one = (*one & mask) | another_gene_seg;
        *another = (*another & mask) | one_gene_seg;
    }
    

    2.3.6、变异算子

    在自然界中,基因变异可以增加个体的多样性,这对于遗传算法来说是增加了个体的随机性,可以增加找到最优解的概率。本文采用的变异算子所做的操作是随机选择基因的某一位进行反转,程序如下:

    /*
        变异函数
        gene_code       输入型参数
        说明:
        1.对传入的基因编码执行变异操作
        2.随机选择基因编码中的一位做反转操作
    */
    static void mutate(unsigned int *gene_code)
    {
        unsigned int mutate_bit = 1 << (rand() % GENE_CODE_LENGTH);
        *gene_code ^= mutate_bit;
    }
    

    2.3.7、繁殖函数及进化函数

    遗传算法的主要算子都在上文中分析过了,下面要做的就是根据遗传算法的流程将这些算子整合起来以实现算法功能。在本文中,这其中涉及到两个关键的函数,即繁殖函数和进化函数。繁殖函数包括基因的复制、交叉及变异,同时本文还采用了子代竞争策略,即父代产生的两个子代个体仅保留适应度最高的,程序如下:

    /*
        繁殖函数
        ga       遗传算法器指针
        father   从种群中选出的父体
        mother   从种群中选出的母体
        返回值:  适应度最高的子代的基因编码
        说明: 
        1.一对父体与母体将繁殖出一对子代
        2.选择出适应性更好的子代返回
    */
    static unsigned int inherit(GA *ga, unsigned int father, unsigned int mother)
    {
        unsigned int son1 = ga->gene_code[father];
        unsigned int son2 = ga->gene_code[mother];
    
        /* 1.交叉 */
        cross(ga, &son1, &son2);
    
        /* 2.变异 */
        mutate(&son1);
        mutate(&son2);
    
        /* 3.子代竞争 */
        double son1_gene1, son1_gene2, son2_gene1, son2_gene2;
        gene_decode(son1, &son1_gene1, &son1_gene2);
        gene_decode(son2, &son2_gene1, &son2_gene2);
    
        return (ga->get_fitness(son1_gene1, son1_gene2) > ga->get_fitness(son2_gene1, son2_gene2)) ? son1 : son2;
    }
    

    进化函数则实现了遗传算法的一次完整的迭代过程,根据上文给出的遗传算法流程图,不难进行如下编码:

    /*
        进化函数
        ga      遗传算法器指针
    */
    static void evolve(GA *ga)
    {
        /* 1.申请暂存子代基因编码的内存 */
        unsigned int *descendants = (unsigned int *)calloc(ga->population_num, sizeof(unsigned int));
        
        /* 2.精英保留(将上一代中适应度最高的个体的基因编码保留) */
        descendants[0] = ga->gene_code[ga->best_individual];
        
        /* 3.选择合适的父体与母体 */
        unsigned int father = select(ga);
        unsigned int mother = select(ga);
    
        /* 4.繁殖(包含交叉与变异) */
        for (int num = 1; num < ga->population_num; num++)
            descendants[num] = inherit(ga, father, mother);
    
        /* 5.将子代记录到ga中并进行基因解码(使新一代的基因编码与基因对应) */
        for (int num = 0; num < ga->population_num; num++)
        {
            ga->gene_code[num] = descendants[num];
            gene_decode(ga->gene_code[num], &ga->gene[num].gene1, &ga->gene[num].gene2);
        }
        
        /* 5.更新种群适应度 */
        fit(ga);
        
        /* 6.更新赌盘 */
        create_roulette(ga);
    
        /* 7.释放之前申请的空间 */
        free(descendants);
    }
    

    3、运行结果及分析

    至此,本文已经给出了一个遗传算法的C语言实现的所有关键程序。下面就调用编写的遗传算法进行测试。本文将创建含有100个个体的种群,并进行100代迭代以求解多峰函数的最大值,一次完整的调用本文实现的遗传算法的程序如下所示:

    /* 创建遗传算法器 */
    GA *ga = create_ga(get_fitness, 100);
    
    /* 初始化遗传算法器 */
    ga->init(ga);
    
    /*迭代100代*/
    for (int i = 0; i < 100; i++)
    ga->evolve(ga);
    
    /*销毁遗传算法器*/
    delete_ga(ga);
    

    经多次调用测试,算法执行的结果较为稳定,所得的多峰函数最大值大多在38以上,多次运行结果中最好的解为38.849744,对应的坐标为(11.625331, 5.725256)。将迭代求得的最大值用MATLAB作图如下:
    在这里插入图片描述
    为验证是否找到了最优解,用MATLAB遍历求出该多峰函数在给定定义域内的最大值为38.8501,与本文求出的结果相差0.000356,可见本文实现的遗传算法表现还不算太差。

    文中给出的程序比较散,这里给出完整程序的下载链接

    展开全文
  • 粒子群算法及其改进算法

    万次阅读 多人点赞 2019-07-01 19:27:08
    首先在这里介绍一下,这个里主要介绍粒子群算法以及一个改进的二阶振荡粒子群算法。 原理 粒子群优化(PSO)算法是Kennedy和Eberhart受 鸟群群体运动的启发于1995年提出的一种新的群智能优化算法[1]。大概的意思...

    标准粒子群算法及其改进算法

    首先在这里介绍一下,这个里主要介绍粒子群算法以及一个改进的二阶振荡粒子群算法。

    原理

    粒子群优化(PSO)算法是Kennedy和Eberhart受 鸟群群体运动的启发于1995年提出的一种新的群智能优化算法[1]。大概的意思就是一片森林里有一群鸟在找一块食物,它们不知道食物具体在哪,但是可以通过感官(例如嗅觉)去察觉到自己当前位置距离食物的远近。鸟可以记住自己走过的位置并且知道自己做过的最优位置。这一群鸟的运动都是随机的,这类似于一种穷举法。

    标准粒子群算法

    粒子群算法一般用来找一个函数的最优值。这个函数一般就是适应度函数。
    函数中未知量的个数就是这个查找的空间维度。

    假设有N个粒子组成一个种群S

    Xi是代表粒子i所在的位置,i=1,2,…,N

    Vi代表粒子i在位置Xi处的速度,i=1,2,…,N

    pi是记录粒子i到走过的最优位置,i=1,2,…,N

    pg是所有粒子走过的最优的位置,i=1,2,…,N

    w 为惯性权重

    c1 、 c2 为学习因子

    r1,r2为[ 0 , 1 ]之间均匀分布的参数

    接下来种群中每个粒子按照公式更新速度和位置:

    Vi( t +1 ) =w * Vi( t ) + c1 * r1 * (pi - xi( t ) ) + c2 * r2 * ( pg - xi( t ) ) (1)
    xi( t + 1 ) = xi( t ) + vi( t + 1 ) (2)

    PS:这里的r1、r2是每一步迭代都需要更新的随机数
    c1、c2和w =则是一开始给定的一些参数,至于参数的给定取决于你自己每次测试这个程序所得到的经验–即哪些参数你跑出的结果比较好就选择哪些参数。
    在这里再插一句,在我的实践中认为没有必要去限制迭代过程中速度和位置是否超过最大值,如果在给定区间内存在最优值那么最后还是会迭代回来。只需要在给定的区间内初始化就OK了。在我最开始的测试过程中限制速度和位置是使程序变慢了的,但是我一开始的思路出了问题,到很后面才改正过来也么有再去测试这个,所以就不加评论了。

    算法流程

    在这里插入图片描述

    标准PSO算法代码

    function [xm,fv]=Pso(N,c1,c2,w,M,D)
    %c1:自我学习因子
    %c2:社会学习因子

    %w:惯性权重
    %N:种群数量`
    %M:最大迭代次数
    %D:维数

    %初始化种群、速度和位置
    tic
    for i=1:N
    for j=1:D
    x(i,j)=unifrnd(-10,10);
    v(i,j)=unifrnd(-10,10);
    end
    end

    %根据适应度计算适应度值,初始化pi和pg
    for i=1:N
    y(i)=test(x(i,:));
    pi(i,:)=x(i,:); %y(i,:)为最优位置
    end
    k = find( y == min(y) ) ;
    pg=x(k(1), : ) ;
    %更新位置与速度
    for t=1:M
    for i=1:N
    v(i,:)=wv(i,:)+c1rand*(pi(i,:)-x(i,:))+c2rand(pg-x(i,:));
    x(i,:)=x(i,:)+v(i,:);
    if test(x(i,:))<y(i)
    y(i)=test(x(i,:));
    pi(i,:)=x(i,:);
    end
    if y(i)<test(pg)
    pg=pi(i,:);
    end
    end
    pbest(t)=test(pg);
    end

    %输出结果
    disp(‘最优位置’);
    xm=pg
    disp(‘最优值’);
    fv=test(pg)
    plot(pbest)
    toc

    test函数–就是适应度函数

    function y = test( V )
    y=0;
    b=length(V);
    for i=1:b
    y=y+i*V(i)^2;
    end

    标准粒子群算法的局限性

    为进一步说明标准粒子群算法的局限性,做如下推理:
    设 φ1=c1*r1 ,φ2=c2r2 ,w=1,由式(1)、(2)可转化 为式(3)、(4):
    Vi( t+1) =Vi( t) +φ1(pi -xi(t))+φ2(pg -xi(t)) (3)
    xi(t+1)=xi(t)+vi(t+1) (4)
    已知速度公式Vi+1=Vi +a×Δt ,可得: a=φ1(pi -xi(t))+φ2(pg -xi(t))=xi(t)″ (5)
    化简可得: xi(t)″+(φ1+φ2)xi(t)-(φ1pi +φ2pg)=0 (6)
    式(6)是二阶微分方程,通过二阶微分方程求解公式,求得搜索位置在 t 代的 x(t)的位置路径公式: xi(t)=C1 cos( φ1+φ2t)+C2 sin( φ1+φ2t) (7)
    由式(7)可知,xi(t) 在一个固定区间内波动,即在 该区间中寻找每个粒子第 t 次迭代后的位置,位置函数 xi(t) 没有振荡收敛,算法容易陷入局部最优。
    例如,令 C1=1,C2=1, φ1+φ2 =1 ,可得位置函数 xi(t)=cos(t)+ sin(t)的图像,如图1所示。在该图像中, 对于t∈[ -∞,+∞] , xi(t) 始终在固定上下限 [- 2, 2] 内范围波动,没有振 荡收敛,容易陷入局部最优。因此,在算法中加入振荡 收敛,是跳出局部最优解,提高粒子群算法搜索性能和精度较有效的方法。[1]

    在这里插入图片描述

    改进标准粒子群算法的思想

    胡建秀,曾建潮通过在标准二阶粒子群算法速度迭 代方程中引入二阶振荡环节的方法改进算法,来增加粒 子的多样性,提高算法的全局搜索能力,是改进位置函 数搜索区域较好的改进方法。使用二阶振荡环节后,算法前期具有较强的全局搜索能力,振荡收敛;而在后期强局部搜索,渐近收敛。 该粒子群算法的进化方程如下:
    Vi( t+1) =w×Vi( t ) + φ1(pi -(1+ξ1)xi(t)+ξ1xi(t-1))+ φ2(pg -(1+ξ2)xi(t)+ξ2xi(t-1)) (8) xi(t+1)=xi(t)+vi(t+1) (9) 算法振荡收敛:
    ξ1<2 φ1 -1 φ1 ,
    ξ2<2 φ2 -1 φ2 (10)
    算法渐进收敛:
    ξ1 ≥2 φ1 -1 φ1 ,
    ξ2 ≥2 φ2 -1 φ2 (11) 振荡收敛和渐进收敛示意图如图2和图3所示。[ 1 ]
    在这里插入图片描述

    改进后二阶振荡粒子群算法的迭代公式

    Vi( t+1 ) =w×Vi( t ) + φ1(pi -(1+ξ1)xi(t)+ξ2xi(t-1))+ φ2(pg -(1+ξ3)xi(t)+ξ4xi(t-1)) (12) xi(t+1)=xi(t)+vi(t+1)

    这个公式的实在上面的二阶振荡粒子群算法的基础上进行的改进,这里ξ虽然是随机数但是取值是有限制的:
    设最大迭代次数为Gmax ,
    0<ξ2<1+ξ1 2 ,0<ξ4 <1+ξ3 2 ,0<ξ1<1,0<ξ3<1 当 t < Gmax / 2时,
    ξ1<ξ2-1,ξ3<ξ4 -1,0<ξ2<1,0<ξ4 <1 当 t > Gmax / 2 时
    这么做的意义在于可以时算法在迭代的前半段振荡收敛;
    在迭代的后半段使其渐进收敛。
    这里的证明和上面的二阶振荡粒子群算法的类似,我这就不展开了,感兴趣的可以自己去找我参考的文献。
    PS:关于改进算法的流程图和标准算法的类似,无非就是加了一个迭代次数前一半和后一半参数的改变,这里就不加上去了。

    改进二阶振荡粒子群算法代码

    function [ z , best ] = PSO_1( w , Gmax ,lizishu , weidu ,a , b )
    %这个测试函数的最小值是0,取值范围是[-10 , 10]
    % z是最优点的适应值,best是记录最优位置的坐标

    %lizishu输入需要的粒子数
    %weidu函数的自变量个数
    %a下界
    %b上界
    q=zeros(1,Gmax);
    tic
    p = inf * ones( 1 , lizishu ) ;%初始的最优解默认为inf
    % pg = 0 ;
    A = unifrnd( a , b , lizishu , weidu ) ;%A矩阵是位置矩阵,用于储存每一步计算出的位置
    B = unifrnd( a , b , lizishu , weidu ) ;%B是速度矩阵,用于储存每一布计算出的V
    C = A ;%用于记录每个粒子的最优位置
    D = unifrnd( a , b , lizishu , weidu ) ;%位置矩阵,用于记录上一次迭代的位置
    c = 2 ; %c通常取1.5 , 这是一个经验值
    y = [] ;%给y申请空间
    for i = 1 : lizishu
    y = [ y , test( A( i ,: ) ) ] ;
    end

    for i = 1 : lizishu
    if p( i ) > y( i )
    C( i , : ) = A( i , : ) ;
    end
    p( i ) = min( [ p( i ) , y( i ) ] ) ;%更新局部最优解
    end

    %初始化全局最优解
    k = find( p == min( p ) ) ;
    pg = C( k( 1 ) , : ) ;
    q( i ) = test( pg ) ;

    for i = 1 : Gmax
    i
    r1 = unifrnd( 0 , 1 ) ;
    r2 = unifrnd( 0 , 1 ) ;
    u1 = r1 * c ;
    u2 = r2 * c ;
    if i < Gmax / 2
    g1 = unifrnd( 0 , 1 ) ; % 这里g1是[ 0 , 1 ]上均匀分布的随机数
    g2 = ( 1 + g1 ) / 2 ;
    g3 = unifrnd( 0 , 1 ) ;
    g4 = ( 1 + g3 ) / 2 ;
    for j = 1 : lizishu
    B( j , : ) = w * B( j , : ) + u1 * ( C( j , : ) - ( 1 + g1 ) * A( j , : ) + g2 * D( j , : ) ) + u2 * ( pg - ( 1 + g3 ) * A( j , : ) + g4 * D( j , : ) ) ;
    D = A ;
    A( j , : ) = A( j , : ) + B( j , : ) ;
    end
    else
    g2 = unifrnd( 0 , 1 ) ; % 这里g2是[ 0 , 1 ]上均匀分布的随机数
    g1 = ( g2 - 1 ) / 4 ;
    g4 = unifrnd( 0 , 1 ) ;
    g3 = ( g4 - 1 ) / 4 ;
    for j = 1 : lizishu
    B( j , : ) = w * B( j , : ) + u1 * ( C( j , : ) - ( 1 + g1 ) * A( j , : ) + g2 * D( j , : ) ) + u2 * ( pg - ( 1 + g3 ) * A( j , : ) + g4 * D( j , : ) ) ;
    D = A ;
    A( j , : ) = A( j , : ) + B( j , : ) ;
    end
    end
    y = [] ;%下一次迭代前清空y
    for j = 1 : lizishu
    y = [ y , test( A( j ,: ) ) ] ;
    end

       for j = 1 : lizishu
          if p( j ) > y( j )
            C( j , : ) = A( j , : ) ;
          end
            p( j ) = min( [ p( j ) , y( j ) ] ) ;
       end
    
        k = find( p == min( p ) ) ;%找到最优解的位置
        pg = C( k( 1 ) , : ) ; %保存最优解坐标
        q(i) = test( pg ) ;
    

    end
    z = q( Gmax ) ;
    best = pg ;
    toc
    plot(q)

    test函数

    function y = test( V )
    y=0;
    b=length(V);
    for i=1:b
    y=y+i*V(i)^2;
    end

    两个算法的比较

    PS:上面我只给了一个test函数,要测试其他的函数直接更改test函数即可。
    下面是两个维度跑出来的结果
    1、标准PSO算法:
    在这里插入图片描述
    在这里插入图片描述
    2、改进的二阶振荡PSO算法:
    在这里插入图片描述

    在这里插入图片描述
    在低维度上这两个算法没有太大差别,改进的算法速度上要稍微快一点。

    下面把维度提升到100维:
    PS:为了便于观看我改了一下程序,最后都只输出一个最优值。
    1、这是标准PSO算法跑出的结果:在这里插入图片描述
    很明显这并没有达到最优值,只是一个局部最优。

    2、改进的PSO算法:
    在这里插入图片描述
    可以看到改进的算法的结果在100维下依旧不错,而且很快。

    下面我们尝试1000维:
    改进的PSO算法结果如下:在这里插入图片描述

    我也试过一些最小值是无穷的函数(X^3),以及一些振荡函数(sinx+cosx),都可以跑出结果,这里就不一个个的给出结果了。

    PS:因为第一个算法不是我写的原因,是我同学写的我拿来用的,所以两个代码在风格上差别有点大。
    这个博客的证明部分基本上我都是从下面的文献里直接拿过来的。
    最后如果找出我的错误请告诉我,我会及时改正的。

    参考文献

    [ 1 ] 蒋丽,叶润周,梁昌勇等,改进的二阶振荡粒子群算法[J],计算机工程与应用,2009,55(9):130-139

    展开全文
  • 小到控制一个元件的温度,大到控制无人机的飞行姿态和飞行速度等等,都可以使用PID控制。这里我们从原来上来理解PID控制。 PID(proportion integration differentiation)其实就是指比例,积分,微分控制。先把图片...

    一文读懂PID控制算法(抛弃公式,从原理上真正理解PID控制)

    PID控制应该算是应用非常广泛的控制算法了。小到控制一个元件的温度,大到控制无人机的飞行姿态和飞行速度等等,都可以使用PID控制。这里我们从原理上来理解PID控制。
    PID(proportion integration differentiation)其实就是指比例,积分,微分控制。先把图片和公式摆出来,看不懂没关系。(一开始看这个算法,公式能看懂,具体怎么用怎么写代码也知道,但是就是不知道原理,不知道为什么要用比例,微分,积分这3个项才能实现最好的控制,用其中两个为什么不行,用了3个项能好在哪里,每一个项各有什么作用

    PID控制算法原理

    PID控制算法公式

    总的来说,当得到系统的输出后,将输出经过比例,积分,微分3种运算方式,叠加到输入中,从而控制系统的行为,下面用一个简单的实例来说明。

    比例控制算法

    我们先说PID中最简单的比例控制,抛开其他两个不谈。还是用一个经典的例子吧。假设我有一个水缸,最终的控制目的是要保证水缸里的水位永远的维持在1米的高度。假设初试时刻,水缸里的水位是0.2米,那么当前时刻的水位和目标水位之间是存在一个误差的error,且error为0.8.这个时候,假设旁边站着一个人,这个人通过往缸里加水的方式来控制水位。如果单纯的用比例控制算法,就是指加入的水量u和误差error是成正比的。即
    u=kp*error
    假设kp取0.5,
    那么t=1时(表示第1次加水,也就是第一次对系统施加控制),那么u=0.5*0.8=0.4,所以这一次加入的水量会使水位在0.2的基础上上升0.4,达到0.6.
    接着,t=2时刻(第2次施加控制),当前水位是0.6,所以error是0.4。u=0.5*0.4=0.2,会使水位再次上升0.2,达到0.8.
    如此这么循环下去,就是比例控制算法的运行方法。
    可以看到,最终水位会达到我们需要的1米。
    但是,单单的比例控制存在着一些不足,其中一点就是 –稳态误差!(我也是看了很多,并且想了好久才想通什么是稳态误差以及为什么有稳态误差)。
    像上述的例子,根据kp取值不同,系统最后都会达到1米,不会有稳态误差。但是,考虑另外一种情况,假设这个水缸在加水的过程中,存在漏水的情况,假设每次加水的过程,都会漏掉0.1米高度的水。仍然假设kp取0.5,那么会存在着某种情况,假设经过几次加水,水缸中的水位到0.8时,水位将不会再变换!!!因为,水位为0.8,则误差error=0.2. 所以每次往水缸中加水的量为u=0.5*0.2=0.1.同时,每次加水缸里又会流出去0.1米的水!!!加入的水和流出的水相抵消,水位将不再变化!!
    也就是说,我的目标是1米,但是最后系统达到0.8米的水位就不在变化了,且系统已经达到稳定。由此产生的误差就是稳态误差了。
    (在实际情况中,这种类似水缸漏水的情况往往更加常见,比如控制汽车运动,摩擦阻力就相当于是“漏水”,控制机械臂、无人机的飞行,各类阻力和消耗都可以理解为本例中的“漏水”)
    所以,单独的比例控制,在很多时候并不能满足要求。

    积分控制算法

    还是用上面的例子,如果仅仅用比例,可以发现存在暂态误差,最后的水位就卡在0.8了。于是,在控制中,我们再引入一个分量,该分量和误差的积分是正比关系。所以,比例+积分控制算法为:
    u=kp*error+ ki ∗ ∫ error
    还是用上面的例子来说明,第一次的误差error是0.8,第二次的误差是0.4,至此,误差的积分(离散情况下积分其实就是做累加), error=0.8+0.4=1.2. 这个时候的控制量,除了比例的那一部分,还有一部分就是一个系数ki乘以这个积分项。由于这个积分项会将前面若干次的误差进行累计,所以可以很好的消除稳态误差(假设在仅有比例项的情况下,系统卡在稳态误差了,即上例中的0.8,由于加入了积分项的存在,会让输入增大,从而使得水缸的水位可以大于0.8,渐渐到达目标的1.0.)这就是积分项的作用。

    微分控制算法

    换一个另外的例子,考虑刹车情况。平稳的驾驶车辆,当发现前面有红灯时,为了使得行车平稳,基本上提前几十米就放松油门并踩刹车了。当车辆离停车线非常近的时候,则使劲踩刹车,使车辆停下来。整个过程可以看做一个加入微分的控制策略。
    微分,说白了在离散情况下,就是error的差值,就是t时刻和t-1时刻error的差,即u=kd*(error(t)-error(t-1)),其中的kd是一个系数项。可以看到,在刹车过程中,因为error是越来越小的,所以这个微分控制项一定是负数,在控制中加入一个负数项,他存在的作用就是为了防止汽车由于刹车不及时而闯过了线。从常识上可以理解,越是靠近停车线,越是应该注意踩刹车,不能让车过线,所以这个微分项的作用,就可以理解为刹车,当车离停车线很近并且车速还很快时,这个微分项的绝对值(实际上是一个负数)就会很大,从而表示应该用力踩刹车才能让车停下来。
    切换到上面给水缸加水的例子,就是当发现水缸里的水快要接近1的时候,加入微分项,可以防止给水缸里的水加到超过1米的高度,说白了就是减少控制过程中的震荡。

    现在在回头看这个公式,就很清楚了
    这里写图片描述
    括号内第一项是比例项,第二项是积分项,第三项是微分项,前面仅仅是一个系数。很多情况下,仅仅需要在离散的时候使用,则控制可以化为
    这里写图片描述
    这里写图片描述
    每一项前面都有系数,这些系数都是需要实验中去尝试然后确定的,为了方便起见,将这些系数进行统一一下:
    这里写图片描述
    这样看就清晰很多了,且比例,微分,积分每个项前面都有一个系数,且离散化的公式,很适合编程实现。
    讲到这里,PID的原理和方法就说完了,剩下的就是实践了。在真正的工程实践中,最难的是如果确定三个项的系数,这就需要大量的实验以及经验来决定了。通过不断的尝试和正确的思考,就能选取合适的系数,实现优良的控制器。

    展开全文
  • 一个算法对于一个输入的循环次数可以事先估计出来吗,一个选择题的选项
  • 贪心算法

    千次阅读 多人点赞 2019-04-07 15:16:00
    贪心算法贪心算法的定义贪心算法解决问题算法流程 贪心算法的定义 贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上...

    贪心算法的定义

    贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

    贪心算法解决问题

    (1)你有n堆石头质量分别为W1,W2,W3…Wn.(n<=100000)现在需要你将两堆石头合并,问一共所用力量最小是多少?
    例如:
    n=4;
    3,1,7,5;
    解题思路:每次挑重量最少的两堆石头,直到所有石头挑完。
    如例题,先将三堆石头按照重量从小到大排序为:1,3,5,7。第一次所用最小力量为1+3=4;然后将4,5,7按从小到大排序,第二次所用最小力量为4+5=9;然后将9,7按从小到大排序,为7,9,第三次所用最小力量为7+9=16;所以一共所用最小力量:4+9+16=29.

    #include<stdio.h>
    typedef struct stone
    {
        int num;
    }Stone;
    Stone st[100];
    void sort(int start_flag,int n)//对n堆石头质量按照从小到大的顺序排序
    {
        int i,j;
        Stone temp;
        for(i=start_flag;i<n-1;i++)
            for(j=start_flag;j<n-1-i;j++)
            if(st[j].num>st[j+1].num)
        {
            temp=st[j];
            st[j]=st[j+1];
            st[j+1]=temp;
        }
    }
    void main()
    {
        int n,i,count=0,start_flag=0;
        printf("输入石头堆数:");
        scanf("%d",&n);
        for(i=0;i<n;i++)
            scanf("%d",&st[i].num);
        for(i=start_flag;i<n-1;i++)
        {
            sort(start_flag,n);
            st[i+1].num+=st[i].num;
            count+=st[i+1].num;
            start_flag++;
        }
        printf("一共所用最少力量%d \n",count);
    }
    

    (2)公司的会议室每天都会有许多会议,有时这些会议的计划时间会发生冲突,需要选择出一些会议。小刘的工作就是安排公司会议室的会议,每个时间最多安排一个会议。现在小刘有一些会议计划的时间表,他想尽可能的安排更多的会议,请问他该如何安排。
    输入
    第一行输入一个整数n(1<n<10000)表示该测试数据共有n个活动。
    随后的n行,每行有两个正整数Bi,Ei(0<=Bi,Ei<10000),分别表示第i个活动的起始与结束时间(Bi<=Ei)
    输出
    输出最多能够安排的会议数量,以及哪些会议被安排。
    样例输入
    3
    1 10
    10 11
    11 20
    样例输出
    最多能够安排的会议数量:2
    安排的会议分别为:
    第1个会议.第3个会议.

    #include<stdio.h>
    typedef struct meet
    {
        int begin;
        int end;
        int flag;
    
    } Meet;
    Meet mt[10000];
    void sort(int n)//根据每场会议的结束时间排序
    {
        int i,j;
        Meet temp;
        for(i=0; i<n-1; i++)
            for(j=0; j<n-1-i; j++)
                if(mt[j].end>mt[j+1].end)
                {
                    temp=mt[j];
                    mt[j]=mt[j+1];
                    mt[j+1]=temp;
                }
    }
    void main()
    {
        int n,i,j,count=1;
        printf("输入会议数:");
        scanf("%d",&n);
        for(i=0; i<n; i++)
            scanf("%d %d",&mt[i].begin,&mt[i].end);
        sort(n);
        i=0;
        mt[0].flag=1;
        for(j=1; j<n; j++)
        {
            if(mt[j].begin>mt[i].end)
            {
                mt[j].flag=1;
                count++;
                i++;
            }
        }
        printf("\n最多能够安排的会议数量:%d \n",count);
        printf("安排的会议分别为:\n");
        for(i=0; i<n; i++)
            if(mt[i].flag==1)
                printf("第%d个会议.",i+1);
    }
    
    

    算法流程

    Created with Raphaël 2.2.0 开始 找到贪心的点,按贪心的点排序 可选择? 结束 yes no
    展开全文
  • RRT路径规划算法

    万次阅读 多人点赞 2019-08-29 21:43:42
    RRT路径规划算法地图RRT算法原理...基本思想是以产生随机点的方式通过一个步长向目标点搜索前进,有效躲避障碍物,避免路径陷入局部极小值,收敛速度快。本文通过matlab实现RRT算法,解决二维平面的路径规划问题。...
  • 算法的复杂度可分为俩种 种时间复杂度 另种是空间复杂度。 俩者的概念:时间复杂度是指执行这个算法所需要的计算工作量;而空间复杂度是指执行这个算法所需要的内存空间。时间和空间(即寄存器)都是计算机资源...
  • MATLAB智能算法30案例分析.史峰等

    万次阅读 2019-03-10 11:36:02
    《MATLAB智能算法30个案例分析》是2011年由北京航空航天大学出版社出版的图书,作者是郁磊、史峰、王辉、胡斐… ...共给出30个案例,每个案例都是一个使用智能算法解决问题的具体实例,所有案例均由理论讲解、案例...
  • 机器学习算法 综述(入门)

    万次阅读 多人点赞 2019-06-16 21:59:28
    学习了一个学期机器学习算法,从什么都不懂到对十个机器学习算法有一定的了解,下面总结一下十大机器学习算法,从算法的概念、原理、优点、缺点、应用等方面来总结,如果错误的地方,欢迎指出。 目录 1.决策树...
  • 一、随机森林算法简介: 在机器学习中,随机森林是一个包含多个决策树的分类器, 并且其输出的类别是由个别树输出的类别的众数而定。 Leo Breiman和Adele Cutler发展出推论出随机森林的算法。而 "Random ...
  • JAVA算法竞赛输入输出专题

    千次阅读 多人点赞 2018-12-23 01:27:00
    小编由于报名了蓝桥杯Java组,所以日常做题从使用C/C++转变成使用Java。在转变的过程中,肯定会遇到很多大大小小的输入输出问题。小编打算总结下来,当做自己学习的材料,也分享给感兴趣的朋友。
  • 语言:C++ #include using namespace std;... cout请依次输入需要存入的数据(尾插法):"; CreateList(L,n); cout单链表L为:"; display(L); max=MaxList(L,e); cout最大值为:"; return 0; }  
  • PWM算法

    千次阅读 2018-05-17 10:13:45
    PWM用于直流斩波,可以用于直流电升压或降压,常见的PWM用于降压。...输出的比特流类似于“1110000000”这是一个占空比为3/10的PWM比特流,它以10个位为一个PWM周期,其中高电平占3个位,所以占空比...
  • #include <bits/stdc++.h> using namespace std; void MaxMin(int a[], int l, int r, int &maxe, int &... if(l == r){ //只有一个元素 maxe = a[l]; mine = a[l]; } else if(l == ...
  • 1.字符串中每个字母都有一个代表的ASCII值,每个字母统计的次数也是一个数值,两个数字可分别作为数值数组的下标和元素: 2.也可利用string中的find、erase和一个整型数字转字符串的函数来实现: 二、代码: //1.在...
  • 文章目录1.序2.动态规划的基本概念[^1]3.动态规划算法的基本思想[^2]4....这篇文章主要介绍动态规划算法的基本思想、使用动态规划算法求解问题的基本步骤、动态规划算法的两基本要素以及一些经典的动态规划问题。...
  • 输入一个 N x N 的数组棋盘,由字符" ","X"和"O"组成,其中字符" "代表一个空位。 以下是井字游戏的规则: 玩家轮流将字符放入空位(" ")中。 第一个玩家总是放字符"O",且第二个玩家总是放字符"X"。 "X"和"O...
  • 从键盘上输入一个后缀表达式,试编写算法计算表达式的值。规定:逆波兰表达式的长 度不超过一行,以 作为输入结束,操作数之间用空格分隔,操作符只可能+、—、*、/四种 运算。例如: 23434 + 2$。* ...
  • 一个带权图中,顶点V0到图中任意一个顶点Vi的一条路径所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径最短的那条路径称为最短路径。 DiskStra算法: 求单源最短路径,即求一个顶点到任意顶点的...
  • 如何评价一个算法的好坏

    万次阅读 2019-04-27 21:38:39
    首先,这个算法必须是正确的 其次,好的算法应该是友好的,便于人们理解和交流,并且是机器可...定义:在计算机科学中,算法的时间复杂度是一个函数,他定量描述了该算法的运行时间.一个算法执行所耗费的时间,从理论上...
  • 为什么算法可以没有输入,可以举具体的例子吗 急!!我刚学数据结构,好多都不理解,求大神支招!
  • 设计一个算法判别一个算术表达式的圆括号是否正确配对。   样例输入: (7+8)*10+( 样例输出: 0 \\不匹配 输入: (7+8)*10 输出: 1 \\匹配   输入输出样例:1组 #1 样例输入: (7+8)*10+( ...
  • #include <iostream> #include <stdlib.h> using namespace std; int main() { int times[36]; char ch; int num; for(int i = 0; i < 36; i++) { times[i] = 0;..."请输入字符"&...
  • 分析假定第一个结点中数据具有最大值,依次与下一个元素比较,若其小于下一个元素,则设其下一个元素为最大值,反复进行比较,直到遍历完该链表。 算法描述 int GetMax(LinkList L){ if(L->next == NULL) ...
  • 我的第算法

    万次阅读 2019-01-18 23:30:40
    481 张步骤图详解 26 个算法和 7 数据结构的基本原理 没有枯燥的理论和复杂的代码,易于理解 采用大量彩色图片,清晰直观,便于记忆 零基础也能轻松掌握,自学算法的好搭档 作者简介 石田保辉,自由职业...
  • 如何对一个算法进行复杂度分析

    千次阅读 2017-06-27 12:05:23
    算法复杂度是在《数据结构》这门课程的第...一个是时间复杂度,一个是渐近时间复杂度。前者是某个算法的时间耗费,它是该算法所求解问题规模n的函数,而后者是指当问题规模趋向无穷大时,该算法时间复杂度的数量级。 
  • BP神经网络:是1986年由Rumelhart和McClelland为首的科学家提出的概念,是种按照误差逆向传播算法训练的多层前馈神经网络,是目前应用最广泛的神经网络。神经网络是把生活中的常见情节推广到计算仿真的范畴,这样...
  • 《机器学习实战》学习笔记():机器学习基础

    万次阅读 多人点赞 2019-08-19 17:01:32
    专栏【机器学习】 【机器学习】《机器学习实战》读书笔记及代码 总目录 ... ————————————————...目录专栏【机器学习】本章内容何谓机器学习2、关键术语3、机器学习的主要任务4、如何选择合适的算法5...
  • 深度学习常见算法的介绍和比较

    万次阅读 多人点赞 2018-02-08 22:00:06
    其实深度学习是机器学习的一个分支。可以理解为具有多层结构的模型。具体的话,深度学习是机器学习中的具有深层结构的神经网络算法,即机器学习>神经网络算法>深度神经网络(深度学习)。 关于深度学习的理论推导,...
  • JavaScript写算法题的输入输出格式

    千次阅读 2018-06-01 21:51:07
    作为名前端程序员,如果在平时的备考刷题时能用自己最熟悉JS来编写代码是件很美滋滋的事情,但是很多小伙伴会发现用JS写的代码过不了OJ,正是因为你的输入输出格式不对导致的。 下面是 输入a b,输出a+b的值得...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,340,157
精华内容 536,062
关键字:

一个算法必须有输入