c语言设计遗传算法旅行商,用遗传算法求解旅行商问题(JavaScript版)

旅行商问题(Travelling Salesman Problem,即 TSP 问题)是一个经典的算法优化问题,它的描述是:一位旅行商人需要辗转若干个城市卖东西,每个城市只去一次,最终需要回到出发的城市,问如何规划路线,使得总旅程最短?

科幻小说《三体》的第二部里,三体人的水滴在太空中攻击地球舰队那一段,就有类似问题的描写:

太空中的无情杀戮在继续,随着舰群间距的拉大,水滴迅速加速,很快把自己的速度增加了一倍,达到60公里/秒。在不间断的攻击中,水滴显示了它冷酷而精确的智慧。在一定的区域内,它完美地解决了邮差问题,攻击路线几乎不重复。在目标位置不断移动的情况下做到这一点,需要全方位的精确测量和复杂的计算,而这些,水滴都在高速运动中不动声色地完成了。但有时,它也会从一个区域专心致志的屠杀中突然离开,奔向舰群的边缘,迅速消灭已经脱离总舰群的一些战舰,在这样做的同时,会把舰群向这个方向逃离的趋势遏止住。……

大刘在故事中写的是邮差问题,与旅行商问题略有差异,前者有若干固定的路线(某些点之间可能没有路线连通),后者则所有点之间都可直接连接,因此这儿说水滴在一定区域内要求解的是旅行商问题其实更合适。

旅行商问题是一个 NP 问题,目前还没有完美的解法,不过已经有很多高效的处理方法,比如动态规划、遗传算法等。本文介绍的是使用遗传算法来求解,并给出一个 JavaScript 版本的示例。(注:也可参见笔者之前写的 Python 版本。)

遗传算法

遗传算法是一种通过模拟生物进化来解决特定问题的方法,它的核心思想是将问题编码为“基因”,然后生成若干个基因不同的个体,让这些个体相互竞争,并使用一种评估机制让它们“优胜劣汰”,最终“进化”出足够优秀的解。

具体而言,它有两个核心要点:第一是编码,即需要找到一种合适的方法,将待解决的问题映射为“基因”编码,生成多个个体;第二是评估,即需要一种定量的方法给每个个体打分,评估哪个个体所对应的方案更接近最佳答案,从而可以根据这个分数优胜劣汰。

遗传算法只是一种思想,并没有很具体的方法,比如编码和评估方法,都需要具体问题具体分析。它的大致流程如下图所示:

f8425d34df7c410a6aa74fb5ed610136.png

编码

为了方便分析,我们可以将所有城市编号。旅行商从一个城市出发,每个城市只走一次,且没有城市被遗漏,那么旅行商的轨迹实际上就可以简化为城市编号的一个排列,这个排列可以用数列(数组)来表示。

举例来说,如果一共有 50 个城市,我们给城市从 1 开始编号,可以得到这样的数列:

1, 2, 3, 4, 5, 6, ..., 47, 48, 49, 50

这个数列顺序可以随意打乱,每一种排列都对应一种走法。比如:

12, 50, 30, 26, 13, 45, ..., 39, 18, 41, 17

这个序列表示旅行商从 12 号城市出发,先到 50 号城市,再到 30 号城市,再到 26 号城市,再到 13 号城市,再到 45 号城市,……,再到 39 号城市,再到 18 号城市,再到 41 号城市,最后到 17 号城市,当然,最后的最后还要回到起点城市 12 号城市。

由于是随机生成的,这个路线在图上看起来可能一团糟:

493ce2baa0aaefba4afac1602fa74883.png

这样的序列便是一种编码方法,每一种序列我们称之为一个“基因”。一个基因对应的数组类似下图这样:

1e77d032f990675c62b395e58a0e1e6e.png

计算开始时,我们可以随机生成 N 个序列,然后进行下一步评估。本例中,城市数量为 50 个,对应的基因个体数为 100 个。

评估

旅行商问题的评估目标非常明确:总路程最短。

为了处理方便,我们通常希望每个个体的分数是越大越好,既然总路程是越小越好,那么评估函数的实现便有一个很简单直接的方案,——取总路程的倒数。在我们这个例子中,这个简单的方案是有效的,不过有些复杂场景下,可能会需要更复杂的评估函数。

rate (gene) {

return 1 / this.getDistance(gene)

}

有了评估函数,我们便能给所有“基因”打分,并基于这个打分产生下一代。

下一代的数量通常与前代相同。生成规则一般是:

前代最佳的基因直接进入下一代;

从前代选中两个基因,选中概率正比于得分,将两个基因随机交叉、变异,生成下一代;

重复第 2 步,直到下一代的个体数达到要求。

交叉

遗传算法模仿的是生物的进化,每次产生下一代时,总是两个前代交换基因(交叉),生成一个后代。交叉的过程如下图所示:

01b433253eff150cf35a41906225e4dd.png

1、输入两个父代

2cf5fbfaca68feab35ad98ba60974a9e.png

