可达格子

可达格子

> 编程

这个算法的目的,是找出一根韭葱从给定的格子出发、带着给定数量的 MP 能够到达的所有格子。

具体来说,就是写一个函数:它接收一个格子和一个数字作为参数,返回一个格子数组。

以后要是有人跟你提起“getAccessibleCells”或“getReachableCells”之类名字的函数,你就知道说的是什么了。

用途

只要你开始想提前规划自己的移动,这个函数就必不可少。

知道自己能去哪些格子,就可以挑出受到的潜在伤害最小的格子,或者看看移动到哪里才能打到对手。

这个算法最典型的用武之地之一,就是捉迷藏算法

最初的几种方法

原理

其实,实现一种获取可达格子的办法并不难。最简单的做法,就是用 getPathLength 测一遍自己到地图上每个格子的距离。

但这个函数的操作数开销很高。对地图上的每一个格子都调用一次……你的操作数消耗会直接飙上天。

稍微聪明一点的做法,是先找出 MP 范围内的格子,再用 getPathLength 确认它们确实走得到。

这两种方案都行得通,但它们的操作数开销很快就会变得很高。

如果你是新手,而下文要讲的方案在你看来还有点复杂,那么一开始先用这两种做法也完全没问题。 刚开始玩的时候,你要做的计算不一定很多;而且等级低的时候,你的 MP 也不多。所以这些做法你完全用得起。

问题所在

用这些方法拿到自己的可达格子,毫无压力。

但当你的 MP 变多时,第一个问题就来了。MP 越高,要测试的格子当然就越多。最坏的情况下,如果你拿 getPathLength 把整张地图扫个遍,最后大概会收到一张大约 400 万操作数的账单——“才”这么点而已。

也就是说,留给连招、伤害地图这类其他算法的操作数,就所剩无几了。

而这还没完!还有另一个问题:战斗里可不止你一个。 你多半还需要算出对手的可达格子,才能防住他们的攻击。 要是场上的韭葱们再召唤出一堆球茎,那要算的格子就更多了。

总之,如果你打算用这些方法,把这一大帮家伙的可达格子全都算一遍,那开销可就要让你倾家荡产了。

所以我们会改用另一种高效得多的方法:从一个格子走到它的邻居,再走到邻居的邻居!

邻居法

前置知识

首先,什么叫“邻居”? 一个格子的邻居,就是与它相邻的那些格子。 当然,这里说的是与它有公共边、直接挨在旁边的格子,而不是斜对角的格子。

因此,位于地图四角的格子只有一个邻居,位于边线上的格子有两个,其余的都有四个。

所以你首先要学会的,就是取得一个格子的邻居。 用 getCellXgetCellYgetCellFromXY 这几个函数,你可以轻松地找出这些邻居。

能取得一个格子的邻居之后,你还要判断这个格子是否“可通行”。 说白了,如果格子上是障碍物,或者站着一根韭葱,你的韭葱就没法移动到上面去。

要判断这一点,你会用到 isObstacleisEmptyCellisEntitygetCellContent 这些函数。

原理

原理其实很简单,尽管对编程新手来说,真要写出来不一定那么容易。

你的韭葱站在某个格子上,手上有一定数量的 MP 可以用来移动。 它当前所在的格子,距离是 0 MP;到这里为止,一切都很顺利……

如果你的韭葱有 1 MP,它能移动到哪里?答案是:出发格子周围空着的邻居。

那如果它有 2 MP 呢?花 1 MP,它能走到出发格子空着的邻居上;而花 2 MP,它还能走到这些空邻居的空邻居上。

以此类推……

绿色是出发格子,红色是正在被探索的格子。

像这样从邻居走到邻居,就能得到所有可以移动到的格子;而且还附带一个好处:每个格子需要多少 MP,你也一并知道了。

那什么时候该停下来?当然是没有 MP 的时候。想知道这一点,倒着数就行。

比如说,一根韭葱有 3 MP:原地不动,它还剩 3 MP;移动一格,还剩 2 MP,以此类推……

绿色是出发格子,红色是正在被探索的格子。

就这样!数到零就停,得到的正好是用我们的 MP 能到达的格子。

