> 草稿
草稿,由 ''Hazurl'' 撰写中
这篇文章会教你解决各类算法问题的基本方法。它主要面向不知道从何下手的新手,但也不仅限于新手。第一部分讲解各种解题方法,也就是理论。接着我会以可达格子算法为例,把这些方法落到实处。之所以选这个算法,是因为它是最有用的算法之一,而且专门讲它的那一页并不适合新手:那一页更侧重于优化。说到优化,本文不会涉及优化,这方面已经有专门的文章了。
一般来说,算法问题指的是可以由计算机解决的问题。算法问题分为两类:
这些不必死记,了解一下就好。
这一步再明显不过,却有很多人会忘掉,而且不只是新手。一定要把题目彻底弄懂,有不清楚的地方就别犹豫,再读一遍。想象一下把题目讲给一个从没读过它的人听。你要能凭记忆复述题目,既不擅自推广也不擅自缩小范围,否则一开始就走偏了。先不要去想程序该怎么解。 要把输入和输出的数据讲清楚,它们是怎样构成的,等等。如果题目对输出没有限制,就设想一下它之后会被怎样使用,好让它用起来更简单。下面两个问题,你必须能(完美地!)回答:
找到算法实现的最好办法,就是先自己亲手解一遍。要解的例子不能纯随机地挑,而要选取各不相同的情况和边界情况。要找出解题在哪一步会变得棘手,然后针对这些地方设计极端的例子。 接下来要准备好测试算法的手段。简单地判断输出是否等于正确结果就够用了,否则也可以把结果简单地显示出来,手动检查。
这是最具体的一步。你要把问题变成解法。最好的做法是从最初的问题出发,一步步地细化,顺便一提,这个过程本身就是一个算法 😉。要把问题拆分成子问题,同时让控制结构显现出来:循环、条件结构……每一步都要重新定义你所有的问题。 举个例子最能说明这个思路:给数组排序。输入一个数组,返回一个排好序的数组(比如说按升序)。 怎样给数组排序?当然要修改它,但光这样说还不够,要一直修改它,直到它排好序为止。哦!这……不就是一个循环吗?于是第一次拆分就让一个循环显现了出来,连同它的循环体和条件。可是“修改数组”说得不够具体……还要考虑到,有循环就可能有死循环。一定要非常小心,确保条件在某个时刻会得出不同的结果,从而退出循环。因此,每次修改数组都要让它比之前更接近有序,这样数组总有一刻会排好序,循环也就会结束。给数组排序的方法多得数不清,最简单的一种是把最小的元素和第一个元素交换,然后对第二个元素做同样的事,更一般地说,就是对数组中位置为 i 的每个元素,把它和它右边最小的元素交换。下面是我们目前的进度:
函数 trier_tableau (tab) // tab 是待排序数组的变量 当 est_pas_trie(tab) 时循环 对 i 从 0 到 n - 1 循环 // n 是数组的大小 pos_elem_min = pos_elem_min_droite(tab, i) // i 右边最小元素在数组中的位置 inverser(tab, i, pos_elem_min) // 交换这两个元素 结束循环 结束循环 返回 tab 结束函数
还需要确定数组是否已经排好序(即循环条件),找出 i 右边最小元素的位置,最后交换数组中的这两个元素。要找出 i 右边最小的元素,需要从 i 开始遍历到数组末尾,取其中最小的那个:
[未完成]
测试算法很重要,先用前面手工解过的例子测试,再用更复杂的问题测试。不过你得有一套验证方法:可以通过计算来验证,也可以观察数据(借助 mark 或 debug)。如果算法达不到题目的要求,就要找准问题的根源,重新修改算法,同样可以借助调试函数。你也可以编写自己的基准测试(bench)来测试算法,不过它通常是为优化服务的。简单介绍一下:基准测试是一个函数,它会在大量样本数据上测试、验证你的函数,并统计其执行开销。例如:function bench (fonctionATester, message, entrer, retourPrevu);。
太好了,现在你已经全都学会了,该动手实践了。不如就拿可达格子算法来练手……我会简单讲一下这个问题,如果你想了解更多细节或优化思路,有一整页专门讲它。
我的输入是什么?
还记得那两个必须能回答的基本问题吗?这就是其中之一,而且它并不难。不过在此之前,我想先分享一个技巧。 不要把函数写得太专用,函数需要的信息应该全部通过参数传入,要避免副作用(到函数外部去获取信息)。无论如何都不要在函数里做类似 getNearestEnemy 这样的事。这一点适用于你所有的函数。 当然,我刚才说的并不是在任何情况下都绝对成立。比如,如果你写了一个 getDistanceFromNearestEnemy 函数,那么调用 getNearestEnemy 就是合理的,不过写一个更通用的函数会对你更有利:getDistanceFrom。凡事要把握好分寸。 好,回到我们的问题……我们要找出从某个格子出发、用一定数量的 MP 能到达的所有格子。参数的选择很重要:如果不设参数,就没法分别求出我们的韭葱和对手韭葱的可达格子。所以参数里至少要能选择韭葱,但这又带来了另一个问题:如果我们想预判移动之后的可达格子呢?这时参数就得用格子,但这样一来就没办法知道 MP 数量了,所以函数签名如下:
getReachableCells (cellFrom, MP)
本页将使用这个签名,你也完全可以换一种做法,比如求出到所有格子的距离,这样在 MP 增益之后就不必重新计算了。
我要得到什么?
我们要得到从某个格子出发、用一定数量的移动点能到达的格子列表。列表用数组来表示。最方便的是让数组按距离排好序,这样用 for 遍历时会先分析最近的格子,从而节省 MP。我们还能做得更好:利用(关联)数组的特性,可以让数组以格子为键、以 MP 数量为值,也就是说:tab[cell] 返回到达 cell 所需的 MP 数量。于是我们确定数组的形式如下:
[cellFrom: 0, cell1 : 1, ... , cellX : 2, ..., cellN : n] // 更简单地说就是 [cell : MP],按 MP 升序排列 这是一个从 cellFrom 出发、拥有 n MP 时的数组。 这样一来,如果用 for (var cell : var mp in getReachableCells (cellLambda, MPLambda) ) 遍历数组,cell 和 mp 这两个变量会先取 cellLambda 和 0,然后依次取所有距离为 1 的格子,再是距离为 2 的,以此类推,直到 MPLambda。
现在我们来拆分问题,以便更轻松地把算法翻译成 LeekScript。 最初的问题是:“找出从某个格子出发、用一定数量的 MP 能到达的所有格子” 由于我们希望结果按特定顺序排列(按距离升序),而排序的开销相对较大,最好的办法是直接按顺序构建数组。所以要先加入距离为 0 的格子,然后是 1、2……直到 n MP。第一个格子我们已经有了,因为它是作为参数传入的。接下来要找出距离为 1 的格子(除非距离是 0 MP),该用什么方法得到它们呢?getCellDistance 不考虑障碍物,getPathLength 开销很大……太大了,而且别忘了,这个算法存在的意义就是避免每次都调用 getPathLength。我们从更全局的角度想一想:找到距离为 1 的可达格子之后,还得找距离为 2 的可达格子,以此类推。 总结一下,当我们要找距离为 x MP 的格子时,已知的有:
起点格子:cellFrom 距离为 x - 1 MP 的格子列表
最简单的办法不是从起点格子出发去找,而是从上一轮的格子列表出发去找。因为要考虑障碍物,从 cellFrom 出发的话一切都得重新计算。要从上一轮的格子得到新的格子,就要找出这些格子的“邻居”,然后逐一分析,避免加入障碍物或已经确定过的格子。
Impossible de charger les données du jeu.
Vérifiez votre connexion et réessayez.