2、随机选择父代的基因片断

a330353e833a4dbac14bc08b91f1a097.png

3、交换基因片断

这样便能得到两个新的子代,我们只需随机返回一个子代即可。

需要注意的是,在旅行商问题中,城市不能重复也不能遗漏,上面的方法产生的子代显然不能满足这个要求。解决方法也很简单,我们直接将另一个父代的基因连接在子代后面,再对数组去重即可:

xFunc (lf1, lf2) {

let p1 = Math.floor(Math.random() * (this.n - 2)) + 1

let p2 = Math.floor(Math.random() * (this.n - p1)) + p1

let piece = lf2.gene.slice(p1, p2)

let new_gene = lf1.gene.slice(0, p1)

piece.concat(lf2.gene).map(i => {

if (!new_gene.includes(i)) {

new_gene.push(i)

}

})

return new_gene

}

变异

遗传算法本质上是一种搜索算法,为了避免陷入局部最优解,我们要以一定概率让基因发生变异。如同生物界,基因突变大多数情况下都是不好的,但如果没有基因突变,仅靠现有个体交换基因,会很难产生新物种,整个种群有可能一直在低水平的局部最优解徘徊。

由于我们的基因是一个数组,常见的基本变异可以有交换、移动、倒序等几种。

交换

交换指的是在一段基因中,随机选取两个片断,然后交换它们的位置。如下图所示:

bbbccbaf9fc6a9d7d684a40cabc48004.png

倒序

在基因中随机选择一个片断,将这个片断的顺序颠倒。如下图所示:

16666f389e6ec31a6a5febeaea13b2f7.png

移动

在基因中随机选择一个片断,将它移到另一个位置。如下图所示:

a22bb4b03837469061d64e23c4f3935c.png

这三种变异对应的代码形如:

let funcs = [

(g, p1, p2) => {

// 交换

let t = g[p1]

g[p1] = g[p2]

g[p2] = t

}, (g, p1, p2) => {

// 倒序

let t = g.slice(p1, p2).reverse()

g.splice(p1, p2 - p1, ...t)

}, (g, p1, p2) => {

// 移动

let t = g.splice(p1, p2 - p1)

g.splice(Math.floor(Math.random() * g.length), 0, ...t)

}

]

实践

有了上面的基础,就可以进行编码实践了。本文所有代码在 GitHub 上开源。

首先随机生成 N 个城市,这儿我们取 N = 50,每个城市包含 x、y 两个坐标。将它们用红圈在画布上画出来,再随机生成一条路线,图片看起来类似下图:

11f915b59908ddb9e940e60527327c29.png

然后开始迭代,并将每一代得分最高的路线绘制到图上。下面是第 710 代的样子:

51506d8be650f9741715798a774c9f39.png

可以看到,已经比初代好多了,但仍然有几个地方明显有优化空间。继续进化迭代,大约到第 3000 代时,得到下面的图:

498656c2d2c151e1c2ca6e29b2d508d7.png

再往后又继续迭代了几千次,但路线没有再变化。看起来,这便是当前能找到的最优解了。

下面是另一次试验的动图:

92ed7d813cffcb17d0a753996ab3c941.gif

你可以访问以下链接在线体验:

小结

本文介绍了遗传算法的主要思想以及一个求解旅行商问题的实例。

遗传算法的应用非常广,很多常规方法无从下手的工程优化问题,用遗传算法可以优雅地求解。不过遗传算法也不是万能药,需要注意的是,遗传算法并不能保证找到全局最优解,如果编码设计得不好,或参数不合适,它很有可能陷入某个局部最优解。

除此之外,遗传算法的应用主要有两个限制:

问题必须可编码为“基因”;

有合适的评估函数。

有很多问题看似简单,但想将解法转为基因编码却非常困难,只有先将解法编码为基因,才可进行下一步的交叉、变异等操作。

对大部分问题来说,编码方法都是存在的,只是有一些不那么容易想到,但是否有合适的评估函数就是一个实在的难题了,因为这个函数要满足稳定(相同的基因得到相同的分数)且高效,以便能进行自动化迭代。

几年前,我们曾探索过使用遗传算法来生成漂亮的广告图片的方案,图片中所包含的各种元素很多,编码挺复杂,但本质上并不难,最后难倒我们的是找不到合适的评估函数,——什么样的结果是“漂亮”的?无法定义评估函数,整个迭代便无法自动化。我们也试过半自动化,即基因的生成、交叉、变异等过程自动化,然后人工给每一代的结果打分。但后来发现这个做法也行不通,因为首先,通常需要上千次迭代才能得到可用的结果,对人来说,这个时间成本太高了,第二也是更重要的是,人其实并不能很好并且稳定地识别什么是“漂亮”的,——有时觉得这样组合很漂亮,但过一会儿再看到可能又觉得一般了。打分的不稳定,导致进化过程的不稳定,很难收敛到最优解。如果你需要处理类似这样的问题,目前更好的方案大概是深度学习。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值