算法源码地址:
https://github.com/wudashan/longest-path-problem
前言
最近产品线举办了一个软件编程大赛,题目非常的有趣,就是在一个9 × 9的格子里,你要和另一个敌人PK,在PK的过程中,你可以吃格子里的果实来提升攻击力。每次可以往正上、正下、正左、正右、左上、左下、右上、右下八个方向走。每次要么连续吃果实要么连续走空白区域,且不能走重复的位置。初始状态如下图所示:
为了提升攻击力,我们需要尽可能地一次吃最多的果实,所以路线可以这样规划:
至此,我们可以对这个问题进行描述:已知空白区域不能走,每次可以往正上、正下、正左、正右、左上、左下、右上、右下八个方向走,走过的位置不能再走,求能吃最多果实的路线(最长路径问题)?
前置条件
地图表示
首先我们将上面的地图使用布尔类型的二维数组表示,其中true表示可以行走的格子,false表示不能行走的格子:
1 | 复制代码boolean[][] simpleMap = new boolean[][] { |
格子表示
对于地图上的每一个格子,我们用一个简单类来表示:
1 | 复制代码public class Pos { |
由于我们是使用横纵坐标而不是几行几列来表示一个格子(没错,我就是这么傲娇),那么我们就需要给地图定义横纵坐标方向。方向如下图所示:
那么起点上方的果实坐标就是[3, 2](横坐标为3,纵坐标为2),但是对应着二维数组为map[2][3](第二行,第三列),即横坐标对应着二维数组的列,纵坐标对应着二维数组的行。
移动表示
为了程序简洁,我们给八个方向的移动定义对应的偏移量,这样每次行走只要对偏移量数组进行for循环就可以了。
1 | 复制代码Pos[] moveOffset = new Pos[] { |
深度优先搜索算法
算法思想
拿到这道题,脑袋里第一个想到的就是深度优先搜索算法,其思想为每次往八个方向递归,当不能继续走下去的时候保存最长路径,并回退到能继续行走的格子,继续递归直到结束。
代码示例
接下来就是我们的深度优先搜索代码:
1 | 复制代码/** |
接下来,就让我们在主函数里验证一下结果吧!
1 | 复制代码public static void main(String[] args) { |
执行Main函数之后,控制台将输出[Pos{x=3, y=2}, Pos{x=2, y=3}, Pos{x=2, y=4}, Pos{x=2, y=5}, Pos{x=3, y=6}, Pos{x=4, y=7}, Pos{x=5, y=6}, Pos{x=5, y=7}]
,即行走的最长路径。
虽然深度优先搜索算法可以计算出最长路径,但是它的时间复杂度却高得惊人!已知每次可以向8个方向移动,最多可以走m × n步(地图的长和宽),那么时间复杂度就是 O(8mn)。由于我们上面的地图可以走的选择比较单一,所以在我的电脑上1ms
就可以算出结果。感兴趣的童鞋可以试试下面这个地图在你们的电脑上需要多久出结果:
1 | 复制代码boolean[][] complexMap = new boolean[][] { |
至少,在我的电脑上需要5471ms
才能得出结果,非常的夸张!由于产品线比赛要求每次计算时间不能超过1000ms
,所以使用该算法基本不可行。那么是否有时间更快的算法呢?别走开,答案就在下面。
贪心算法
算法思想
贪心算法采用的是这样一种思想:每次都走出路最少的格子,这样后面可以选择的余地就比较大,最优解的概率也就大的多。
代码示例
下面便是贪心算法的代码:
1 | 复制代码/** |
写好算法之后,再验证一下结果!
1 | 复制代码public static void main(String[] args) { |
执行Main函数之后,控制台将输出[Pos{x=3, y=2}, Pos{x=2, y=3}, Pos{x=2, y=4}, Pos{x=2, y=5}, Pos{x=3, y=6}, Pos{x=4, y=7}, Pos{x=5, y=7}, Pos{x=5, y=6}]
,路径长度与深度优先搜索算法一致,即也能找到最长路径。
那么在复杂一点的地图上,与深度优先搜索相比,贪心算法的结果怎么样呢?在我的机器上,计算结果如下:
simpleMap | complexMap | |
---|---|---|
深度优先搜索算法 | 最长路径为8步,计算时间为1ms | 最长路径为33步,计算时间为5254ms |
贪心算法 | 最长路径为8步,计算时间为1ms | 最长路径为4/9/20步,计算时间为1ms |
从结果上可以发现,由于贪心算法并没有遍历所有路径,而是每次都往出路最少的格子走,所以时间上快很多,但是其结果却非常地不稳定,这是因为贪心算法容易陷入局部最优解的情况!
显然如果用贪心算法来寻路吃果实,那么能不能打败敌人就要靠运气了。怎么办?是否有折中一点的算法,既耗费可接受的时间,又可以计算出较好的结果呢?答案还是肯定的,接下来就介绍更高端的算法——模拟退火算法。
模拟退火算法
算法思想
模拟退火算法的灵感是来自物理学里的固体退火原理:将固体加热时,固体内部粒子随温度上升变为无序状态,内能不断增大;当慢慢冷却时内部粒子逐渐有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小。
用计算机语言来描述的话就是:在函数不断迭代的过程中,以一定的概率来接受一个比当前解要差的新解,因此有可能会跳出这个局部最优解,从而达到全局最优解。
代码示例
由于是高端算法,所以代码会比较多,但据说能看完模拟退火算法代码的人智商都超过180!
1 | 复制代码/** |
测试代码我就不再列出了,最后让我们看一下这三种算法在两个地图上的执行结果:
simpleMap | complexMap | |
---|---|---|
深度优先搜索算法 | 最长路径为8步,计算时间为1ms | 最长路径为33步,计算时间为5254ms |
贪心算法 | 最长路径为8步,计算时间为1ms | 最长路径为4/9/20步,计算时间为1ms |
模拟退火算法 | 最长路径为8步,计算时间为147ms | 最长路径为30~33步,计算时间为212ms |
总结
求最长路径问题可以看成是哈密尔顿路径问题
,由于寻找哈密尔顿路径是一个典型的NPC问题,所以不能在多项式时间内得到最优解。感兴趣的小伙伴可以去了解一下相关的知识,我在参考阅读章节给出了几个相应的链接。
解决这类问题,我们可以通过深度优先搜索算法
得到最优解,但是时间复杂度是指数级的;也可以通过贪心算法
得到一个局部最优解,其时间复杂度是线性级的,但得到的解时好时坏;还可以通过模拟退火算法
得到一个近似解,这个时间复杂度也是线性级的,只要退火参数配置得当,其解是稳定地,且是一个趋向最优解的近似解。
参考阅读
本文转载自: 掘金