最后是障碍物的处理,如果还没做的话,要做的只有一件事。 处理障碍物很简单:跳过那些是障碍物、或者被韭葱占着的格子就行。当然,还有地图的边界。

你最终得到的算法,运行起来应该是这样的:

绿色是出发格子,红色是正在被探索的格子。

如果你得到的是这种结果,那么恭喜你,你已经成功写出了一个漂亮的可达格子算法!

操作数爆炸

注意,这种方法并不一定比用 getPathLength 的那种更省。 要是处理得不好,它反而可能贵得多。

因为当你从邻居到邻居地探索地图时,必须小心别往回走。你要做的是把可达格子的范围不断向外扩展,而不是反过来再把它们重新走一遍。

如果你没防着这一手,像图中那样把一些格子反复走上好几遍,那么随着 MP 变多,操作数开销就可能爆炸。

红色是已经走过的格子,橙色是每次迭代中被分析的格子。

左边是本该发生的情况,右边是你“往回走”时可能发生的情况。

各种改进

下面是几种改进:有的能减少算法消耗的操作数,有的能让结果用起来更方便。

使用映射(关联数组)

前置知识

想用上这个技巧,你需要了解映射的原理,并且会用它。

原理

与其照常把可达格子、或者正在测试的格子存进数组,不如把它们用作映射表的键:这样一来,检查某个格子是否可达、或者是否已经测试过,就不必调用 inArray 函数,只要这么写:

if(mapContainsKey(table, cellule)) { //这个格子已经测试过 } else { //这个格子还没有测试过 }

特点

既然格子就是这张映射表的键,你就可以为每个格子存一条信息,比如走到这个格子所需的 MP 数量。

使用缓存

前置知识

知道有全局变量这回事,会把数组套进数组里,最好再了解一下 getTurn 函数。

原理

对某个格子来说,它周围不是障碍物的邻居(先不算韭葱)在战斗中一般变化不大。因此没必要每次调用函数时都重新算一遍:只要在第 1 回合把它们存进一个全局变量,做成一个巨大的数组,为每个格子保存一个小数组,里面装着它所有不是障碍物的邻居。 这样,对某个格子,只要遍历这个小数组,逐一检查这些格子有没有被韭葱占着就行了。 你也可以在每个回合开始时修改这个全局变量,去掉被韭葱占住的邻居(并把上一回合被韭葱占住的邻居放回来)。

使用二进制

前置知识

这种方法基于一种利用二进制性质和按位运算符的地图表示法:一个整数由 64 个二进制位组成,其中一部分位可以用来存储某个格子的信息。

原理

地图被拆成 35 行,每一行对应一个整数。这个整数的第 i 个二进制位,就用来存储该行第 i 个格子的信息。

!二进制可达格子

深蓝色的格子属于第 1 行,亮黄色的格子属于第 8 行,红色的格子是第 19 行的第 3 个格子

接着要按这个思路建两个数组:

然后初始化算法:在第二个数组里,把出发格子对应的位置成 1。

接下来,只要用几个按位运算符,就能找出所有与已可达格子相邻的格子:

//obs 指第一个数组,表示一个格子是否为空 //acc 指第二个数组,表示一个格子是否可达

//情况 1,N 为偶数: acc[N] |= (acc[N-1] | acc[N+1] | (acc[N-1] >> 1) | (acc[N+1] >> 1)) & obs[N] //情况 2,N 为奇数: acc[N] |= (acc[N-1] | acc[N+1] | (acc[N-1] 注意:自从整数用 64 位而不是 32 位表示以来,除了这一种,你还可以用别的方式来表示地图。哪种最顺手,就由你自己挑了!

注意 2:(有难度)自从有了 BigInteger,你可以只用一个变量来表示整张地图,再用掩码取出偶数行或奇数行的格子。小心边界!

特点

这种方法用的是按位运算符,它们的特点是每个只消耗 1 次操作。因此,它在操作数上比其他方法便宜得多。 不过它也不完美:比如说,要知道走到某个格子需要多少 MP,就比较困难。

基准测试

欢迎去看看 Leek Wars 论坛上的这个主题

你可以把自己算法的结果拿去和其他玩家比一比。

在那里你还能找到很多思路,用来优化你代码的操作数开销。