计算机科学侦探手册-全-
计算机科学侦探手册(全)
原文:
zh.annas-archive.org/md5/5c4f1965b44eaa0c39cb6a563ce3ee61译者:飞龙
第一章:—1—
搜索问题
门没有敲响便打开了——只有门轴的吱呀声宣告了访客的到来。弗兰克立刻朝他的弩箭走去,但又停了下来。如果是维内特家族来找他,那他们肯定会敲门——带着一把斧头。门外的人应该是想跟他说话。弗兰克于是伸手拿起他的杯子,一口气喝掉了那杯已经冷却的咖啡。
“多诺万队长,”他在男人走进时说道。“什么风把您吹到这个好地方来了?我以为您不再来十五街以下了。”
“好久不见了,”队长简单地说道。“你最近怎么样,弗兰克?”
“壮观,”弗兰克干巴巴地回答,目光跟着队长绕房间慢慢走一圈。
多诺万环顾了一下弗兰克破旧的办公室。他的红色军官披风在他身后轻轻飘动。“私人侦探的生意怎么样?”
“这能支付账单,”弗兰克撒谎道。
队长点了点头。他停顿了一下,然后走到书架前,浏览着书架上的书籍。
“那这算是社交拜访吗?”弗兰克说道。“我是不是该问问玛琳娜和孩子们怎么样了?”

“他们都很好,”多诺万没有回头说道。“玛琳娜的乌龟美容生意最近做得不错。比尔去年加入了警队。维罗妮卡是个会计师,这几乎是我们最不可能的职业——”
“我其实并没有问,”弗兰克打断道。
队长耸了耸肩。他从书架上拿了一本书,翻阅着书页。弗兰克伸长脖子看了看封面——警察学院年鉴:第 21 届班级。
“你到底想要什么,队长?”弗兰克质问道。
最终,队长与弗兰克对视了。 “我需要你的帮助,弗兰克,”他说道。
弗兰克挺直了身子。自从弗兰克离开警队的五年里,队长只来看过他两次,而且都是警告他远离正在处理的案件。威胁几乎是弗兰克预料到的全部,但现在看起来队长有了特别的麻烦——或许是那种能让弗兰克欠租不还的麻烦。
“我现在不在警队了,”弗兰克轻松地说道。“你怎么不让你信任的侦探来做呢?”
“我需要一个不在警队中的人,”队长说道。“别装了,弗兰克。如果你不知道我在这里意味着什么,那你就不是我需要的人。”
弗兰克轻笑了一声。“泄密?在你们的队伍里?”
“更糟。昨晚有人闯入了警局的档案室,偷走了超过 500 卷卷宗。”
“他们到底想要什么?”弗兰克问道。他没有多想,便身体前倾,伸手拿起一卷新的羊皮纸和一只羽毛笔。这一动作对他来说就像喝咖啡或避开楼梯一样自然而然。
“我不知道,”多诺万说道。“没有规律可循。他们偷走了整整一排的文件,从财产纠纷到支出报告,应有尽有。他们拿走了我们所有关于刺客、名人、私人侦探、公证人的账本……他们甚至拿走了农场主斯温森的噪音投诉文件箱。但其他的书架却完全没有被动过。我们至少统计出有 512 份文件丢失。”
“也许是农夫斯温森的邻居干的,”弗兰克开玩笑道。“他们肯定听说过,只要投诉一百次,实习生就会来你家给你上一次严厉的课。”
唐诺万队长没有回应。他只是带着怜悯的目光盯着弗兰克,直到弗兰克清了清嗓子打破了沉默。“所以你是要我找到这些文件?”
队长摇了摇头。“我希望你能找到那些小偷。我们有文件的备份。我想知道他们需要什么信息,以及他们打算用这些信息做什么。”
“一个搜索问题,”弗兰克沉思道。在警队时,他的两项专长就是搜索问题和惹队长生气。
“国王知道吗?”弗兰克问道。
“我昨天向他简报过了,”队长带着一丝恼怒说道。“自从那位疯狂巫师的麻烦之后,国王坚持要对所有事情每天都进行简报。”两年前,一位名叫 Exponentious 的自大巫师曾试图摧毁整个王国。自那时以来,弗雷德里克国王亲自实施了大规模的安全升级,包括超过 300 条新的安全法规,其中至少有 5 条涉及政府大楼中十层以下的官方文件存储。
“不过我也不能怪他,”唐诺万咕哝道。“差一点就完了。如果不是安娜公主,谁知道现在王国会是什么样子。”
弗兰克默默点了点头。Exponentious 攻击了王国算法的基础,通过诅咒那些研究算法的学者。几个月内,他让连最简单的操作也变得低效,王国开始陷入停滞。损坏的证据随处可见;甚至在他的本地面包店,弗兰克亲眼目睹了顾客们发现自己竟然不记得如何排队时的恐慌。
“国王当然对这件事非常关心,”队长继续不耐烦地说道。“他想要了解所有的细节:谁负责这个案件?我们使用了哪些搜索算法?我们是否已经搜查了所有邻近的建筑物?”
弗兰克压抑住笑意,思索着这个提议。为首都的警察提供咨询服务是一笔不错的收入。他低头看了看自己的鞋子,看到鞋尖从鞋底的一个洞口露了出来。“如果我要做咨询,”他说,“我就按我自己的方式做。”
这是关键时刻。五年前,他因按自己的方式做事而被开除出警队。队长是一个讲究规则和秩序的人。弗兰克最后一次使用启发式方法正是压垮骆驼的最后一根稻草——当天下午,唐诺万队长就从他身上拿走了警徽。不过,话说回来,按自己的方式做事总是能让弗兰克得到结果。
“我就知道你会这么想,”队长终于回应道。他从自己的风衣下拉出一个薄文件夹,放到弗兰克的桌子上。
“我会联系你的,”唐诺万说。然后,他没有任何仪式地转身离开了办公室。
三个小时和十二杯咖啡后,弗兰克弯腰坐在桌前,翻阅着那本薄薄的信息文件夹,这是他第七次这么做。字词在忽明忽暗的烛光中跳动晃动,但并没有提供任何新的见解。
可供参考的资料不多。队长只给了他一份失踪文件清单和相关夜班的值班表,但除此之外没有更多的信息。
最后,弗兰克叹了口气,夸张地抓起一张羊皮纸开始做笔记。
任何搜索问题的第一步是确定你希望找到的目标——正如他在《警察算法 101》课上的老教授所说的,叫做目标。弗兰克很早就学到了这个教训;他在刚成为警察的第一周就被指派去寻找公爵的奖马,结果那天下午他就带着一只 42 磅重的角龟回到了警局。显然,这只令人印象深刻的爬行动物并不符合要求。如果你在寻找错误的东西,即使是再好的搜索算法也没有意义。
在这个案例中,问题不是什么,而是谁。队长在这一点上说得对。既然小偷已经拿到了文件,警察是否能找回来已经不再重要。小偷已经得到了他们需要的所有信息。
所以他的目标很简单:找出偷走文件的人。
任何搜索问题的第二步是确定搜索空间。你在搜寻什么?在弗兰克每天寻找钥匙的过程中,搜索空间是办公室里的每一块平面。而当弗兰克想要找到一个罪犯时,他的搜索空间就是首都周围的每一个人。
弗兰克坐回椅子,揉了揉眼睛。这是一个大搜索问题,在一个犯罪分子遍布的城市中寻找特定的罪犯。但他见过更糟糕的情况。
既然他已经定义了问题,就可以开始设计算法了。线性搜索不适用;他无法承担在整个城市里询问每个人的成本。他也可以排除许多他在学院里学过的更复杂的算法。对于这样的问题,他必须回到自己基本的搜索算法工具包——私家侦探最信赖的朋友。
弗兰克在羊皮纸上做了个记号。他已经确定了目标,知道了搜索空间,也有了算法。现在是时候开始工作了。
警察算法 101:搜索问题
出自德雷克教授的讲座摘录
在本课程中,我们将讨论几种解决搜索问题的不同算法(以及相关的数据结构)。搜索问题被定义为需要在一组可能值(搜索空间)中找到一个特定值(或目标)的任何问题。
那些毕业后成为警察的同学们,每天都会面临属于这一类别的问题。搜索问题的广泛定义涵盖了许多不同的计算问题,从在警察日志中查找特定记录,到在藏匿处寻找房间,再到找到所有符合某些标准的逮捕记录。本课程不会详尽无遗——那将需要数年时间——但我会给你们一些基本而重要的算法示例。
本课程中描述的算法将具有三个共同的组成部分:
目标 你正在搜索的数据项。目标可以是一个特定的值,或者是表示搜索成功完成的标准。
搜索空间 用于测试目标的所有可能性的集合。例如,搜索空间可以是一个值的列表,或者图中的所有节点。搜索空间中的一个单一可能性称为状态。
搜索算法 一组用于执行搜索的具体步骤或指令。
一些搜索问题可能会有额外的要求或复杂性,我们将在介绍不同算法时涉及这些内容。
第二章:—2—
穷举搜索线人的信息
“高效算法的关键是信息。” 这是德雷克教授的座右铭,每次开始警察算法课时都会怒吼着对学员们说,声音足够响亮,牢牢印在弗兰克的记忆中。“一个好的算法依赖于在数据中找到结构并加以利用。它依赖于信息。”
弗兰克微笑着回想起这个场景,转入三点巷,这是一条坑坑洼洼的泥土路,沿路混杂着低级酒吧和高档咖啡店。他对一对经过的骑士点了点头,那对骑士穿着盔甲,叮当作响,迅速经过,弗兰克心里默默记下,离开之前一定要喝一杯三倍浓缩的浓缩咖啡。首先,他需要信息,一些能帮助他引导搜索的信息。他知道该从哪里开始。
玻璃盒子比利此刻应该已经在某个酒吧里,静静地坐着,听着飘散在房间里的零碎对话。人们并没有故意绕着比利说话,他们只是没注意到他在场。比利天生拥有一个显著的天赋:完全不引人注意。无论他做什么,总有一种魔力让人们根本察觉不到他。也许是他那苍白的皮肤,或者他瘦小的身材;也许是他异常平凡的穿着品味。无论是什么,比利早就决定将他的这一特长用于偷听、收集信息,并把这些信息卖给任何愿意购买的人。

弗兰克目光扫过三点巷里紧挨在一起的八家店铺,心里想着比利会选择哪一家。他在脑海里过了一遍六七种搜索算法,但这毫无意义。弗兰克没有任何信息可以依赖。比利可能在任何一个酒吧或咖啡店里。
他必须使用穷举法——简单地尝试所有可能性,直到找到比利。这让他感到不舒服。多年的侦探和私人调查经验告诉他,几乎总有比穷举法更好的算法,而他讨厌诉诸于如此低效的方式。
弗兰克嘟囔着开始了他的搜索。他走进了街上的第一家酒吧,绝对值酒吧。

酒吧老板,一个名叫艾布的暴躁男人,瞪着弗兰克进来,故意把手放在了被划伤的柜台下方。信息很明确:“我现在拿着武器,猜猜是什么样的。可是,如果你惹我,我会让你近距离看看。”
“我不想惹麻烦,艾布,”弗兰克举起双手说。“我只是来找比利的。”
“比利不在这儿,”酒吧老板说道。
弗兰克差点因松了一口气而微笑。“那我就走了,”他说。
艾布简短地点了点头,看着弗兰克离开,手依然放在柜台下。
弗兰克深吸了几口气,摇了摇头,感受着凉爽的空气。艾布的记仇能力可比他见过的任何人都要强。话说回来,弗兰克曾经逮捕过他的四个兄弟姐妹。
街上的下一个店铺是“无畏布尔值”,一家典型的布尔风格咖啡店——黑白分明。布尔城市的居民以对逻辑绝对概念的狂热奉献而闻名,他们将一切事物视为“真”或“假”。他们是很好的证人。作为镇上唯一的布尔风格咖啡馆,“无畏布尔值”成了外来者的避风港。毕竟,要么你是布尔派,要么你不是。

弗兰克探头进门,向大家问道:“比利在吗?”有片刻的沉默,二十双眼睛小心翼翼地扫视着咖啡馆的每一寸地方。布尔派的人在完全确认之前是不会回答问题的。
“不,”比利精确地回答道。
弗兰克继续他那详尽的搜索。

第三和第四家店铺同样没有收获,虽然明显更加宜人。恒常常量酒吧的酒保热情地迎接了弗兰克,并邀请他一起回忆过去的美好时光,这很奇怪,因为弗兰克只在上个月才见过他。而在“大胆双重”酒吧,那个臭名昭著的巫师聚集地,每个新来的人都会引起一阵欢呼,大家开心地围绕着蒸腾的咖啡杯唱歌。

弗兰克在第五家店,指数级浓缩咖啡店找到了比利。这是街上最嘈杂、最俗气的咖啡店,但因为使用了三倍浓缩的咖啡豆,它却吸引了最多的忠实顾客。在好日子里,每张桌子都会坐满那些神经过敏的人,他们似乎认为,畅聊的关键就是大声说话。

今天早上,指数级浓缩咖啡店接待了一个相对安静的客群。只有几张桌子被占据,而且大多数都是独自喝咖啡的人,他们自己低声喃喃自语,颤抖着。
比利坐在一张中央桌子旁,尴尬地靠向一旁的对话。似乎没有人注意到他。弗兰克在第一次扫视房间时甚至没有看到他。
“比利!”弗兰克叫道。
比利突然慌张地跳了起来。“弗兰克?”他露出笑容,高兴有人注意到他,然后坐回椅子。“拉把椅子坐吧。”
“我在找些信息,”弗兰克解释道,同时坐在比利对面的位置。
“可能我有点记得,”比利说。“最近我记性不好,”他说着,朝着一只可能不属于自己的、早已空了的杯子看了一眼。
弗兰克示意咖啡师,咖啡师很快就把一杯新鲜的咖啡放在桌上。“你记得警察局的盗窃案吗?”弗兰克问比利。
比利的眼睛睁大了,他吓了一跳。“你说是抢劫?”他不太相信地问道。他的眼睛四下扫视,但像往常一样,没有人注意到他。
弗兰克把两枚金币放在桌上,忽视了胃中的不快。他负担不起这种花费,特别是在不知道自己是在付钱买线索还是无聊的闲话时。但他知道这不会便宜。他凑近桌子,低声说道:“两天前,窃贼带走了一大堆文件。”
“听起来不像是健康记住的事情,”比利说。他盯着那些金币。“恐怕你找错人了,弗兰克。”
“那是黄金,”弗兰克咆哮道。
“抱歉,我帮不上忙,”比利说。他再次扫视了一遍房间,才接着说道,“即使我知道一些关于抢劫的事情,那也是我想要忘掉的事情。即使我知道一些小事,比如谁可能帮忙做后勤,那也不值得冒着被人发现自己鞋子里塞满牦牛粪的风险。”
弗兰克盯着他,但比利已经沉默了。对于一个靠分享信息为生的人,比利有个奇怪的习惯,就是不说话。“牦牛粪?”弗兰克问道。
比利点了点头,但没有再说什么。
“你能更具体一点吗?”弗兰克问。“我们说的是北方还是南方的牦牛?”
“这重要吗?”比利问。“关键是,如果我知道是谁安排了交通,我也不会记得。特别是如果那些人恰好有一个位于城外五英里处的大农场,在那里他们能轻易让人消失。而且如果那个农场的家族有违法历史和不健康的幽默感,那就更不值得记住了。绝对不值得记住。”
“可惜,”弗兰克笑着说道。“那下次吧。”他朝金币点了点头。“作为未来记得事情的激励。”
说完,弗兰克站起身,走出了指数快餐店。他转左,继续沿街走。一旦离开三分街,他就能绕过去,前往克拉诺克的农场——唯一一个和比利描述稍微匹配的农场。
当他经过故障登记处时,他注意到一个阴影迅速进入了附近的巷子。他低声
但当他离开城市,走上通往克拉诺克农场的泥土小道时,他发现自己心情不错。比利没给他很多信息,但即便是一点点信息,也可能意味着高效搜索算法和穷举搜索之间的差别。
警察算法 101:穷举搜索
德雷克教授讲座摘录
穷举搜索算法会在整个搜索空间内搜索每一个可能性,以寻找目标值。最常见的穷举搜索是线性搜索,它会按顺序检查所有不同的可能性。
想象一下,当你追赶抢劫者进入一家废弃酒店的二楼走廊时会发生什么。走廊有 30 扇门,所有门都关着。如果你遵循正确的警察程序,你的搭档已经封锁了对面的楼梯,抢劫者被困在那一层。那么,你该怎么找到他呢?你是随机选择门,一次又一次地跑来跑去,直到幸运地找到他吗?不!你沿着走廊逐一踢开每一扇门。
或者考虑一个扫描数字列表(一个数组)的算法,用来查找目标值。该算法沿着列表从一个数字扫描到另一个数字,逐个检查每个值,以免遗漏任何一个,直到找到目标值为止。如果我们在数组中查找数字 5,那么搜索的过程如下:

线性搜索算法的优点在于它们实现简单,甚至可以在没有结构化数据的情况下工作。你不需要对抢劫者选择的房间做任何假设;你只需检查每一个房间。缺点是,当数据具有可以利用的结构时,穷举算法往往不是最有效的算法。如果你知道抢劫者去了哪里,通过利用这一信息,你就能避免踢开许多房门。
高效算法的关键是信息!
第三章:—3—
犯罪分子的农场上的数组与索引
弗兰克看到警察的马栓在克兰诺克家门外时,大声咒骂了一句。既然队长亲自雇用了弗兰克,他本来没想到会遇到任何警员。如果队长不信任他的警员,要么他们涉嫌某事——所以他会把他们调到市区另一头的案件——要么他们根本不够优秀。但从眼前的情况来看,显然有人已经介入了案件,而弗兰克已经落后了。
他穿过开着的前门,走进了大厅,和警员以及克兰诺克先生一起站在那儿。克兰诺克看了他一眼,表情中带着厌恶,但似乎并不意外看到他。那名警员则显得有些措手不及。
“你是谁?”她要求道,转身对他怒视,手里拿着羊皮纸和羽毛笔。
弗兰克忽视了她。“克兰诺克先生,”他说。“很高兴再次见到您。”
“来骚扰我们了吗?”克兰诺克问道。“你不受欢迎,弗兰克。”
“我不是来寻求欢迎的,”弗兰克回答道。“我来是找您的妻子。我有几个简单的问题要问她。”
那名警员盯着他看。“弗兰克?”她问。“弗兰克·运行?前侦探转行做私家侦探?你来这里干什么?是不是丢了什么宠物龙?”她讥讽地笑了。
弗兰克再次忽视了她。“克兰诺克先生,您的妻子在哪儿?我能在哪里找到她?”
那个老人摊开双手。“她什么都没做!她已经改过自新了,你知道的。真的,这次是真心的。”对于一个业余演员来说,他的演技还不错。
弗兰克微笑了一下;他知道这笑容让人不安。果然,克兰诺克皱起了眉头。
“我知道,克兰诺克先生。我来是为了挖掘她的专业知识。或者,我也可以干脆把对话留给……”
“诺塔申警官,”那名年轻的警员冷冷地说道。“而且这是我的调查。”
那是个谎言。警员通常是两人一组进行调查。更重要的是,弗兰克从队长给他的值班表上认出了诺塔申的名字。她在案发之夜就在警局。
“诺塔申警官,”弗兰克说道。“谁说我来是为了调查?也许我只是来找丢失的龙而已。”
她皱了皱眉。
从房子后面传来了骚动。有人喊着克兰诺克的名字,但被一声刺耳的长嘶声打断了。“我妻子在马厩里,”克兰诺克不耐烦地说道。“2 号棚。现在快走,滚出我的家!”克兰诺克朝前门挥了挥手,自己则从后门匆匆离开。
“谢谢,”弗兰克边走边喊道。“很高兴见到您,克兰诺克先生。”
诺塔申警官跟着弗兰克穿过院子。她走得很快,愤怒的脚步声敲打着地面。“你知道自己去哪吗?”她问道。
“2 号棚,”弗兰克回答道。
“我知道,”诺塔申愤怒地说道。“但是 2 号棚在哪儿?”
弗兰克停下脚步,转向她。“刚从警校毕业吗,诺塔申?”他问道。
“什么?”
“只有新手才会问这种搜索问题。你没有学过《警察程序》和《数据结构》吗?还是说这些课程已经被一些不那么严格的课程替代了——比如《海龟图形入门》?”
符号似乎有些愣住了。“当然我学过警务程序和数据结构,”她说,虽然听起来有些不确定。“但我的意思是——”
弗兰克打断她的话,“那你肯定知道数组和索引了。”
“是的,不过——”符号开始说道。
“在农场上找一个谷仓是一个足够简单的搜索任务,”弗兰克再次打断道。“我们可以用穷举法检查每个建筑物。对农场上的每个建筑物:检查它是否是#2 号谷仓。在我那时,你第一天就会学到这个搜索方法,警务算法课程上。”
“但我们可以做得更好。克拉诺克家族有六个谷仓排成一条整齐的线——就像一个巨大的数组。克拉诺克先生非常友好地提供了谷仓的编号,那个数组的索引。我们只需走到对应的谷仓。”

“那不是我的意思!”符号喊道,挥舞着双臂。“我知道怎么使用数组的索引。我知道我们只需要走到外面有一个巨大的#2 的谷仓。我在数据结构和警务算法课程中都以班级第一毕业,所以别来给我讲数组的正确用法。”
“好吧,你问了,”弗兰克回答。
“我问的是:你知道这些美妙的谷仓数组在哪吗?”
“当然是你,”弗兰克说。他开始再次走动。“不过你还是听起来像个菜鸟,拿班级排名说事。”
“谷仓在哪里?”警官喊道,跺脚追上来。
弗兰克回头朝她露出一个微笑。“就在这座山丘之后。”
就像弗兰克多年前学到的那样,克拉诺克家族几乎以狂热的方式拥抱了数组的概念。他们将一切都组织成线性结构,为每个元素标注了清晰的索引。当弗兰克经过#0 号谷仓时,他注意到有 15 个猪槽,每个槽可以存放一份食物。一名农场工人正在沿着一排谷仓循环,给每个数组位置舀入下一餐。
弗兰克和符号警官继续走向#2 号谷仓,门外挂着一个标牌。与之前的遭遇相比,克拉诺克太太冷冰冰的问候几乎显得有些亲切;她甚至什么都没扔出来……至少还没。
“你想要什么?”克拉诺克太太厉声问道。
“克拉诺克太太,”符号在弗兰克抢先问话之前打断了她。“我希望能问你几个问题。”

弗兰克让符号问问题。比利的线索没有提供更多的信息,只是指向了农场,但符号似乎有一套更好的线索。
克拉诺克太太冷笑了一声,吐了一口唾沫。“我什么都没做,”她说。“你知道,我已经改过自新了。”
“我不是来抓你的,”符号说。“我需要问你一些关于某辆驴车——数组车(ArrayCart)的事。”
弗兰克心中闪过一丝怀疑。符号警官会不会是来处理其他案件的?他有些怀疑,但直觉告诉他,她是在找失踪的文件,而他学会了相信自己的直觉。
“ArrayCart,”克兰诺克夫人怀疑地说道,语气中带着一丝自豪,“是我自己发明的。基于数组原理。它有专门的存储隔间,每个隔间只能容纳一只动物。由于每个隔间都有独立的门,你可以走到任何一个隔间,拿出动物或放入动物。轻松访问任何存储位置。省去了几个小时的麻烦。”

“这真是巧妙,”诺泰申警官承认道,“你们找到了一种将数组和索引的概念应用到牲畜运输中的方法。”
“而且这只是个开始,”克兰诺克夫人补充道,“我正在和一个巫师合作,开发一种全新的 ArrayCart——带有魔法指针!我敢打赌,它们对于警察局来说简直是完美的。告诉你们的队长,我能给他个好价格。”
弗兰克不得不佩服诺泰申。让克兰诺克开口最有效的方法就是提到数组。
“你现在有几辆 ArrayCart 出租,对吧?”诺泰申警官继续探问。
克兰诺克夫人的眼神瞬间变得冰冷。“这是合法生意。我们按时缴税。”
弗兰克忍住了轻蔑的嗤笑。
“你有没有在两天前租给别人一辆 ArrayCart?”诺泰申警官追问,“一辆小型号的,带六个隔间的。”
“我可能……”克兰诺克夫人说道,她冷冰冰的态度开始变得有些敌对。
“你有记录显示谁租了这辆车吗?”警官问。
“不,”克兰诺克夫人说道,“一旦马车归还,我们就会销毁记录。我不记得是谁租了那辆车。”
看起来比利的暗示确实有效。如果你是一个需要交通工具的罪犯,很少有地方会租给你一辆马车,而且更少的地方会在事后忘记你的名字。克兰诺克夫人可能声称已经改邪归正,但显然,她至少在某种程度上,仍然为她以前的同伙提供着有价值的服务。
“你真的不记得你的客户有什么信息吗?”诺泰申警官追问,但弗兰克知道这是徒劳的。他曾经问过她三小时关于一头被盗的牦牛的事。尽管是她自己被盗,克兰诺克夫人却一声不吭。克兰诺克夫人根本不会说话。
当诺泰申警官反复问了几遍类似的问题时,弗兰克悄悄溜出了马厩,找到了马车停放区。果然,停车场像一个数组一样被组织成 10 个标记清晰的车位。只有#2、#4 和#8 被占用。#2 和#4 的车位上分别有 10 个隔间,因此它们太大,不符合诺泰申的描述。但#8 车位上停着一辆六格的 ArrayCart,车轮上仍然覆盖着新鲜的泥巴。

弗兰克环顾四周,迅速跳上了六格 ArrayCart 的后部。地板上散落着一些稻草,但车厢里除此之外什么都没有。弗兰克逐一打开每个隔间,扫描空荡荡的储物空间,寻找任何线索。然后,他跪下来,翻动稻草,直到找到几片羊皮纸的碎片。
他一共收集了六小块碎片,可能是卷轴在卸货时被钉子勾住的角落。只有两块碎片上有文字,而且看起来是账本的内容。这并不算是一个可靠的线索,但它把这辆车与犯罪事件联系了起来。
弗兰克把搜索移到车厢前部,小心地检查驾驶座周围的所有物品。座椅本身给了他第一个重要线索。他在那儿发现了几根被座椅破损木板卡住的黑色和橙色线头。仅凭颜色的鲜艳程度,弗兰克就能判断出披风一定是新的。他满意地把线头收进口袋,然后从车厢上走下来。
只有当一阵新鲜空气迎面吹来时,他才注意到自己一直在屏住呼吸。车周围弥漫着腐烂鱼的臭味。他轻轻嗅了嗅,顺着味道走,来到泥巴覆盖的车轮旁。他深深地吸了一口气,立刻后悔了。泥土中的腐烂鳗鱼味道浓烈且让人不愉快,辨识度极高。
弗兰克一边勉强笑着,一边作呕着,从车厢旁踉踉跄跄地退开。他可能不知道是谁租了这辆车,但现在他知道它曾经在哪里。
警察算法 101:数组
德雷克教授讲座摘录
数组是简单的数据结构,允许你存储多个值。数组就像一排箱子,每个箱子可以存储一条信息,如一个数字或一个字符。

数组的结构意味着你可以通过指定其在数组中的位置,或索引,来访问数组中的任何值(或元素),无论是写入还是读取。许多编程语言使用从 0 开始的数组,这意味着数组的第一个值位于索引 0,第二个值位于索引 1,以此类推。通常,你会通过索引i来引用数组A中的值,表示为A[i];例如,数组A的第三个元素是A*[2],它的值为 19。
你可能从昨天在首都警察局的拘留室初步参观中就已经认识到这种结构。国王亲自建议使用有索引的单人拘留室,以简化囚犯的检索过程。每个警察局配备有四到八个拘留室,具体数量取决于当地的犯罪人口规模。
第四章:—4—
字符串与隐藏信息
Frank 摆脱了 Notation 警官,走出了农场的后门,那里有一块大牌子面朝着路。多年来,Crannock 家族一直用这块牌子发布关于各种非法活动的密码消息。如今,这里成了犯罪分子参观的景点——恶棍们带着他们年轻的学徒,聚在一起回忆那些总是以“在我那时候……”开头的故事。
这块标牌本身是一个 AnyText 模型。它包含 3 组字母,每组 12 个位置。每个字母、空格或标点符号都占用一个位置,这意味着整个标牌最多可以容纳 36 个字符——足够用来发布一系列非法活动的广告。每周一早晨,Crannock 家族的成员会拖着一篮子字母来到标牌前,逐个将合适的字符放入每个位置。
在刚加入警局的第一周,Frank 的搭档带他来到这里“查看标牌”。当时的信息是——苹果采摘者招聘。你有蜗牛吗?——对 Frank 来说听起来无害。Crannock 家族正在寻找一个苹果采摘者来帮助收获,并且提供处理蜗牛的服务。当他这样告诉他的搭档时,这位拥有 20 年经验的侦探笑了。

“那是他们想让你认为的,”侦探 Rossile 解释道。“你必须超越表面意思,看看犯罪分子会怎么解读。这个‘苹果采摘者招聘’意味着他们在试图雇佣一个小偷。一个会从车上偷苹果的人,或者类似的。”
“那蜗牛呢?”Frank 问道。
“非法蜗牛赛跑,”她回答道。“他们每几个月就在这里举办一次比赛。你会慢慢明白的。”
因此,Frank 学会了每周检查 Crannock 家族的标牌,以便了解犯罪世界的动态。几个月后,他已经学会了破译大部分的密码。农场工人指的是打手,如果需要力量、暴力或纯粹的人数,词语后会加上附加修饰。印刷艺术家是指伪造者,声乐艺术家是指骗子,等等。短语一群鸡让 Frank 困惑了好几天,直到 Rossile 将其翻译为“大量热腾腾的身体,四处奔跑发出噪音,制造干扰;不需要智慧”。
到了第一年结束时,Frank 已经成为了阅读标牌的专家。在过去的几年里,唯一一次 Frank 在破译标牌上的犯罪信息时感到困难,是在巫师 Exponentious 攻击王国的时候。Exponentious 施放了“错误索引法术”,使得整个王国的 ArrayDesignBoards 都出现了问题。顾名思义,这个法术改变了索引位置,因此 Mrs. Crannock 认为她设置字母的位置是错误的。在一周的时间里,Crannock 家族的标牌上充满了乱码。

由于魔法只会在数组中打乱字母,而 AnyText 模型是通过三个独立的数组实现的,弗兰克必须逐行解开每个数组。他解开了信息:需要防御法师。
然而今天,信息是明确的。事实上,这是他在克兰诺克家族的黑板上见过的最不含蓄的信息。内容是出租数组车。没有问题。

警察算法 101:字符串
德雷克教授讲座摘录
数组不仅仅用来存储数字列表;它们也可以用来存储文本字符的字符串。许多编程语言使用数组来实现字符串。数组中的每个块存储一个字符,可以是字母、数字、符号或空格。与其他数据类型的数组一样,这些字符串中的字符可以通过其在数组中的索引直接访问。

在你作为警察的职业生涯中,你将会非常熟悉这种文本表示方式。所有标准的警察表格都要求警员在每一页顶部的 32 格区域内记录他们的名字。在一个典型的月度里,你将填写超过 400 个这样的区域。
第五章:—5—
二分查找走私船
Usb 的港口不过是一个渔村。十几座风化的建筑聚集在一条长长的码头尽头。几个零星的活动点围绕着最新到达的船只,但总体来说,这个小镇安静得令人放心。
弗兰克直接走向“螃蟹之钳”,那是一个以蛤蜊浓汤和每周三晚的海洋民谣比赛闻名的渔民酒吧。运气好的话,他的一个联系人可能会在今天之内出现。毕竟,“螃蟹之钳”是 Usb 唯一的去处。所以,弗兰克坐到了酒吧后角的一张桌子旁,点了浓汤,开始等。
没过多久,一名自由走私者梅维斯走进了这个阴暗的小酒吧。天生小心的梅维斯虽然从未被正式定罪,但众所周知,她曾为了销毁证据而故意纵火烧毁了自己的船。弗兰克和她相处得还不错,至少在他离开警队之后,他们偶尔也会交换一些信息。
弗兰克已经喝了一个小时的浓汤,终于推开了碗,示意梅维斯过来。她在门口犹豫了一下,才挤进了酒吧。

“梅维斯,”弗兰克在她走到角落里时说,“你怎么样?”
“十分钟前我还好多了,”她吐槽道。
在弗兰克能开口之前,诺塔申警官大步走进了房间,举起了双手。“女士们,先生们,”她喊道。“能借我一点时间吗?我在找一辆两天前经过这里的货车。”
弗兰克在心里咒骂了一声。看来他的线索就到此为止了。
“我从黎明航班回来,本想着吃一碗热腾腾的蛤蜊浓汤,享受几分钟的安宁,”梅维斯抱怨道。“结果我遇到了这个警察,整天嚷嚷着驴车的事。”
弗兰克干笑了一声。“直到她走了,你就不能卸货,对吧?”
梅维斯皱了皱眉看着他,但没有反驳。Usb 无论在渔业还是航运业上都没找到什么成功。不过,这个港口确实吸引了那些不愿与麻烦的政府官员打交道的罪犯。弗兰克敢打赌,一个月的租金都能确保这里码头上的每一艘船都在走私某些东西。
“你知道这辆车的事吗?”弗兰克把声音压得几乎听不见。
梅维斯耸耸肩。“码头上总是有货车,弗兰克。这是个港口,人们搬运货物。”
“这是一辆特别的货车,”弗兰克逼问道。“一堆单独的动物栏,就像一个巨大的轮式阵列。”
“听起来很高级,”梅维斯说。“但我没听说过有什么船在搬运动物。我倒是听说过几只小型乌龟的木箱,但没听说有什么大到需要动物栏的。你确定是从这里经过的吗?”
弗兰克点了点头。那股味道就像是厕所里散发的鱼腥空气清新剂,而没有哪个地方的味道比 Usb 更糟糕了。
“那个时候有谁出港吗?”他问。如果小偷已经把被盗的文件运送到这么远的地方,他们应该不会再等下去。
“只有重试循环,”梅维斯说。“我告诉你这些只是因为它是公开的。我不知道它运载了什么,也不关心。”
“你知道它什么时候回来吗?”弗兰克问道。
“19 小时前刚刚返回港口,”梅维斯回答道。“那时我也不知道它运载了什么。”
弗兰克露出一个灿烂的笑容。“听起来是时候在城里转一圈了,”他说。
梅维斯心不在焉地对他微笑,并转身招手叫服务员。
弗兰克走了不到 20 米,警官诺泰申便走到他身边。
“Runtime 先生,这是我的调查,”她开始说道。“如果你有信息——”
弗兰克停下脚步,让她也不得不停下来。“诺泰申警官,您究竟在调查什么?”他问道。
结果比弗兰克预期的还要好。诺泰申几次张口又闭口,红色的潮热从脖子向上蔓延。
“船长不知道你在这里,对吧?”弗兰克问。“这可不算是一次正式的调查。”
“我不知道你在——”诺泰申警官开口说道,但弗兰克打断了她。
“别装了,”他说。“你一个人出来就是我需要的所有证据。你在用自己的时间独立进行这个调查。问题是,为什么?”
红潮已经完全爬上了诺泰申警官的脸颊。她的耳朵染上了特别鲜明的红色。
“那不关你的事。”她回答道。
“当船长来找我,因为他无法信任自己的官员时,这就成了我的事,”弗兰克平静地回答道。
“船长雇了像你这样的落魄侦探?”
“是的。因为他能信任我。”
警官诺泰申的脸色变得严峻,眼中燃烧着怒火。弗兰克一度以为她会用警棍结束这场对话。但几乎在她的怒火爆发的同时,它也迅速消退了。
“我必须找回那些文件,”她悲伤地说道。“是我的错——那天晚上我负责守卫。”
“我明白了,”弗兰克若有所思地说。
“我必须找回那些文件,”诺泰申警官焦虑地重复道。“我才加入警局几个月,而且——”
弗兰克打断了她,给了她一个他希望能让她安心的微笑。这正是他所预料的。新手在面对第一次错误时很难处理得好,而诺泰申看起来比大多数人都更紧张。“我们在找重试循环,”他说。“克兰诺克家的马车在抢劫的那晚在那里卸下了什么东西。那艘船 19 小时前入港。”
当然,弗兰克不信任她,但他想把她留在身边,保持对她的观察。她找到了克兰诺克家,这意味着她知道的比报告中说的要多。她的故事中有遗漏,他需要找出她还知道些什么。
“我们最好开始了,”符号看着码头,神色有些焦虑。“有很多船要检查。我们应该从前面开始吗?”由于港口的大多数船只都属于走私者,因此它们都没有显示身份标识。他们必须逐一询问每艘船的名字。
“我们可以做得更好,”弗兰克解释道。“港务局长对组织有着近乎狂热的追求。他坚持要求停靠的船只按到达时间排序。最新到达的船只停在离市区近的最佳位置,船员可以轻松上下货物,但每当有新船到达,其他船只都必须向后挪动,为新船腾出前面的空间。”
“这太荒谬了,”符号抗议道。“这简直是浪费精力。为什么他要这么做?”
弗兰克笑了。“他说这是为了效率,但任何在 Usb 待过一周的人都知道真相。港务局长受不了腐烂鱼的味道。停靠在港口的船只如果没有销售货物,会变得,嗯……有点‘芳香’。港务局长的组织方案就是把那些停留时间更长的船移到他小屋的远处。”
符号警官盯着他看。“你是认真的吗?”她终于问道。
弗兰克再次笑了。“是的。一旦你在巡逻一段时间后,你也会开始获得这些有用的信息。关键是我们知道船只是按顺序排好的,而重试循环已经在这里停了 19 小时,所以我们可以直接做二分查找。”
“我们的目标值是 19,我们的算法是二分查找。现在搜索空间就是这一整排船只,所以我们已经有了上限和下限。如果使用包含边界,我们的下限是第一艘船,上限是最后一艘船。如果重试循环就在这里,显然它不可能在第一艘船之前或最后一艘船之后。”
“所以我们从中间的船开始,问它停港了多长时间。如果它停得不到 19 小时,那它一定是在重试循环之前。这样就能将我们的搜索空间一分为二。然后——”
“如果它已经在那里超过 19 小时,那它一定是在重试循环之后,”符号打断了。“我知道二分查找。我刚刚参加完警察算法的期末考试,就在两个月前。”
说完,两人开始寻找重试循环。中间的那艘船是一艘黄色的双桅帆船,奇怪地散发着香蕉的气味,它已经在港口停了 17 小时。

这意味着他们可以排除前半部分的船只,包括那艘中间的船。弗兰克将下限调整为那艘可能是重试循环的船,只比黄色双桅帆船多一艘。

在缩小的搜索空间中,他们选择了一个新的中点。说服下一艘船的船长他们不是卧底海关官员花了一些时间。10 分钟后,诺塔申把她的徽章塞到船长鼻子下,他的语气立即变得愤怒而抱怨,告知他们他的船,腐败数据包,已经在港口停了 22 个小时。他要求他们代表他与港务局长交谈。

由于他们的目标是 19 小时,他们知道 重试循环 必须出现在 腐败数据包 之前。他们再次改变了边界,使得位于 腐败数据包 左边的船现在成为了上边界。

这只剩下两艘船在搜索范围内;他们迅速接近搜索的终点。如果这两艘船都不是 重试循环,他们可以确定它已经离开了港口,因为一旦搜索空间中没有更多的元素,他们就可以排除整个搜索空间。
由于只剩下两艘船,他们可以选择其中任何一艘作为新的中点。凭着直觉,弗兰克选择了较早的那艘船,这艘船恰好也是他们的下边界。与在码头徘徊的船员聊了几句后,他们确认这艘船就是 重试循环,并且已经在港口停了 19 个小时。

“接下来怎么办?”诺塔申警官问道,他们站着看着那艘船。
“我们再次用你的闪亮徽章,”弗兰克回答道。
警察算法 101:二分查找
德雷克教授讲座摘录
二分查找算法用于在已排序的数组 A 中高效地找到目标值 v。与线性扫描不同,二分查找利用数据结构的信息使搜索更加高效。高效算法的关键是信息。在这种情况下,我们利用数组按升序排序的事实:
A[i] ≤ A[j],对于任何一对索引 i 和 j,其中 i < j
这看起来可能信息不多,但足以让搜索更加高效。
二分查找算法通过反复将搜索空间一分为二,并将搜索限制在其中一个半部分来工作。该算法通过追踪两个边界来限制活动的搜索空间。上边界 (IndexHigh) 标记着数组中属于活动搜索空间的最大索引。下边界 (IndexLow) 标记着最小索引。在整个算法过程中,如果目标值在数组中,我们保证以下内容:
A[IndexLow] ≤ v ≤ A[IndexHigh]
在每一步搜索中,我们检查位于上下边界之间的中间值:

然后我们可以将这个中间位置的值 A[IndexMid] 与目标值 v 进行比较。如果中点小于目标值,A[IndexMid] < v,我们知道目标值一定位于中间索引之后。这时我们可以通过将 IndexLow = IndexMid + 1 再次将搜索范围缩小一半。
如果中间点大于目标值,A[IndexMid] > v,我们知道目标值一定位于中间索引之前,这样我们可以通过将 IndexHigh = IndexMid – 1 来将搜索范围缩小一半。
当然,如果我们发现 A[IndexMid] 等于 v,我们可以立即得出结论,搜索完成。我们找到了目标值。
让我们考虑在以下(已排序的)数组中搜索值 15。带有虚线框的框对应算法已检查的值,阴影部分是被排除的值。

第一次中点检查找到的值是 11,小于我们目标值 15。由于我们知道数组是按升序排列的,我们可以排除中点及其之前的所有值。我们适当移动下界索引 (IndexLow = IndexMid + 1)。

同样,在第二次比较后,我们找到的中点值是 52,大于目标值。我们可以排除中点及其之后的所有值。我们将上界索引 (IndexHigh = IndexMid – 1) 向前移动。

请注意,尽管下界的索引在多次迭代中指向了目标值 (v = 15),我们仍然继续搜索直到中点指向目标值。这是因为我们的搜索仅检查中点的值。在中点指向之前,我们不会检查下界或上界的值。
如果目标值不在数组中会发生什么?随着搜索的进行,边界会越来越接近,直到它们之间没有未探索的值。由于我们始终将一个边界移到中点索引的之后,当 IndexHigh < IndexLow 时,我们可以停止搜索。此时我们可以保证目标值不在数组中。
第六章:—6—
寻找线索的二分查找
“食品检查员,”弗兰克大声喊道,他和标注官一起走上了狭窄的跳板,登上了船。应弗兰克的要求,标注官挥动着徽章,动作太快,以至于没人能看清。
“食品检查员?”一个船员问道。“我们没有运送任何食品。”
弗兰克打量了一下那人。他不是军官或雇佣保安,可能只是一个在军官们离开时接管了指挥的水手。这并不罕见。走私者很少雇佣保安来看管他们的船,因为这样会引起太多注意。
弗兰克转身对着水手低吼道:“我们走着瞧。我听说这码头上有一批腐烂的鳗鱼,我打算找出来。”
“鳗鱼?”水手显然感到手足无措。
“腐烂的鳗鱼,”弗兰克反击道。“我们下去检查一下储藏室。”然后,他毫不等待回应,大步走向通往甲板下方的舱口。
标注赶紧跟了上去。
“我们没多少时间了,他们很快就会抓住船长。我们需要找到航海日志,”弗兰克一边说,一边爬下梯子。航海日志里会包含货物清单和访问过的港口名单。当然,货单会是假的。走私船从不记录真实货物。但如果运气好,他可以从谎言中找到线索。
标注官在货舱后部找到了航海日志并拿了出来。弗兰克查看了一下封面,忍不住骂了一声:
重试循环的清单和日志
船长:A. 詹姆斯
本港:Usb
所有者:维内特航运集团有限公司
几个月来,弗兰克成功地避开了维内特人,但他却走进了他们的船上。他本能地扫描着货舱,寻找隐藏的打手、武器化的农用设备或蛞蝓赛车的证据。弗兰克很快排除了最后一种可能——大家都知道蛞蝓不会在船上比赛,因为那和船身周围的盐水有关系。
他摇了摇头,集中精力解决眼前的问题。弗兰克必须在维内特人发现他在船上的时候找到线索,否则他可能再也无法离开了。他转向书的末尾,开始一页一页地翻回前面。

“你在做什么?”标注问道。
“在找最后一条记录,”弗兰克说道。
“一页一页地翻?”标注问道。“这得有一千页吧。为什么不再用一次二分查找?我们才刚用过。”
弗兰克停了下来。他并不在找特定的页码,但他仍然可以用二分查找来找到最后一条记录。他会根据当前页面是否有文字来调整查找范围。
“好的。二分查找,”他同意道。
他再次翻到最后一页,确认这本书共有 1000 页,这让他有了页面 1 的下限和页面 1000 的上限。他将数字相加,除以 2,计算出中点为 500 页。他翻到了那一页。

第 500 页和 501 页都是空白的,所以 Frank 知道最后一页的内容在 499 页或之前——这是他的新上界。再进行一次中点计算后,他翻到 250 页,仍然是空白的。

“看起来像是一本新书,”Notation 补充道,“幸好你没有从后面继续翻。”
Frank 没有回答。下界为 1,上界为 249,他计算出中点为 125。这个时候他发现有内容,于是他相应地将下界调整为 125。

“187,”Notation 在 Frank 还没算出中点之前就补充道。他翻到 187,再次发现有内容,并调整了下界。

“218,”Notation 说道。页面是空白的,于是 Frank 将上下界分别调整为 187 和 217。

“202,”Notation 在 Frank 还没完成上下界的计算时就说。
“你怎么这么快就做到了?”Frank 问。
“练习,”她回答,“我们以前在学院里每当需要休息时,就会举行二分查找比赛。我从未被打败过。”
Frank 摇了摇头,“听起来像是个疯狂的时刻,”他低声说。
第 202 页和 203 页已填满。“210,”Notation 补充道。

在第 210 页,他们终于找到了最后一条记录,详细描述了重试循环的最后一次航行。“现在怎么办?”Notation 问。

“我们要搜索一个有趣的包或者端口。在上一次航行中,他们做了大约 70 条记录。我们得扫描它们。”
“穷举搜索?”Notation 问道,“我们不能用更高效的方法吗?记录不是按照取货和交货时间排序的吗?”
“排序在这里帮不上忙,”Frank 回答道,“我们不知道时间。排序数据只有在按有用的维度排序时才有帮助。他们没考虑按可疑度排序。真是搞不懂。”
“哦,是‘天气记录问题’,”Notation 说道。
“什么问题?”Frank 问道。
“这是一个例子,说明按照错误的值排序数据对搜索没有帮助,”Notation 解释道,“Drecker 教授举了一个找出过去 10 年最冷的一天的例子。如果日志按天排序,你可以使用二分查找高效地找到任何具体的一天。但这对我们找到最冷的一天并没有帮助,所以我们还是得扫描所有数据。我没想到会在课外看到这么清晰的例子。”
“欢迎来到现实世界,”Frank 说道,“在这里,你得判断数据结构什么时候对你有用,什么时候没有用。别担心,这是一个常见的新手错误。”
他能看到符号对他的话不满,忍不住不去享受她的反应。每个新兵从学院出来时,都觉得自己什么都懂,而每个人都有很多东西需要学习。符号还算幸运,得到了一场讲座。至于他自己学二分查找时,得花几个小时在猪粪堆里刨来刨去,同时怀疑自己的职业选择。
约三分钟后,他们找到了唯一的线索。重试循环最近在泥墙港和破线岛停留过两次,停留时间都很可疑。即使是走私者,这些地方也很奇怪。泥墙港的贸易几乎只限于外围的泥土农场。而破线岛则更加荒凉,这个小而崎岖的岛屿上只有一座建筑——现在已废弃的铁环监狱。
“在那里,”弗兰克指着说道,“那就是他们拿走你文件的地方。可能是泥墙港或破线岛。他们可能在一个港口把文件丢下,在另一个港口拿到付款。”
“你怎么知道?”符号问道,她看起来很怀疑,“难道我们不应该把所有港口都考虑为——”
弗兰克打断了她的话,“没时间检查所有的。”他没有多加解释。他现在正在使用自己的算法,那种曾让他因过于草率的启发式搜索而惹上船长麻烦的方法。但他有一种直觉,而弗兰克学会了信任自己的直觉。
“你确定——”符号开始说话,但被上方传来的声音打断了。
弗兰克听不清楚话语,但他能清楚地辨别出语气。麻烦来了。
警察算法 101:二分查找 II
德雷克教授讲座摘录
高效算法的关键是信息。以二分查找为例,我们要求数据已经排序,并且我们了解数据的排序方式。为了排除(或剪枝)搜索空间中的大区域,算法必须能够保证目标值不可能出现在该区域。如果我们知道数据在数组中如何变化,我们才能做到这一点。在计算问题中,如果数组中的所有值都是按递增(或递减)顺序排列的,我们就说这个数组是排序过的。
然而,仅仅因为数据按一个维度排序,并不意味着你可以沿着另一个维度进行二分查找。假设你正在搜索一本账簿以寻找线索。账簿是按交易编号排序的,交易编号表示交易的记录时间。这意味着每条记录的交易编号都小于下一条记录的交易编号。如果当前记录的交易编号是 105,那么我们知道它之前的所有记录的交易编号都会小于 105,之后的记录的交易编号都会大于 105。

然而,这也意味着其他字段中的条目,比如实际交易日期、商家的名称或交易金额,并未按排序顺序排列。如果你有兴趣查找超过某个可疑金额的交易,或者找出与已知武器商人相关的交易,这时排序能帮你吗?不,它仍然会让你使用穷举法进行线性搜索。知道交易 105 是在 Zed’s Coffee 发生的,并不能告诉你之前或之后的交易中的商家或金额信息。
同样,如果你按交易金额的升序对账本进行排序,这可以帮助你快速找到所有金额为 250 美元的交易,但它无法帮助你查找特定的交易日期、ID 或商家。
第七章:—7—
适应算法以进行大胆逃脱
沉重的脚步声在甲板上方砰砰作响。Frank 环顾四周,评估着他们有限的选择。唯一的舱口通向甲板和新来的那群人。货舱几乎空了,船员们在抵达 Usb 后已经卸载了货物。躲在这里等于站在角落里小声说“你看不见我”。
当 Frank 列举并排除了每一个可能的选项,包括那种少见但有效的伎俩——躺下装死时,他看到 Notation 拿出徽章并站得笔直。
“你在想什么?”他 hissed。
“我是执法人员,正在进行正式调查。”Notation 解释道。
Frank 难以置信地摇了摇头。“‘以法律的名义停止’这种套路在这里行不通。或者说,大多数地方也行不通。我们是在一艘走私船上,调查警方财产被盗案。你们这边没人知道你在这儿,对吧?我敢打赌,那个从门口进来的人知道这一点。”
Notation 张开嘴想争辩,但停了下来又闭上了嘴。她把徽章滑回夹克里,一群大块头、穿着出奇精致的暴徒涌过门口。他们散开,在货舱里围成一个松散的圈,将 Frank 和 Notation 围在其中。
“各位,”Frank 说道。“我们已经完成检查,看起来你们并没有携带腐烂的鳗鱼。感谢你们的耐心配合,我们将继续努力确保本王国的食物供应安全。我们现在就要离开。”
作为回应,两名较大的暴徒抓住了 Frank 的双臂。他们一起把他抬离地面,并继续把他抬到甲板上。多年的经验让 Frank 为这种反应做好了准备,他已经发展出了一种定位自己的技巧,尽量减少不适感,但他仍然能感觉到在他们强有力的抓握下,淤伤正在形成。
“嘿!”Notation 的喊声表明她也被类似地带到了外面。
Frank 眨了眨眼,他们走出阳光下。那些人把他抬到甲板中间,把他扔到木地板上。Notation 砰地一声摔在他旁边,暴徒们再次围成了一个松散的圈。
Frank 慢慢地将自己推到坐姿,打量着他们的看守。他们随着船的摇晃而晃动,但其他没有动作。看起来他们在等人,也就是说,负责人还没有到。Frank 抓住机会,转向离自己最近的一个暴徒。
“那接下来怎么办?”Frank 问。“把我们关起来?把我们丢到船外?交给你老板的雇主?”
那个男人耸了耸肩。“别看我,我才在这里工作十五天。”
“新手,嗯?”Frank 说道。
Vinettees 对信息的保密非常狂热。他们只与最资深的船员分享计划。新加入的成员需要证明自己的忠诚,并逐步晋升。要获得任何有用的信息,弗兰克需要找到最资深的船员。
弗兰克脑中开始形成一个计划。Vinettees 的船员总是按资历排序的,这与导师制度有关——最初级的船员会作为新人的导师,依此类推。在群体中,他们总是倾向于站在自己导师旁边。

那些暴徒的圈子不过是一个已经弯成环形的排序数组。二分查找几乎可以在这里使用,但必须对数据的组织方式进行适配:是一个环形,而不是一条直线的值。不幸的是,这意味着弗兰克不知道数组的起始和结束位置。他很快就开发出了一个新算法,用来高效地寻找最资深的船员。在这种情况下,高效意味着既要尽量减少与暴徒交谈的次数,又要尽量在他们察觉之前获得答案。
他转向暴徒右边的女人,“你呢?你是这里的资深人物吗?”
“十九天,”她说。

现在,弗兰克预期他已经得到了一个排序——资历从逆时针方向增加。但他还不能确定。尽管这种可能性非常荒谬,十五天和十九天可能分别是最初级和最资深的暴徒。他在选择搜索起点时曾不小心出过差错,结果总是不好。他需要另一个数据点。所以他选择了剩下的暴徒范围中的中间位置。
“你呢?”他问了一个站在十五天对面的女人。
“三十七天,”那个暴徒回答道,“关你什么事?”

得到这个信息后,他的直觉——按逆时针排序——得到了确认。因此,他排除了从十五天到三十七天之前的所有人——如果最资深的暴徒不是三十七天,那么他必定会在三十七天的逆时针方向,但要在十五天之前。

弗兰克抑制住了心头涌上的不满。他从未遇到过如此初级的船员,实际上他感到有些受辱。“这真让人尴尬,”弗兰克对他们的俘虏说,“我们竟然被一群新手抓住了。你们是后备队吧?”
“你在做什么?”Notation 低声问道。
“一种改良的二分查找,”弗兰克咆哮着回答。
Notation 叹了口气。“我早就看出来了。看来你是在寻找最资深的人。你并不算太隐秘。但为什么呢?你怎么知道他们是按顺序站着的?”
弗兰克不理会她。他深吸了一口气,重新集中精力完成手头的任务。他不知道在老板出现之前还有多少时间。他选择了剩余范围中的中间点。“那你呢?”
“这是我的第三天,”那人犹豫地回答道。
“拜托!”弗兰克大喊,“真的吗?”
“三天?你不在培训中吗?”注释员问道,语气中透出真诚的好奇。

弗兰克再次精确地缩小了范围,考虑到最资深的人不可能是三天、十五天,或者介于这两者之间的任何人。
弗兰克再次将剩余范围分成一半。“那你应该是个相对资深的人吧?”弗兰克问道。
“呃……这是我的第一天,先生,”那混混结巴道。当大家的目光集中在他身上时,他开始大汗淋漓。
弗兰克低声咒骂。
“别叫他先生,”十九天大声喊道,“他是我们的囚犯。”

弗兰克现在把搜索范围缩小到了一个非常狭小的窗口。“我猜你这是第一天上班吧?”他问最后一个考虑中的混混。他没掩饰自己心中的轻蔑。
那个女人笑了。“我已经和维内特家族合作一个多月了,”她说,“四十二天的时间,防止好奇的警察插手。”

对了。“真的?”弗兰克说,“那你在这里干什么?”
那个混混皱起了眉头。“你什么意思?”
“维内特家族通常把他们的高级打手安排到更重要的任务上。看守一批走私的卷心菜似乎是浪费你们的时间,”他说,努力回想是否在日志中看到过关于卷心菜的内容。反正这也是一个合理的虚张声势。所有走私者在某个阶段都会涉及卷心菜。最近卷心菜税收的增加几乎使 Usb 黑市贸易翻了一番。
“卷心菜?”那女人嗤笑道,“我第一天就做过卷心菜的工作。现在我们有更重要的任务。”
“真的?”弗兰克说,“直接升到胡萝卜这一层了?”
那个混混的脸涨得通红。尽管蔬菜走私通常占走私者利润的 80%以上,但不知为何,蔬菜走私始终是这个行业中令人尴尬的一部分。
“不,”她说,“比胡萝卜好一百倍。一个私人合同。”
“真的吗?”弗兰克说,“我听说做胡萝卜生意不错,生意好,钱也多。”
“哦,别担心这个,”那个混混傲慢地说道,“联盟为我们的服务支付丰厚报酬。我们被告知——”
“Runtime 先生。”一个熟悉的声音穿透了空气。“别再试图从我的员工那里套取信息了。你会发现,这些信息既没什么用,也难以获得。显然,他们什么有价值的东西都不知道。”
弗兰克抬头看见瑞贝卡·维内特加入了圈子。他的胃一阵紧缩。

“那你是?”维内特问,目光转向了注释员。
“这是来自食品安全局腌制鳗鱼部门的苏珊·波因特,”弗兰克说,“我们正在调查一批不合格的 Usb Greytails 货物。”
瑞贝卡·维内特发出轻微的“啧”声。“不,运行先生。我不这么认为。”她停顿了一下,仔细观察标记。“如果我没搞错的话,这是伊丽莎白·标记警官。刚入警察局一年。”
“我在执行公务——”标记开始说道。
“不,”瑞贝卡·维内特打断道。“你不是在进行官方调查,标记警官。我知道所有目前参与官方调查的警员,从大规模水产盗窃到蠕虫赛跑。我的消息来源让我对这些事情了如指掌,而你并不在他们的名单上。但你确实在我的船上非法入侵。所以问题是该如何处理你?”
“我以为问题是‘为什么’?”弗兰克说道。
“运行先生,”瑞贝卡·维内特以夸张的耐心说道,“请不要小看我。我知道原因。我在你之前就知道原因。我还知道谁、什么时候、什么,甚至如何。”
“但问题依然是,如何处理两个多管闲事的警员——哦,抱歉,你不再是警员了,是吗,弗兰克?我该问:我该如何处理一个多管闲事的警员和一个多管闲事的被除名前警员?”
弗兰克紧握拳头,思绪回到了他在警局的最后一个月,以及瑞贝卡被释放时她那嘲弄的笑声。他当时无法建立案情,至少在按照规则行事的情况下是无法建立的,而队长被迫放了她。
“怎么样,用独白把我们无聊死?”弗兰克问道。“为什么不讲讲联盟的事?”
瑞贝卡笑了。“别担心,运行先生,我没打算让你待到足够长的时间来感到无聊。当然,我早就决定了如何处理你,如果你再次挡了我的路。这个问题不过是出于一种好意,给你一种对自己命运有控制感的假象罢了。”
“死亡?”标记尖声问道。
瑞贝卡·维内特点了点头,示意一圈手下,他们开始行动,从昂贵的西装里拿出各种华丽的武器,仿佛魔术师在进行复杂的变魔术。一位高个子男子从领带下拿出了一根三英尺长的带刺大棒;另一名打手则从袖子里滑出一把阔剑。
然后,出乎所有人意料的是,一个桶带着巨大的咚声砸在甲板上,随即裂开,腌制的鳗鱼喷洒在木板上。
警察算法 101:调整你的二分查找
德雷克教授讲座摘录
在你职业生涯中,面对的并非每一个计算问题都会有现成的、打包好的解决方案。当然,学者们花了多年时间研究广泛的问题并写出了解决方案。但在这个领域,你会遇到新的问题和新的变数。如果你仅仅通过背诵一张算法清单从这门课上毕业,那么很快你就会发现自己陷入了深深的困境。
为了处理新颖的问题,理解算法的工作原理以及如何将其应用于新问题是非常重要的。二分查找的基本思想——利用数据结构反复将搜索空间对半分割——比具体应用的细节更加重要。通过这一点直觉,你可以将二分查找应用于查找环形(但仍然有序的)数组,甚至通过不断测试更温暖和更凉的咖啡,直到找到“恰到好处”的温度。
第八章:—8—
袜子:插曲与引言
当腌制汁液和滑动的鳗鱼溅到甲板时,混乱爆发了,恶棍们跳开了。一桶腌汁随之落下,激起了新一轮的波涛。从鳗鱼的外观来看,它们是深水长背鳗,每条大约有四英尺长,令人既想绊倒又想纠缠,更别提那种恶心的感觉了。一群三个恶棍试图逃避溅射区时跌成一团。
弗兰克跳了起来,抓住诺塔申的手臂,把她拉了上来。
“发生了什么?”她喊道,第三桶从他们头顶飞过,击中了栏杆。
“不知道,”弗兰克说道。“但现在是我们的机会。走这边!”
他拉着诺塔申朝船边的栏杆走去,踩过腌制汁液的水洼和鳗鱼堆。第四桶落到甲板上,恶棍们再次四散逃开。甲板开始看起来像是一个堆满长长的、苍白灰色面条的怪异碗。
船旁边漂浮着一艘小型、不起眼,但奇异熟悉的双桅帆船,船上装满了更多的桶。一个穿着巫师长袍的少年正在将新的一桶滚到由木板组成的装置上。

“我们应该跳过去,”诺塔申说。
“跳吧,”弗兰克同意道,目光盯着另一艘船。他没认出那个男孩,但猜测他一定是维内提人的敌人。除非他们已经开战,否则没人会在理智的情况下把腌制的鳗鱼扔向维内提人。再说了,理智的人在任何情况下都不可能选择鳗鱼作为武器。
诺塔申点点头,毫不犹豫地跳上了栏杆。她轻松地跃过间隙,稳稳地落在了双桅帆船的甲板上。
弗兰克爬上了栏杆,低声咒骂了一堆布尔值的脏话,在木梁上犹豫了一下。然后,他数了几次到三,终于跳了下去。弗兰克差了两英尺没跳到双桅帆船,跌入了冰冷的水中。他浮出水面,刚好看到另一桶从头顶飞过。他稍微停顿了一下,心想那个男孩还在继续扔鳗鱼吗,还是换了别的东西——比如软奶酪。
诺塔申伸手把他拉上了船。他站起来,开始拧干他的披风,同时评估他们的新处境。那个少年还在继续向重试循环发动攻击。水手们在甲板上忙碌,拿着长木杆推开那艘小船,远离重试循环。
“梅维斯?”弗兰克大声叫道。“我知道你就在这儿某个地方。”
梅维斯在片刻后从货舱里走了出来。“他付了货物费,”她指着那个少年说。“他想怎么卸货,就看他了。”
“我以为你不能——”
梅维斯挥手示意他不要再说了。“反正我欠维内提人一个人情,”她解释道。“上个月他们偷走了我的一个藏匿点——17 蒲式耳的胡萝卜!”
Frank 目瞪口呆地盯着她。与 Vinettees 作对是件危险的事。一百车篓的胡萝卜也远远不值这个。
“别那样看我,Frank,”她说。“我不是出于好心这样做的。这是一个有偿合同。就是这么简单。Socks 让这一切值得。”
“Socks?”Frank 问道。
“Socks?”Notation 继续问道。
Mavis 朝那个男孩示意,然后耸了耸肩。“那是他自己叫的名字。在我的行业里,你不能要求知道真实姓名。”
“你在让他登船之前没有要求出示正式身份证明吗?”Notation 问道。Mavis 投给她一个怀疑的目光。
到现在为止,Mavis 的船员已经将 TCP Flyer 转向,他们正驶向大海。脏污的帆被升到空中,挡住了 Socks 投掷桶的视线。他最后长时间地看了一眼 Retry Loop,微笑着,最终转向 Frank。
“嗨,”他说得异常欢快,“我是 Socks。”他伸出手。Frank 小心翼翼地和他握手。
“谢谢你救了我们,Socks,”Notation 也伸出手和他握手。她眼神锐利地打量着他,然后显然对自己这种微妙的方式不满意,于是接着问道,“Socks,你的真名是什么?”
“不幸的是,那确实是我的真名,”Socks 有些伤感地回答道。“我的全名是 Socks Repellent,警官。”
“哦……”Notation 没有继续下去。显然未能找到合适的安慰来应对这样的不幸,她重复道,“谢谢你救了我们。”
“随时效劳,”Socks 回答道。“我之前没有扔桶的经验,所以很高兴能成功。”
“为什么帮助我们,Socks?”Frank 问道。“更重要的是,你怎么知道我们需要帮助?”
“嗯,”Socks 说道。“你看……我整个早晨都在跟踪你们俩。”
“三比特巷的那条小巷?”Frank 问道。
“是的,”Socks 回答道,脸红了。“而且就在 #2 停车位的大型 ArrayCart 后面。”
“我错过了那个,”Frank 承认道。
“他的腿在车轮旁边清晰可见,”Notation 说道。
“你为什么跟踪我?”Frank 问道。
“我们,”Notation 更正道。
“因为我们追的就是同一伙人,”Socks 说得理所当然。
“我们是吗?”Notation 问道。
“我想是的,”Socks 突然看起来有些不确定。“你在调查警察局的盗窃案件,对吧?我昨晚看到 Donovan 队长去找 Frank,所以我以为这跟总部的案件有关。”
“盗窃案件?”Frank 问道。“不止一起吗?”
“哦,”Socks 说道。“你们不知道。抱歉,也许我不该说什么。但我觉得这不应该是秘密。我猜——”
“为什么巫师们关心从警察局偷盗的事?”Frank 插话道。
“国王召集了他们。几周前,Fredrick 国王召集了一些王国最资深和受人尊敬的巫师调查一起盗窃案件。我当然是 Gretchen 的学徒。虽然只有第二年,但——”
“为什么他要召集高级巫师?”Frank 再次打断道。Gretchen 这个名字他之前没有听过。显然,实力更强的巫师们已经被更紧急的任务占据,而国王只能按名单往下走,但 Frank 不想因为质疑导师的资历让孩子难堪。
“首都警察有足够的装备处理盗窃案件,”Notation 补充道,“应该由他们来调查。”
“国王召集了巫师来商讨面具的事情,看看他们是否能找到它,”Socks 解释道。
“什么面具?”Notation 问道。
“你不知道那个面具?”Socks 有些慌张地说,“哦,天啊。也许我不该说这些。”
“什么面具?”Frank 低声咆哮道。他揉了揉太阳穴,深吸了几口气。
“Repellent 先生,”Notation 用她那种正式的警察语气接话道,“我们正在进行一项重要的调查。如果你有能够帮助此案件的信息,这是你的责任提供出来。请告诉我们一切。”
“从头开始说,”Frank 补充道。
经过 10 分钟,Socks 终于讲完了他那段过于详细的故事,内容是关于一场持续一个月的犯罪狂潮,期间大量无关的物品从安全的地方被盗走。最让人不安的揭示是,盗贼从一支军队护送的车队中偷走了一件危险的魔法物品——一副魔法面具。当被追问面具的功能或为何需要武装护送时,Socks 只会重复说那是“极其强大的”。他语气中的敬畏让 Frank 感到不安。
“你认为这次盗窃与警察局的盗窃案件有关联吗?”Notation 问道。
“Gretchen 也是这么认为的,”Socks 回答道,“在每个案件中,警卫们直到第二天才发现盗窃事件发生。她认为小偷使用了记忆法术或睡眠咒语。”
“我没有睡着,”Notation 用如此坚定的语气说,甚至让 Frank 都往后退了下去。
“我……我不是那个意思,”Socks 结巴着说道。
Frank 让他继续胡乱拼凑语言,自己则在思考孩子的故事。这个故事有些不对劲。“为什么跟踪我们?”他最终问道。“你是接受国王授权的调查对吧?你本可以直接走上前,自我介绍的。”
“是的!但是……”Socks 声音逐渐低了下来。
“但是?”Frank 问道。
“我不确定这会不会值得,”Socks 承认道。
Frank 瞪了他一眼,直到他继续说下去。
“我不确定你们会发现任何有趣的东西,”Socks 解释道,“一旦我自我介绍,我就得跟着你们去帮忙,对吧?我想保持选择的余地。万一我听到更好的、魔法方面的线索呢?抱歉,”孩子低声说道。
他们陷入了沉思的沉默,船员们继续在他们周围忙碌,做着让船航行所需的一切。从 Frank 的观察来看,这似乎主要是拉绳子。
“那……现在怎么办?”Socks 问道。
“我们跟随线索,”Frank 说道。
“我们在重试循环的日志中发现了两个可疑的港口,”符号解释道。“我们的下一步是调查这些港口,首先是泥墙港。事实上,我应该通知船长。当然,我们需要借用她的船来进行调查。”
弗兰克笑了。“祝你好运向梅维斯解释这一切。”
“不用了,”袜子急忙说道。“我已经付钱租用了这艘船。你只需要告诉梅维斯航行的方向。”
“太好了,”符号说道。
“这意味着你要和我们一起去,不是吗?”弗兰克问道,语气中没有试图掩饰的恼怒。
“当然了,”袜子说道。“我付了船费。我救了你。而且我似乎比这里的任何人都更了解这个案件。而且我是个学徒巫师。这可能派得上用场。”每多说一个理由,他的绝望似乎就加深一分。
“雷波伦特先生,我们会感谢您的帮助的,”符号向他保证道。“对吧,弗兰克?”
“是的。太好了,”弗兰克嘀咕道。他又带上了一个他不信任的拖油瓶。照这个速度,天黑之前他可能会把船塞满。
第九章:—9—
返回继续寻找线索
泥墙港口甚至连名字都不配得上。它根本不算什么港口,那座摇摇欲坠的木栈道最多只能停下两艘中等大小的船。所谓的泥墙,理论上是一个高达 20 英尺的土城墙,但实际上根本没有建成,只是城市周围三分之一的地方零星地有 2 英尺高的土包。
Frank 跨过土包,朝城市里唯一的商店走去,Notation 和 Socks 跟在他后面。
当他们走进商店时,店主的脸从惊讶变为因强烈打喷嚏而彻底的愉快。他急忙走到他们面前时,不小心把一堆胡萝卜主题的旅游小册子打翻了。
“你好!”他几乎大喊。“欢迎来到泥墙,这里有著名的泥胡萝卜农场。您需要什么?食物?用品?胡萝卜?胡萝卜味的烘焙食品?我们有非常好吃的胡萝卜派。”
“信息。”Frank 说道。
店主的脸色一下子垮了。“哦,”他说。“那你们不是来参加胡萝卜节的吗?”
“胡萝卜节?”Notation 问道。
店主点了点头。“本周晚些时候是第 50 届胡萝卜节。”
“你们为此吸引了很多游客吗?”她问。
“最近没有了。”店主承认道。“泥墙港不像以前那样吸引游客。自从 G’Raph 举办了泥萝卜节之后,大家都更愿意去那里,享受大城市的氛围。”
Notation 和 Socks 互相对视。Socks 口型说:“什么是泥萝卜?”Notation 摇了摇头。
“你知道几天前通过这里的船吗?”Frank 问道。
“什么船?”店主问道。“我们这里已经好几个月没见过船了。倒是有几辆驴车从沿海路经过。但没有船。”
“你确定吗?”Notation 问道。“因为我们正在进行一项正式的调查,了解最近所有经过这里的船只的信息对我们来说非常重要。”
“如果有船的话,我早就知道了。”店主说道。“从我的窗户能看到码头的情况。即使我没看到过,也一定会听说的。现在我们这儿船不多,大家都会很兴奋。‘胡萝卜之声’,我们三人组成的行进乐队,通常会迎接每一艘船。我肯定会听到他们的。”
Notation 看起来像是准备迎接新一轮的提问,但 Frank 先开口了。“谢谢您,先生。”他说。“感谢您抽出时间。”
他把 Notation 和 Socks 从店里赶了出来,带回了泥泞的街道。
“你觉得他说的是真的吗?”Notation 问道。
Frank 点了点头,扫视着街道。“他听到船的事时看起来真的很惊讶,”他说。“不过,再问问其他人也无妨。”

他们采访了镇上的十几位居民,样本大约占了人口的一半,然后才承认失败。没有人见过船。没有人听说过有人见过船。甚至没有人理解为什么船会来。显然这个港口没有接待很多船只。
“也许是在晚上来的,”Notation 一边走向TCP Flyer号飞船的码头一边沉思。“他们派了一小队人乘着小船上岸,交接文件后就离开了。有人可能就在这里等着,旁边有一辆马车。”她指向码头上随意的一块木板,看起来和周围的木板没什么不同,既不更可疑,也不更无辜。
“也许吧,”Frank 说。“不过没关系。没有目击者,就没有线索。”
“那是什么意思?我们找不到他们了吗?调查结束了吗?”Socks 问道。
Frank 哼了一声笑道:“不,孩子。死胡同是调查工作的一部分。这就是我们使用回溯搜索的原因。”
Socks 茫然地盯着 Frank 看。当那孩子脸上没有显现出理解的神情时,Frank 补充道:“我们会沿着最有前景的方向继续探索,直到那条线索走到死胡同。然后我们回溯到之前未探索过的、但有潜力的线索,从那里继续搜索。”
“所以你们还有其他线索?”Socks 问道。
“有几个,”Frank 承认道。
“如果我们回溯到下一个未探索的线索,”Notation 沉思道,“我们回到日志本。那里列出了另一个目的地:断线岛。”
Frank 点点头,考虑着剩下的线索。那位高级暴徒提到过一个什么联盟。这意味着 Frank 目前的未探索线索列表可怜地包含了:
断线岛
ArrayCart 上的线索
Vinettees
联盟?
搜索现在将回溯到日志本和未探索的岛屿。如果他们在断线岛没有找到任何线索,他将不得不追随其中一个较为微弱的线索。从推车上来的彩色线索足够独特,值得适度地尝试。
“所以我们还没完成吗?”当TCP Flyer号飞船驶向断线岛时,Socks 第十次确认。“你们还有一些线索,对吧?”
“调查总是会回溯的,”Notation 再次安慰他说。“你在工作中不需要回溯吗?做药水时呢?”
Socks 看起来很震惊。“你在调制药水时怎么回溯?你不能把蜘蛛腿拿掉,也不能把混合物搅拌回去。”
“我是说在开发药水时回溯。你尝试加蜘蛛腿,然后意识到这破坏了魔法的连贯性之类的?所以你把那条指令划掉,下次调制药水时换一种方法。你尝试的每个配方就是你搜索空间中的一个状态,回溯的方式是回到上一个有效的配方。”
“哦,”Socks 说道。“你是说修改。你在开发法术和药水时必须进行修改。你仍然在朝着目标前进,只是不是直线。没有人能第一次就做对。”
“没错。就是这个意思,”Notation 说。
梅维斯加入了小组,补充道:“这就像是在洞穴中寻找一个‘遗忘’的物资藏匿处,你会沿着不同的路径探索,当你没有发现任何有价值的东西时,就会退回去。”
“是的,”符号有些迟疑地同意道。“回溯搜索就像是在探索洞穴。虽然我必须指出,发现的物品是否被遗忘很难判断。我猜你可以——”
“说到搜索,我们已经到达你的下一个目的地,”梅维斯打断道。“破损电缆岛没有码头。我们会把TCP Flyer停在这里,但你们得划船进去。”
警察算法 101:回溯
德雷克教授讲座摘录
几乎每一次调查都涉及一些回溯。即使是最优秀的警员,也不可能总是顺着线索从头到尾直线前进。这个世界充满了无用的信息、模糊的线索和误导性假象。而且,你还会犯错。所以,知道如何在遇到死胡同时回溯搜索是非常重要的。简单来说,这意味着将你的搜索回到之前的状态,并尝试不同的选项。
直到目前为止,算法都在那些可以高效地从任意状态跳跃到其他任意状态的搜索空间中呈现。例如,在数组中,你可以通过索引轻松地检查任何位置的值。而在酒店的走廊里,你可以在房间之间跑来跑去。这种灵活性为算法提供了效率。
然而,许多搜索空间有一定的约束,限制了你如何从一个状态移动到另一个状态。如果你在现实世界中搜索一座城堡,你不能随意在房间之间跳跃——你必须穿越其中的走廊和房间。在计算机领域,类似的约束也适用于图或链表等数据结构。
即使在可以在状态之间跳跃的情况下,将回溯想象为在前进的过程中回到之前的状态,往往也是很有用的。在算法世界中,这比物理上重走一遍路程的成本要低得多。然而,两个过程在概念上是相似的:你回到之前的搜索状态,然后沿着另一条路前进。
在接下来的讲座中,我们将看到许多在遇到死胡同时需要回溯的搜索示例。等你加入警队后,你将会经历比你想象的更多的死胡同。
第十章:—10—
使用广度优先搜索开锁
弗兰克、袜子和符号站在监狱外墙的后门旁。尽管门上确实覆盖了一层令人印象深刻的锈迹,但这道锁门仍然抵抗了弗兰克两次试图踢开的努力。他只成功地将空气弄得一片红尘,还让符号学到了至少六个新的布尔逻辑咒语。
“所以……那不行,”袜子补充道。弗兰克没理他,开始研究锁的机制。这是一个标准的刻花数字键盘,按钮上标有 1、2、3、A、B 和 C,按键排列成一行,下面有一个 ENTER 键。

“我们得老式点做了,”弗兰克说。
“踢开大门不是老式的方法吗?”符号问道。
弗兰克同样没有理会她。“袜子,你知道什么魔法开锁法术吗?”
“不行,”袜子大声抗议。“这些是非法的!”
“那要不试试弱化锁呢?或者也许是铰链?”弗兰克问道。

“你要我帮你破坏财产?”袜子露出惊愕的表情。“那比开锁还要糟糕。你知道这会惹多大麻烦吗——”
“那是搜索法术吗?‘全组合法术’或‘广度优先搜索法术’?”符号打断道。自从弗兰克随意询问关于在金币上复制法术的可行性之后——这种魔法明显违反了袜子和她自己的伦理底线——她已经听够了关于正当与不正当法术的讨论。
“我用过几次广度优先搜索法术,”袜子回答。“我真正擅长的是二叉搜索树,但我熟悉一系列计算技术。曾经有一次——”
“广度优先搜索能在锁上起作用吗?”弗兰克打断道。多年来,弗兰克处理过几个不同程度尊重的巫师案件。他见过至少十几种不同的开锁法术,但从未见过通过明确的广度优先搜索打开门锁。
符号微笑着说:“当然!这有点抽象,但我最近在我的《警察算法》课程中看到过类似的问题。想一想,密码锁就是一个搜索问题;你输入一串字符来打开它。搜索空间是所有可以由这些字符组成的可能字符串。每个字符串都是一个有效的搜索选项,从一个字符如 1 或 A 到复杂的序列如 ABC123CBA321。搜索目标是那个打开锁的字符串。”
“但是我们连需要多少个字符都不知道,”袜子抗议道。“那个锁可能有 30 个字符的组合。”
“这就是她建议使用广度优先搜索的原因,”弗兰克边思考边回答袜子的问题。
“我不明白,”袜子说。
符号迅速接过了解释。“你看,广度优先搜索从一个起点开始向外扩展,在解的边界上进行探索。它自然会先尝试较短的解。”
“嗯?”Socks 困惑地问道,显得有些慌张。“我以为广度优先搜索使用的是魔法列表。我一直用的是魔法列表,不是就是魔法列表吗?”
“是的,”Notation 同意道。“广度优先搜索会维护一个待尝试的选项列表,如果当前选项不起作用,我们就会从当前选项出发,寻找新的选项并将它们添加到列表的末尾。每次循环时,我们从列表的前端挑选一个新的选项尝试。如果那不是我们想要的,我们检查是否有任何新的选项可以从当前选项到达,并把这些未探索的选项添加到列表的末尾。”
“你从搜索空间的一个点开始,在这种情况下,从一个长度为零的密码开始。然后,对于每个尝试的密码,你在列表的末尾添加新的搜索可能性。在这种情况下,每次我们尝试一个密码时,我们会把所有的单字符扩展密码添加到列表的末尾。例如,这里我们知道密码只能包含字符 1、2、3 和 A、B、C。一旦我们测试过 3A,就会把 3A1、3A2、3A3、3AA、3AB 和 3AC 添加到我们的列表末尾。”
Socks 皱着脸集中注意力,然后问道,“我们怎么知道该加哪些选项?”
“把它当作一个可能性的树,”Notation 建议道,“每个分支,或者说节点,都是我们列表中的一个密码,比如 3A。它下面的相邻选项是那些通过在末尾加一个字符得到的密码。广度优先搜索会先遍历树的每一层,再移动到下一层。”

“因为我们把新的、更长的密码添加到列表的末尾,所以我们会先尝试所有短的密码,”Frank 补充道,“现在,你能做到吗?”
“这不是一个合适的使用——”
“来吧!真的吗?”Frank 打断道。
“这基本上就是一个开锁魔法,”Socks 回答道。
“对!完全是这个意思!”Frank 喊道。
“算了,”Notation 沮丧地举起双臂说道,“如果他不舒服开锁,我们大喊也改变不了他的想法。”她转身看着石墙,那面墙至少有 10 英尺高。过了一会儿,她继续说道,“Frank,如果你给我一个支撑,我也许可以爬过去。”
Frank 怀疑地看了看墙壁。尽管这堵墙已经被废弃了多年,但墙面没有那种老旧城堡墙上常见的大裂缝和藤蔓,这些往往能帮助登山。工艺相当精湛。显然,有人对建造这堵墙投入了真心,你可以从金属尖刺的艺术性扭曲形状看出来,那些细节显示出不小的工夫。
“也许吧。不过它挺高的,那些尖刺看起来非常锋利,”他说。
“这就像是学院里的障碍赛道,”Notation 说道,“除了坚硬的地面、没有把手的地方和那些大金属尖刺外。”
“那些可能会增添些刺激感,”Frank 插话道。
“闭嘴,给我一个支撑,Frank。”
“不,不。我来做,”Socks 急忙说道。“我会使用广度优先搜索法术。不过我需要一些东西来列出列表,可能需要一卷羊皮纸?”
Frank 和 Notation 互相看了一眼。“不行,小子。用地面吧,泥巴够多的。”
“哦,是的,当然。”
几分钟后,锁开始发光。“来了,”Socks 说道。
ENTER 这个词短暂发光,接着传来点击声。但大门依旧锁住。法术已经尝试了第一个密码,什么都没有。接下来,一系列的数字和字母出现在泥土中:
1, 2, 3, A, B, C
Frank 能想象出这个列表所代表的可能密码树。

一瞬间,数字 1 发光,随后是 ENTER。再次传来点击声,但大门没有打开。地上的列表发生了变化,显示了新的密码尝试列表,树的第三层被展开。
2, 3, A, B, C
11, 12, 13, 1A, 1B, 1C
但这些条目被加到了列表的末尾。搜索仍然继续进行,保持在当前层级,尝试 2。

密码 2 没有成功,列表再次增长。
3, A, B, C
11, 12, 13, 1A, 1B, 1C
21, 22, 23, 2A, 2B, 2C
树再次展开了新的可能性,但搜索仍然沿着当前层级前进,尝试所有单字符密码后再向更深层次移动。

换句话说,搜索在深入到更深层级之前,先探索了每个层级的所有可能。

搜索完成了第一层,尝试了密码 3、A、B 和 C,直到 Socks 打破了沉默。“这可能需要一段时间。”
Frank 点点头,眼睛盯着不断增长的数字列表。“Notation,你去前面侦察一下如何?”
“好的,”她同意道,表情显露出极大的宽慰。新手通常不太能应对守夜任务。连续几个小时坐着什么也不做,这不是在军校能学到的技能,尽管 Cloud 教授的《执法哲学》讲座差不多讲到了这个程度。
Notation 离开后五分钟,锁发出了响亮的咔嚓声,大门在生锈的铰链上吱吱作响地打开了。泥土中的列表逐渐消失,搜索算法完成了。
“1111,” Frank 说道,脸上毫无惊讶的表情。保持代码简单到足以让手下记住,通常是个明智的选择。
他用一根棍子在一片泥地上写下代码,并圈出了两次。即使是新手也不可能错过这个信息。然后他转向 Socks,说:“走吧。”
警察算法 101:广度优先搜索
摘自 Drecker 教授的讲座
广度优先搜索是一种按遇到的顺序探索搜索状态的算法。换句话说,它总是优先探索最早未搜索到的状态。
你可以将广度优先搜索想象为保持一个已知但未探索状态的列表(或更正式地,队列)。在每一步中,算法从队列的前端选择下一个要探索的状态。随着算法发现新的选项,它会将它们添加到队列的后端,以确保在继续探索新的选项之前,所有先前的选项都已被探索。
以图的探索方式来描述广度优先搜索是非常有帮助的。图是一种数据结构,由单独的节点组成,边连接这些节点。如果两个节点通过一条边相连,我们说它们是邻居,这意味着你可以在这些节点之间移动。在你的培训过程中,你至少学习过一个图——《王国高速公路地图》。这张地图将每个城市表示为一个节点,将连接它们的高速公路表示为边。确保你拥有一份这张地图的好版本。罪犯有逃离城市的倾向,你需要知道他们最可能前往哪些邻近的城市。
搜索《王国高速公路地图》是一个经典的图搜索问题。我们的搜索状态是图中的节点——地图上的城市。想象一下,A 市发生了一起犯罪,你的任务是找到逃跑的罪犯。

广度优先搜索沿着不断扩展的边界进行探索,首先检查从初始节点出发的X步之内的每个节点,然后才会检查距离初始节点X + 1 步之内的节点。在你探索完 A 市后,它的两个邻居 B 和 D 会被添加到队列的末尾。队列中没有其他城市,因此 B 是你下一个将要访问的城市。

如果每个节点有许多邻居,维护待探索节点的队列可能会占用大量内存。在大型搜索问题中,这种内存需求可能变得非常昂贵。作为一名警官,你可能需要投资购买一些好的笔记本。
在广度优先搜索的每一步,我们都会测试当前节点是否是目标节点。在这个例子中,这意味着要彻底检查城市是否藏有我们的罪犯。如果当前节点不是目标节点,我们只会将其之前未见过的邻居添加到列表中。(未见过的节点是指还没有添加到列表中的节点。)因此,我们避免再次添加已经探索过的节点或已经在列表中的未探索节点。例如,在检查完 B 市之后,我们不会再次将 A 市添加到列表中。

请注意,检查一个邻居是否是未见过的节点需要更多的内存,因为我们必须追踪已见过的节点。然而,这种方法的好处是显著的——我们避免了对已探索节点的循环。再次强调,仔细追踪你的搜索过程可以带来显著的回报。


在这个具体的例子中,我们发现我们的嫌疑人藏匿在 H 市。我们可以在这里停止搜索并进行逮捕。
在那些任何两个相邻节点之间的移动具有相同成本(时间、能量等)的搜索问题中,广度优先搜索能够确保找到具有最小总成本的路径。它通过从起始节点向外扩展,先探索每一个与起点相距X步的节点,才会探索与起点相距X + 1 步的节点。

广度优先搜索甚至可以通过保持回溯指针来适应返回最短路径。每个节点都会记录前一个节点。然后,在找到目标状态后,可以通过回溯指针来重建路径。
然而,记住这只适用于相邻节点之间的每一次移动具有相同成本的情况。在一般情况下,最小化搜索空间中的步骤数可能与最小化到目标路径的成本有很大不同。例如,如果远足者想要最小化他们的能量消耗(成本),他们可能会更倾向于选择一条更长的路线,以避开穿越山脉的路径。虽然山口的路线更短,且可以说更具观光价值,但它可能需要消耗更多的能量。

第十一章:—11—
在废弃监狱中的深度优先搜索
踏入监狱的两步,弗兰克就知道他们已经走进了一个迷宫。旧时的计算监狱往往依赖于其奇特的结构,和守卫一样,构成了其防守的核心。潜在的逃脱者在不知道另一边是自由还是守卫的休息室时,会三思而后行,犹豫是否通过一扇门。
“要不要来点光?”弗兰克建议道。
“哦。对,”袜子同意了。他低声念出一个咒语,蓝色的火焰从他法杖的尖端闪烁出来,点亮了这个完全不起眼的房间。
方形的房间、粗糙的石墙和沉重的橡木门足以证实弗兰克已经知道的事情:整个结构是一个房间的网格,每个房间只有一些相邻房间有门。他们必须从一个房间走到另一个房间。但由于他们不知道哪些房间之间有门,他们必须在前进的过程中寻找出一条路径。
“该再搜索一次了,”他说。
“搜索?”袜子问道。“搜索什么?”
“当然是文件,”弗兰克回答道。他毫不怀疑那些文件藏在这里。废弃的监狱提供了一个理想的藏匿赃物的地方,显然比更常用的仓库更合适。可以说,唯一更好的地方是废弃的城堡——前提是它有护城河。现在的问题是,他们能否找到这些文件,之后,如果找到了,这些文件是否能提供任何有价值的线索。
“不是广度优先搜索,”袜子抗议道。
弗兰克考虑了一下这个想法。从理论上讲,广度优先搜索在网格上是行得通的。搜索空间的每个状态都是一个网格方格。一旦你探索了一个网格方格,你就可以将它的未探索相邻方格添加到你要尝试的列表中。弗兰克可以清晰地想象到,搜索会像波浪在水面上扩展,蔓延开来,覆盖整个空旷的网格。

然而,广度优先搜索在物理世界中有一个主要缺点——过多的回溯。因为你总是将新的项目添加到列表的末尾,下一个要探索的方格可能会非常远。即使在一个空的网格上,没有墙壁阻挡你的路,你也可能发现自己不得不走回搜索空间的另一端。

这是弗兰克决定避免的那种不必要的动作。
“没有,”弗兰克说。“回溯太多。我们还是用深度优先搜索比较好。”
“深度优先搜索,深度优先搜索,”袜子低声自言自语,仿佛在将这个咒语刻进他的记忆中。“我—我想我记不住—”
弗兰克挥了挥手,信心十足地朝走廊走去。“这个我们不需要咒语。我从你还在穿尿布的时候就开始在建筑物里做深度优先搜索了。”
“那深度优先搜索就没有回溯吗?”袜子问道。
“大多数搜索算法都有回溯。但是深度优先搜索中的回溯更适合走路。”
“嗯。。。我明白了。”
“不,你不知道,”弗兰克直截了当地说。“如果你不知道算法,就直接问。假装知道算法会导致灾难。我见过太多新手因为糟糕的搜索而被绊倒。像你这样的好孩子。”
“好吧。什么是深度优先搜索?”索克斯问。
“这只是一个简单的算法,”弗兰克解释道。“基本上我们会深入探索每条路径。我们沿着一条路径走,直到遇到死胡同。然后我们回溯到最近一个没有走过的路径,并尝试那条。我们会在找到目标时停止。”
“在这种情况下,我们将使用顺时针顺序。每当我们有多个选项时,我们会尝试北、东、南、西——当然,避开我们已经尝试过的路径。我们会在每个交叉口使用相同的顺序,所以如果可能,我们总是优先选择北边。但在这种情况下我们只有一个选项,所以我们先选择向南走。”

即使弗兰克在说话,他们也已经到达了第一个决策点。弗兰克看了一眼选项。他们是从北边来的,所以他选择了东边——在他的顺序中下一个未探索的方向。在离开交叉口之前,他从口袋里拿出一块粉笔,在墙上做了一个小标记。

经过两个交叉口——先是向北转,然后向东转——他们达到了第一个死胡同。到目前为止,房间要么完全空荡,要么只包含一些奇怪的监狱单元格——这些单元格是房间内的围栏。由于缺乏其他明显特征,弗兰克在每个房间的墙上标记了一个数字,并在脑海中将这个数字与他在那儿发现的不同霉菌形态联系起来。

“现在我们回溯到最后一个房间,5 号房,那里有一个看起来像马的霉菌,”弗兰克一边回溯一边解释道。这次他们选择了从 5 号房间出来后唯一未探索的选项,向西走。不幸的是,他们马上又遇到了一个死胡同——一个空房间,墙上布满了复杂的绿色和蓝色毛绒图案。

他们回溯到最近一个选项已经被探索完的交叉口,直到他们在房间 4 找到一个新的选项。东边的选项是死胡同,他们已经探索过北边的选项,所以这次他们选择了向南走。
他们走过了两个新的空房间(8 号和 9 号),仅通过一个大橙色霉菌的钟乳石来区分,而他们尽量远离这些霉菌。橙色霉菌的结构稳定性不太好。经过另一个死胡同后,他们发现自己又回到了 2 号房间的第一个交叉口。

“如果我们错过了怎么办?”索克斯以他那种标准的担忧语气问。“或者如果我们陷入死循环怎么办?我们可能永远卡在这里!”
弗兰克呻吟了一声。“听着,孩子。这不是我第一次做深度优先搜索了。我知道自己在做什么。”
“但死循环。”
“你为什么认为我在标记墙壁?”弗兰克问道。“如果我们避免走已经探索过的通道,就能避免进入死胡同。”
弗兰克在警察算法的练习中学到了这个教训。当全班同学在场时,弗兰克走了六圈树篱迷宫,直到他听到另一位同学大声开玩笑道:“他又来了。”
他们继续深入迷宫,沿着曲折的小路走,不时在死胡同处倒回头。

然后,在 23 号房间,他们发现了一个小牢房,里面堆满了羊皮纸卷和账簿。

“我们找到了!”袜子兴奋地说道。他的法杖火焰在房间里投射出闪烁的蓝光。
弗兰克感到脖子上的汗毛竖起,他仔细观察眼前的场景。他对比了书堆的高度与他多年来完成的文书堆积,做了几个快速的计算。队长从不避讳把文件堆给他,但弗兰克从未见过如此场面。书堆底部甚至有发霉的页面。一切都让人觉得不对劲。
弗兰克走到最近的书堆前,拿起一张羊皮纸:一则关于使用鸭栅栏的注意事项。日期和车站编号标明这份文件属于被盗的档案。接下来的那张,列出了西部序列港口的噪音投诉,也来自被盗的档案集。它看起来同样随机且无助。
他跪下来,撬开底部附近的缝隙,猛地拉出一本账簿。页面上点缀着三只霉菌蝶的斑点,但弗兰克仍然能清楚地看出城堡卫兵的物资清单。这本账簿只能来自城堡本身。他拿起另一本书,发现上面记录了去年十一月的城堡卫兵轮换情况。
“这不对劲,”他低声说,“这里太多了,还有城堡的账簿。”弗兰克移到旁边的一堆文件,重新从上面开始。
“有规律可循吗?”袜子问道,似乎他刚刚注意到文件堆积的规模。
“我—”弗兰克开始说道,但他停住了脚步,当他翻开另一本账簿,上面写着转移请求。账簿中间的四页被撕掉了。
“很奇怪,”弗兰克一边翻阅完好的页面一边说,“这可能是—”
当袜子跌跌撞撞地朝他走来时,弗兰克的话被打断了,袜子挣扎着保持平衡。弗兰克看到背后在昏暗中有动静。直到他听到门铰链发出的生锈尖叫声,他才意识到发生了什么。
“门!”弗兰克大喊道,当年轻的巫师撞进他怀里。

他们俩摔倒在地。门砰的一声关上了。锁扣发出一声响亮的“咔嗒”声,锁定了。袜子的法杖在混乱中掉落,懒洋洋地旋转着,撞入了一堆干羊皮纸。法杖的蓝色火焰似乎比弗兰克记得的要大得多。
弗兰克震惊地躺在石地板上,看着纸张燃起火焰。
警察算法 101:深度优先搜索
来自德雷克教授讲座的摘录
与广度优先搜索不同,深度优先搜索是一种优先探索最近遇到的搜索状态的算法。该算法沿路径推进,直到找到目标或遇到死胡同。
与广度优先搜索一样,你可以将深度优先搜索想象成保持一个已知但未探索的状态列表(在这种情况下是一个栈)。在每一步中,算法从栈的顶部选择下一个状态进行探索。但不同于广度优先搜索,深度优先搜索将新选项添加到栈顶。
让我们回顾一下讲座中关于广度优先搜索的图示例子。记住,图是由单独的节点和连接这些节点的边组成的数据结构。它们可以用来表示各种概念,如城市地图、犯罪网络,甚至是城堡的布局。我们将使用讲座中提到的王国高速公路地图,从城市 A(犯罪现场)开始我们的搜索。

深度优先搜索沿着一条路径深入,直到遇到死胡同(或已经探索过的节点)。通过这种方式,算法优先在路径上进行深入探索,而不是像广度优先搜索那样在所有选项中进行广泛探索。

再次,我们在城市 H 找到了嫌疑人隐藏的地方——不过这次我们在搜索过程中走了不同的路径。
与广度优先搜索一样,我们通过跟踪之前访问过的节点来避免重复探索节点。如果你想避免陷入无尽的循环,一直检查相同的节点,这个检查尤为重要。在上面的例子中,我们完全避免将以前见过的节点(无论是已探索的还是未探索的)再次添加到列表中。
第十二章:—12—
自助餐厅的栈与队列
弗兰克蹲下身子,匆忙跑向门口。他一边拉一边推,把门撞得啪啪作响,试图把门打开。他抓住生锈的铁栅栏,用力将自己的全部体重压上去,却只发出更响亮的撞击声。
弗兰克转向索克斯,希望这位年轻的巫师知道一种能弯曲铁条的咒语。考虑到当前的情况,他确信索克斯甚至会同意使用开锁咒语。但当弗兰克的目光扫过那些冒着烟的羊皮纸堆和缭绕的烟雾时,他猛然僵住了。脑海中闪现出一个烟雾弥漫的厨房画面,唤起了他对学院第一年时光的遗忘记忆。几乎能听到厨师的喊声。弗兰克使劲闭上眼睛,试图把那段记忆赶走。
在他进入学术学院的前两个月,弗兰克将课业和在学校自助餐厅的勤工俭学工作安排得井井有条。这个工作并不光鲜亮丽;他们不允许新来的学生洗碗,更别提做饭了。相反,弗兰克每周花 15 小时把清洁过的托盘、盘子和餐具从厨房搬到自助餐厅的各个指定位置。

尽管工作枯燥乏味,弗兰克却发现自己乐在其中。“看看我!我正在逆向操作你们的工作。我是反洗碗工,”他会对三位正在收拾桌子并将脏盘子放进垃圾桶的洗碗工大喊。他曾尝试挑战学校记录,争取在两分钟内搬运最多的盘子,但失败了。他还创造了一个全新的自助餐厅游戏——“甩勺子”。但直到与海普教授的一次偶然相遇,他才从这份工作中真正学到了一些东西。
“唉。有些数据结构根本不适合出现在自助餐厅,”海普教授一边研究餐厅的食物选项,一边大声嘀咕。
下午 2:30,午餐高峰已经过去,弗兰克·运行正忙着将一堆碗运送到汤站。虽然这句话并不是对他说的,但他忍不住问教授:“什么是数据结构?”
“栈,”海普教授抬头看着弗兰克说道,“栈几乎永远不应该出现在自助餐厅里。”
“当然知道,”弗兰克回答时,带着只有新生和真心无知的人才有的那种自信。他低头看着自己正拿着的一堆碗。“碗堆。盘子堆。煎饼堆。”
海普教授做了个不屑的手势,开始走开。“你知道什么是数据结构吗?”
“你到底该怎么摆放盘子?”弗兰克问。“如果把它们一个接一个地摆放,空间太大了。”
教授停下脚步,盯着弗兰克,表情充满了深深的担忧。过了将近一分钟,他问:“你知道栈和队列的区别吗?”
弗兰克摇了摇头。他还没学过《警察数据结构》。
“栈是一种后进先出数据结构,”教授解释道,“它有两个操作。你可以把东西推到栈的顶部。或者你可以从栈的顶部弹出东西。”
他指了指排队前面的那堆盘子。“就像那堆盘子一样。你可以把一个盘子放到堆里去。”
他把空盘子放在了那堆盘子上面。

“或者你可以把盘子从顶端取下来。”他又拿回了自己的盘子。

“而且每次你从栈顶弹出东西时,你得到的都是栈中最新的项目。最旧的项目将一直待在栈底,直到你弹出它上面所有的东西。”
“那又怎样?”弗兰克问。“那有什么问题?”
“如果你正确使用后进先出数据结构,它没什么问题。栈在写深度优先搜索时非常好用;你只需不断地把新的搜索选项推到栈里,然后在回溯时一个个弹出。但是自助餐厅已经误用了栈几十年了!”
“拿这堆盘子看看。你知道底下那个盘子已经放了多久了吗?”
弗兰克试图回想上次看到那堆盘子空的时候,但他甚至连那个画面都想不起来。
“五年!”赫平斯教授大喊,“我知道,因为我做了标记。那个底盘已经在那里放了五年,没人用,像你这样的学生把其他干净的盘子堆在上面。它就那样静静地待着,边缘堆满了灰尘。”
“但这还不是最糟糕的。看看他们怎么对待马铃薯泥!”
弗兰克瞥了一眼那个盛 mashed potatoes(马铃薯泥)的木大碗。一名厨师正在给碗里加料,他一只手拿着大锅,兴高采烈地将新鲜的马铃薯泥盛进碗里。过了一会儿,弗兰克才意识到,旧的食物只是在被埋在下面。他感到一阵恶心。
“多久了?”他喉咙里哑着问,实际上他并不想知道答案。
“别担心,他们至少每周会清洗一次盛菜碗,所以那些旧的马铃薯泥也不过一周左右。”
弗兰克并没有感到放心。事实上,他感觉相当不适。快速扫视了一下自助餐厅,发现到处都是后进先出模式的应用。当他停在沙拉酱的大桶旁时,他的胃翻腾,混合着恶心和恐慌的感觉。
“我们该怎么办?”他问道。
“队列,”教授回应道,“队列几乎是为了自助餐厅而设计的。”
“队列?”弗兰克问。
“先进先出数据结构,”赫平斯教授解释道,“像栈一样,它们也存储东西,并且有两种操作。你可以通过把某个东西加入到队列的末尾来入队。或者你可以通过从队列的前端拿出东西来出队。这样,你总是取出最旧的项目。”
弗兰克试图想象总是从栈的底部取盘子。“那怎么取?”
“这就是数据结构的工作方式。看看三明治队伍,它就是一个队列。现在队伍里有四个人,最前面的人等得最久。”

就在赫潘教授说这话的时候,又有一个人加入了队伍。“看,他们从后面入队!”他指出。

他们站着观察着队伍,直到最前面的人拿到她的三明治并离开。

“然后从前面出队,”教授高兴地说道,“这个食堂需要更多的队列。每个食堂都需要更多的队列。”
弗兰克回想起土豆泥的堆叠方式,意识到教授是对的。数据如何存储,确实会对它的访问方式产生重大影响。像土豆泥这种情况,顺序是很重要的。
尽管这个看似简单的启示,弗兰克却花了好几天才将队列融入到食堂的运营中。盘子和碗相对容易解决。他只需将旧的堆叠起来,并把新的盘子滑到下面。说服厨师们改变舀食物的方式则困难得多。厨师们非常喜欢舀起一大勺土豆,微笑着看着土豆团啪嗒一声掉入碗中。弗兰克最终建议了一种双碗方法,将旧的土豆舀到新碗的上面。虽然这不完全算是队列,但保留了舀土豆的乐趣,而且旧食物不会被埋在底部。
不幸的是,当他代替生病的面包师时,灾难降临了。由于没有注意到面包是分批烤制的原因,弗兰克坚持认为后面的面包被最后放进烤箱是不公平的。他设计了一个旋转方案,每隔 25 秒插入一个新的面包,旋转烤箱中的所有面包,并移除最老的面包。
如果烤箱有两个门,一个在前,一个在后,弗兰克的烘焙队列尝试或许能成功。不幸的是,食堂使用的是一个旧款单门烤箱,这使得面包的旋转进出变得非常困难。虽然持续的旋转确保了所有面包有更一致的烤制时间,但弗兰克发现自己跟不上烤制的进度。很快,浓烟从炉膛中冒出,面包变黑了。
当其他厨师拿着水桶冲向火堆时,弗兰克呆呆地盯着焦黑的面包。随着他意识到队列可能并不是解决每个食堂问题的答案,绝望的迷茫感悄悄袭来。他仍然有很多关于数据结构的知识需要学习。
警察算法 101:栈和队列
来自德雷克教授讲座的摘录
栈和队列是两种简单的数据存储结构。乍一看,这两种数据结构似乎不过是数值的列表。然而,它们的区别在于数据是如何被插入和移除的。
栈是一种先进后出(LIFO)的数据结构,操作方式类似于你在每个办公室桌面上看到的文件堆。新元素被压入栈顶,而元素则通过从栈顶弹出的方式移除。如果五个元素按顺序 1、2、3、4、5 被压入一个空栈,它们将按相反的顺序被弹出,5、4、3、2、1。当然,一旦你的文件堆被清空,你的队长就会给你更多的文件工作。
你可以使用数组和一个单独的变量来实现栈,并跟踪对应栈顶的索引。当你将一个新元素压入栈时,你将其添加到数组中的下一个空槽位:索引 = top + 1。你也需要相应地增加栈顶索引。

当你从栈中弹出一个元素时,你可以再次使用栈顶索引找到正确的元素。然后,你可以将其从数组中移除,并相应地减少栈顶索引。

当然,当你向一个固定大小的数组中添加元素时,必须小心避免超出数组的末端。
队列是一种先进先出(FIFO)的数据结构,类似于一排等待处理的嫌疑人。新元素被入队到队列的末尾,元素通过从前端出队的方式移除。如果五个元素按顺序 1、2、3、4、5 被入队到一个空队列,它们将按相同的顺序 1、2、3、4、5 被出队。
队列也可以通过数组来实现。在这种情况下,你需要跟踪两个索引——队列中的第一个和最后一个元素。当你将一个新元素入队时,你将其添加到当前最后一个元素后面,并增加后端索引。

当你从队列中出队一个元素时,你移除前面的元素并相应地增加前端索引。

当你在一个固定数组中进行入队和出队操作时,数组的前端会积累一块空闲空间。虽然你可以设计队列使其具有环绕功能,但在入队和出队操作时必须小心,避免索引超出数组的末端。
第十三章:—13—
搜索的栈和队列
Frank 甩开了脑海中关于烧焦面包的画面,重新回到眼前的困境——被困在一个充满即将燃烧的羊皮纸的小牢房里。火苗还很小,只是烧着堆积物边缘的松散纸张。但一旦大堆纸张完全着火,热量将变得难以忍受。
Socks 爬到门口,靠在门上。“门锁了吗?”他问。
Frank 咽下了好几个带刺的回答,简单地点了点头。“你能打开它吗?”他问。“这是一个老式的双销锁,应该没有那么多组合。”
Socks 摇了摇头。“没有时间了。不过我知道一个咒语可以削弱金属。这样会毁掉门,但是……考虑到现在的情况,我想这也没关系。”
他拿起法杖,立刻开始工作,嘴里念念有词,一只手在栏杆上滑动。锈斑在他手下绽放,慢慢蔓延过金属。不到一分钟后,Socks 站了回去。门看起来已经完全生锈了,虽然依然是金属制的。
“铁栏杆应该已经大大削弱了,”他说道。他退后一步,期待地看着 Frank,仿佛在说,“你现在可以随时撞开门了。”
Frank 退后了几步,打量着门。“有多弱?”他问道。“是像牙签一样弱,还是像厚木板一样弱?”
“嗯……肯定比普通金属弱,”Socks 回答。“我加了很多锈。栏杆很厚,但我想它们现在应该很弱了。”
Frank 呻吟了一声,深吸一口气,猛地冲向门,低下肩膀,撞向门。撞击使他的全身一震,但他突破了。
Frank 躺在地上,周围弥漫着锈屑和烟雾的混合物。
Socks 急忙跑到他身边。“你还好吗?”他回头看了看门,露出了灿烂的笑容。“成功了!”他一脸得意地说道。“它们真的很弱吗?感觉如何?”
“就像一英寸厚的松木,”Frank 说。“真的很疼。”
微笑稍微暗淡了一些。“哦。”
Frank 艰难地站了起来。他的肩膀疼得厉害,明天肯定会有个大淤青,但逃脱火焰死亡的短暂快感很轻松地抵消了那点痛苦。

“该走了,”他说着,走进了下一个房间。
“你记得怎么回去吗?”Socks 问。
“当然,”Frank 回答。“我们用的是深度优先搜索来找到这里。我们可以沿着栈回去。”
“栈?”Socks 一边跟着 Frank 走,一边问道。
“是的,”Frank 说道,仍然沉浸在刚才逃脱的兴奋中。“搜索很容易从它们使用的数据结构角度来看。例如,广度优先搜索使用队列,深度优先搜索使用栈。” 解释像《符号学》教材里的标准答案一样滔滔不绝地从他口中流出。
“其实,深度优先搜索有几种不同的方法来跟踪你的选择。有些人喜欢使用栈来列出未来要探索的房间,类似于广度优先搜索中使用队列的方式。我更倾向于另一种方法。”
“你可以用栈来跟踪当前路径上的房间。每当你探索一个新房间时,就将它压入栈中,表示你当前的路径。”


“当你回溯时,你将该房间从栈中弹出,回到之前的那个房间。这样,你总是知道如何回溯。我甚至给房间编号,以便更容易回溯。”

“我以为你总是直接回溯到最后的决策点,”索克斯说道。
“你实际上是这么做的,”弗兰克说。“但是把房间放在栈里会让这个过程变得容易得多。你只需要回溯,直到遇到一个有新路径的房间,再将已完全探索的房间从栈中弹出。”
索克斯看起来印象深刻。“你把我们探索过的房间都写下来了?”
“我在脑海中记住了栈的内容,用粉笔给房间编号,”弗兰克回答道。“正如我所说,这不是我第一次做深度优先搜索。我们得回溯七个房间。”

他们穿过两个黑暗的房间匆忙返回,这时索克斯才想起手中的法杖。他再次低声念出火焰咒语,蓝色的火焰从法杖尖端跃出。
弗兰克警惕地看着工作人员。“这次要牢牢把握住它,”他建议道。
经过三个房间后,索克斯突然问:“队列怎么样?”
“它们怎么了?”弗兰克问道。
“你说它们是用于广度优先搜索的。”
“它们是的,”弗兰克同意道。“你的魔法列表实际上就是一个队列。在广度优先搜索中,队列跟踪未探索的选项。你不是将当前状态压入栈中,而是将新邻居添加到队列的末尾。”

“在深度优先搜索中,你可以使用列表或栈来跟踪未探索的邻居或当前路径吗?”索克斯问道,他对于在废弃监狱里逃离不明攻击者的情境下,仍显得异常兴奋。
“无论哪种方法,如果你在记账上小心,都会奏效,”弗兰克同意道。
“我以前从未把搜索问题与堆栈和队列联系起来,”索克斯若有所思地说。“我想知道还有哪些数据结构我忽视了。我敢打赌‘解开绳结咒语’就用了几个。”
弗兰克没有理会他的胡言乱语,继续回溯到出口。他们快速行动,优先考虑逃脱而非进一步探索。简单的逻辑告诉弗兰克,攻击者早已离开。没有人试图阻止他们逃跑,而且证据已经被烧毁,犯罪分子也没什么可得益的理由再待下去。
几分钟后,他找到了最后一扇门,他们迅速冲了出去。一股细烟随他们一起飘出。此时火焰应该已经吞噬了纸堆,摧毁了所有线索。
警察算法 101:栈和队列
德雷克教授讲座摘录
高效算法的关键在于信息。我们如何组织这些信息以及所使用的数据结构,不仅对算法的效率产生重大影响,还会影响算法的实际运作。以数据结构重要性的简单例子来看,考虑之前课程中的广度优先搜索和深度优先搜索。虽然这些算法在概念上相似,但我们是否使用栈或队列来存储我们的线索列表,会显著改变搜索的进程。
在选择数据结构时,你需要小心。数据结构应该有助于算法的实现。想象一下,如果我们将一个已排序的数字列表存储在图中会发生什么。即使我们保持排序的特性,也无法对数据执行高效的二分查找,因为图结构限制了我们访问数据的方式。与数组不同,图没有索引让我们能够访问数据值。相反,我们只能通过图的边缘,沿着从一个节点到另一个节点的路径执行线性扫描。
第十四章:—14—
分头行动:并行搜索
“发生了什么?”符号警官问道,她站在大门旁边。弗兰克边喘气边打量着她。他在想,她是担心吗?还是困惑?
“我们被攻击了!”袜子急促地说。“我们被困在一个牢房里,周围都是火!但我用了一个金属削弱法术,才得以逃脱。”他说话时看起来很得意。
“攻击?”符号问道。“谁攻击了你?你看到他们了吗?他们长什么样?”
“没有,”袜子承认道。“他悄悄地从我背后接近的。”
“弗兰克?”符号问道,转向弗兰克。
弗兰克摇了摇头。“我只看到袜子朝我飞过来。”
“我敢打赌他很大,”袜子插话道。“一个巨型暴徒。而且他很隐秘,可能是个训练有素的刺客。”
弗兰克翻了个白眼。“抱歉,孩子。他是个业余的。职业刺客不会把人锁在牢房里然后逃跑。”
“但是火灾,”袜子说道。
“你的法杖引发了火灾,”弗兰克提醒道。“你把它掉在了文件上。”
“文件?”符号问道。“你找到日志了吗?你知道他们在找什么吗?”
弗兰克和袜子互相看了看。符号从一个人看向另一个人。最后,弗兰克开口了:“日志不见了。我们的年轻法师把他的法杖掉了,砰——火焰四起。任何线索都没了。”
袜子脸红得像个熟透的番茄,低头盯着地面。
“没了?”符号问道。“一切都没了?你确定?”
“是的,”弗兰克说。他朝着门口飘出的烟雾点了点头。
“那攻击你的人呢?”符号问道。
“我没看清楚他长什么样,”弗兰克说。“我猜你也没看到什么吧?”他说道。这个问题说得有些尖锐,但在经历了被攻击、被困在火海中,以及逃离黑暗监狱后,他实在不想再做任何客气。
“没有,”她平静地说。“前面没有任何东西。”
“门附近没有任何脚印吗?”弗兰克问道。“有能给我们提供线索的东西吗?”
符号摇了摇头。“什么也没有,”她说。“看起来好像好几个月没人走到另一边了。”
弗兰克点了点头,但没说话。感觉有些不对劲。要么攻击者巧妙地从符号身边溜过去,利用他们进来的门,要么她没告诉他们什么。她离开大门多久了?为什么她一直待在外面?弗兰克决定不再追问。“好了,咱们回船上去吧。”
“现在怎么办?”袜子问道,边朝水边走去。
“该回溯了,”弗兰克说道。“这里已经没有线索了。”
“回溯到哪里?”
“开放的线索,”弗兰克回答道。“我们调查剩下的线索。”他停了一下,权衡了一下选择。“我觉得现在是时候进行并行搜索了。”
“真的吗?”符号问道。
“并行?”袜子问道。
“这意味着我们分开,探索搜索空间的不同部分,”Notation 回答道。“并行算法将工作分配出去,并且同时进行——也就是在同一时间。例如,工作可能会分配给不同的人。在这种情况下,我们可以将线索分成三组。然后你、Frank 和我可以分别处理一组线索。我们可以同时调查不同的线索,这样就能使我们的工作效率几乎提升三倍。”
“但是,”Socks 反对道,“我不是警察,也不是私人侦探。我不知道该怎么办。我是不是应该和你们中的一个待在一起?”
“不,”Frank 说。“我仍然不确定发生了什么,但我有一种感觉,我们的时间有限。无论我们追踪的是谁,他们现在知道我们已经开始调查,并且知道我们已经追踪到这个地步。如果他们足够聪明,他们会开始销毁剩余的证据。”
“等我们回到 Usb 时会很晚了,”Socks 说道。
“我们今晚可以分开,明天早上在我办公室见面,”Frank 说。“这样应该有足够的时间去追踪线索,可能还可以小睡一会儿。”
“好的,”Notation 同意道。“我们如何分配工作?”
Frank 知道,高效并行算法的关键是确保使用多个工作者的好处值得分工的成本。并行化工作涉及一定的开销。问题需要被分解成多个部分。每个工作者都必须获得任务、准备好执行它,然后真正去做。最后,工作需要重新合并。并行化一个简单的任务有时比直接解决它还要更为昂贵。然而,当问题变得足够复杂时,使用并行化可以极大地加速算法的执行。
“很简单,”Frank 说。“Socks,我需要你去找你的巫师朋友们。问问他们是否知道有一个叫做‘某某联盟’的团体。船上的那些暴徒说他们以前为一个联盟工作,直到 Rebecca Vinettee 打断了他们。根据以往的案例,这个名字应该是类似‘渴望权力的疯子联盟’或者‘黑暗联盟’这样的邪恶名称。邪恶的联盟通常在名字上不会太含蓄。找出你能知道的关于这个团体的所有信息。”
“这信息太少了,”Socks 抱怨道。
“Notation,”Frank 继续说道,“我需要你调取过去六个月的所有警察转移记录。”虽然他不喜欢将这个线索交给 Notation,但她是唯一能够轻松获取这些记录的人。如果他自己去收集,肯定会遇到怀疑的目光和一堆文书工作。首都警察局处理文书工作时的效率,简直可以和设置路障相媲美。
“转移记录?”Notation 问道,显然有些惊讶。“为什么?”
“算是直觉吧,”Frank 撒谎道。“我们明天早上在我办公室见面,汇总我们的信息。”
“那你呢?”Notation 问道。她的语气中带有一丝恼怒。显然,她知道 Frank 没有把所有事情都告诉她。
弗兰克给了她一个无辜的微笑。“我得去购物。”
当TCP Flyer缓缓地返回 Usb 时,弗兰克在甲板的一个僻静角落找了个地方坐下来思考。这是他最讨厌的调查部分,当有前景的线索开始枯竭,或者在这种情况下,变得破灭时。失去一个关键线索总是让弗兰克感到一种不安——就像他总是落后一大步。弗兰克强迫自己排除心中的怀疑,重新集中精力去关注他手上的线索。前往 Usb 的航程将给他时间回顾自己所见的事物,找到那些他错过的连接。
他闭上眼睛,深吸了一口气。
“哦,抱歉。你在睡觉吗?”Socks 问。
“不,是在思考,”弗兰克说,并为自己没有大喊大叫而感到庆幸。毕竟,那个男孩救了他的命。
当 Socks 没有再说什么时,弗兰克继续问道:“你想要什么,Socks?”
“嗯... 我对这个搜索很好奇,”Socks 回答。
“怎么说?”弗兰克问。
令弗兰克失望的是,Socks 走了过来,坐在了他旁边。
“你觉得我们会找到罪犯吗?”Socks 问。
弗兰克耸了耸肩。“我们还有一些很好的线索,”他说。
“但你觉得我们会赶得及吗?”Socks 问。
警报在弗兰克的脑海里响起。他转身,狠狠地盯着 Socks。“什么时间?”
Socks 差点摔倒后仰。他的眼睛四下 darting,仿佛在寻找一个合适的答案。“他们计划的事情?”他终于结巴地说道。
弗兰克不信。“你还知道什么?”他问。
“没有,”Socks 回答。“至少,没什么确凿的线索。只是推测。不是我的——是我导师 Gretchen 的。她对这些事情有很好的洞察力。”
“哪一种?”
“我真的不应该说什么。这只是推测。”
“哪一种?”弗兰克低声咆哮道。
“她认为,不管是谁在幕后,几天后他们会攻击城堡。”
弗兰克猛地站了起来。“你为什么不早点提这个?”他大喊道。
“这只是推测,”Socks 重复道。
“除非这是完全的猜测,否则她一定有某些理由,”弗兰克说。“这是猜测吗?”
“不,完全不是,”Socks 说。“这是基于被盗的面具。魔法物品在满月时最有效,而满月就在两天后。”

“这个面具到底有什么作用?”弗兰克问,开始焦虑地来回走动。
Socks 犹豫了一下。“它是一个极其强大的物品,”他开始说。在看到弗兰克眼中的愤怒神情后,他加快了语速。“它正式叫做组合面具。几百年前的大蛞蝓战争中它失落了。大家都以为它被摧毁了,直到安娜公主在一次探险中找到了它。她找到它——”
“它能做什么?”弗兰克继续追问。
“它允许佩戴者看起来像任何其他人。学者们认为它使用了大规模并行搜索。每个特征都会进行自己的搜索,以找到最佳匹配。鼻子会变成目标鼻子的完美匹配。眼睛会变成——”
“一个完美的伪装,”Frank 提供道。
“有的,”Socks 说道。
Frank 咒骂了一声。“那城堡呢?为什么 Gretchen 认为他们会攻击城堡?”
“她没说,”Socks 承认道。“也许那部分是猜测,”他补充道,语气没有什么信心。
Frank 也不相信。
“抱歉我没有早点提到这件事,”Socks 提供道。“由于没有确凿的证据……”他停顿了一下,看起来很沮丧。
“你还有什么没告诉我们的吗?”Frank 问道,眼睛盯着 Socks。
Socks 思考了这个问题很长时间,然后回答道:“我想就是这样。”
“全部都吗?”
“我知道的一切,”Socks 补充道。
Frank 深深地吸了一口气,抬头看着帆,心里希望帆能更饱满些。过去一个小时,风力减弱了,TCP Flyer 看起来几乎没有向目的地推进。
他在脑海中回顾接下来几天的时间安排,想知道他们是否有足够的时间。即使有三个人同时搜索,也无法保证他们能够覆盖足够的区域。更糟的是,他们甚至无法在 TCP Flyer 停靠之前开始并行搜索。在此之前,他们都只能困在船上。
警察算法 101:并行算法
摘自 Drecker 教授的讲座
并行算法将问题分解成多个部分,同时对这些部分进行计算(大致上是同时),然后在所有部分完成后合并结果。它将工作分配给不同的工人,使他们比单个工人完成任务的速度更快。考虑我们最喜欢的例子:在废弃的建筑物中寻找嫌疑人。你拥有的警察越多,你就能同时检查更多的房间,也能更快找到嫌疑人。如果你有 30 个房间和 30 个警察,他们可以同时踢开所有的门。
高效并行算法的关键是将工作有效地划分成独立的单元,然后再进行合并。有些问题非常容易并行化。例如,如果你在查找一堆卷轴中的某个线索,可以很容易地将工作划分给每个工人一部分卷轴。
然而,其他算法则要难得多,甚至不可能并行化。即使你有 100 个警察,你也无法更快地审问嫌疑人。这是一个固有的串行问题。你需要根据嫌疑人之前的回答来提问。而且,也许更重要的是,嫌疑人每次只能回答一个问题。我曾经看到过 8 个警察同时大声问问题,审讯并没有因此变得更快。
另一个在并行化算法时需要考虑的方面是,效率是否值得付出额外的开销。并行算法需要额外的设置时间来划分工作,并且还需要时间来合并结果。每个任务必须分配给不同的工作者,这通常需要一定的沟通。考虑一下在一个仅包含三个值的未排序数组中进行搜索的任务。在设置完成之前,一个人可能已经能多次扫描整个数组。
第十五章:—15—
迭代加深可以救命
“我知道那个表情,”梅维斯说。弗兰克不耐烦地抬头看了看TCP Flyer号的船长。他更喜欢安静地沉思,而这已经是 10 分钟内的第二次打扰了。
“什么表情?”他咆哮道。
“那个表情,”她说着,手指着弗兰克的方向。“你在质疑你的搜索,想知道自己是不是花太多时间在死胡同上。”
“我为什么要这么做?”弗兰克问道。
“我听到那小子说的话了,”梅维斯解释道。“你现在有了紧迫的时间限制,而我们至少还有一个小时才能回到 Usb。”
弗兰克点了点头。“如果这堆破烂——”
“嘿,别这样。你只是质疑自己的搜索,不代表可以侮辱我的船。”
“嗯,我猜是吧。”弗兰克低声道歉。
他一直在脑海中回顾线索,想知道其中是否有能够提供更快答案的。虽然他知道这些日志条目是有效的线索——在这种情况下已经是他能找到的最好线索了,但它们却非常耗时。他几乎花了一整天在TCP Flyer号船的各个港口之间旅行。
梅维斯 grunt 了一声,坐到弗兰克旁边。“迭代加深?”
弗兰克耸了耸肩。这个想法曾经在他脑海中闪过。迭代加深算法介于纯深度优先搜索和广度优先搜索之间。该算法分为几轮,每一轮都是深度优先搜索,但搜索的深度有限制。
“我从来不喜欢这样,”弗兰克承认道。他一直无法忍受每次迭代都要重复搜索的一部分工作。很多时候,感觉这些工作就是浪费时间。
梅维斯笑了笑。“那你显然还没遇到足够的死胡同。”
弗兰克挑了挑眉。“你这是在跟一个私人侦探说话。我的工作就是不断地在死胡同和正确路径之间徘徊。”
“有没有因为一个死胡同丢了犯罪嫌疑人?”梅维斯问。
“几次吧,”弗兰克承认道。
“那你应该理解迭代加深的重要性,”梅维斯说。“当我第一次看到它被应用时,我也为每次重启感到烦恼。但它已经救过我不止一次了。”
“一遍又一遍地重启搜索救了你的命?”弗兰克问道。
“限制我沿着错误路径探索的距离救了我的命。”梅维斯纠正道。
“迭代加深什么时候救了你的命?”弗兰克问,语气里无法掩饰怀疑。
梅维斯凝视着远处的大海。“嗯……第一次是在我还是个孩子的时候。我在一艘叫做Void Star的货船上当学徒。那是一艘了不起的船,能装下任何东西。总之,我们在‘刀锋脊’中迷路了——那是一系列密集的火山峰,形成了一个巨大的迷宫——而且我们的重要补给已经快没了。”
“水?”弗兰克问道。
“不,”梅维斯回答道。“我们至少有两周的食物和水。咖啡快没了,对船员来说那可不是好消息。没了咖啡一天后,副船长就会变得焦躁不安,唱起忧郁的海上歌曲。”

“听起来还不算太糟。”
“没有咖啡,那人的歌声吸引了八英里范围内所有凶猛的鸟类。”
弗兰克想到这里,不禁皱了皱眉。
“总之,”梅维斯继续说道,“咖啡对船来说至关重要。船长估计我们只有不到两天的时间找到一个有补给站的岛屿。她知道附近一定有一个,但不知道具体位置。你看,我们在一场即兴的纸飞机比赛中丢失了地图。再加上山脊上弥漫着浓雾,我们根本看不见补给站,直到我们站在它的正上方。”

“我们最初是想找一个有咖啡的岛屿。我那时还很年轻,没听说过迭代加深,所以我大胆地建议使用深度优先搜索。船长只是笑了笑,告诉我她永远不会在刀锋山脊上信任深度优先搜索——那里有太多长长的死路。
“嗯。她将海洋划分成一英里见方的块状。每一英里大约是你在雾中能看到的最大距离,因此我们必须处于与补给站相同的格子里才能看到它。然后我们开始使用迭代加深来探索。我们使用了深度优先搜索,但将其限制为单个单元步长。我们采用了经典的北、东、南、西顺序,每次都回溯到起始位置。我们在第一步中没有找到任何东西,但至少我们高效地进行了探索。几个小时内,我们就排除了所有相邻的格子。”

梅维斯摇了摇头。“完全没有发现补给站的踪迹。所以我们重新开始,从原始起点做另一次深度优先搜索。这次我们探测了两步,覆盖了更多的区域。在这个过程中,我们又重新探索了相邻的格子。依然没有补给站的踪迹,但我们很快就排除了所有两步内的格子。”

“那为什么不直接用广度优先搜索呢?”弗兰克问道。“反正你们做的就是这个。你们的搜索是向外扩展,越来越远离起点。”
梅维斯点了点头。“广度优先搜索和迭代加深有很多相似之处。但你忘了一个关键点。我们丢失了地图。在没有地图的情况下,使用广度优先搜索来追踪你尚未探索的状态是非常困难的。你怎么记住你的边界?迭代加深让我们能够向外扩展,而不需要显式地记住所有未探索的状态。我们只需要遵循一个深度有限的路径。”

“我想是的,”弗兰克同意道。
“总之,”梅维斯继续说道,“那时我们的咖啡已经快喝完了。一群志愿者,包括船长本人,开始喝脱咖啡因的咖啡。但我们都知道这只能让我们多撑一会儿。我们继续前进。我们又一次重新开始深度优先搜索,这次允许我们探索得更远。”
“你是用长度为三的搜索找到的吗?”弗兰克问道。
“幸运的是,我们成功了,”梅维斯回答道。“在那次迭代中,我们检查了每一个距离一步、两步、三步的地方。那时,军需官,那个完全不需要无咖啡因咖啡的人,已经将咖啡渣重复使用了第十次,但大副已经在唱‘甲板上的海蛞蝓’了。幸运的是,那是他比较愉快的曲调之一。”

弗兰克想了想。“如果你跳过了重复的工作怎么办?如果你直接用了深度优先搜索呢?”
“我们本来会走进一条漫长的死胡同,然后咖啡也没了,”她回答道。“我不是一开始就告诉你,它救了我的命吗?”
“公平。但是那是运气问题。最近的补给站可能需要进行一次深度优先搜索,深度为五。”
“哈!弗兰克,你比这更聪明。总是能遇到幸运或不幸运的问题。迭代加深可以帮助你规避那些极不幸运的情况。它限定了你在每次迭代中能走多远。”
“其他算法也能做到这一点,”他反驳道。
梅维斯皱了皱眉。“我可没说迭代加深是唯一能救我们的算法。我只是说这是我们用的那个。而且我从那以后一直在用它。”
“有一次我甚至用了它来追踪一群愤怒的鱿鱼,避免它们在首都港口喷墨。哦,那简直会是场大麻烦。有时候我在想,我是不是应该就让它们这么做。国王的反应一定非常精彩。”
弗兰克沉思了很久,想知道迭代加深是否能在这里为他节省时间。如果早点结束搜索,他本可以回溯并追踪那些线索或神秘的联盟。但那样他就不会跟随最高优先级的线索了。
他摇了摇头。“我还是坚持用我平常的搜索方式,”他说道,最后决定。
梅维斯庄重地点了点头,望着大海。“公平。但是小心,弗兰克。你时间不多,长时间的死胡同会很昂贵。使用任何算法时,至少应该考虑如何保护自己,避免碰上最坏的情况。”
警察算法 101:迭代加深
德雷克教授讲座摘录
迭代加深是深度优先搜索的一种改进方法,它反复执行有限深度的深度优先搜索。在迭代(或轮次)k期间,算法执行一个深度限制为k的搜索。
再考虑一下从城市 A 开始寻找嫌疑人的例子。

我们从深度优先搜索开始,但在第一个节点 A 之后就结束了搜索。这相当于将我们自己限制在只搜索犯罪现场。

下一次迭代重新开始深度优先搜索,但允许它探索一座城市的距离。我们覆盖了附近的城市,访问了 A、B 和 D。

随着搜索的进行,我们不得不离犯罪现场越来越远。我们最终会在不同的搜索迭代中多次搜索附近的城市。事实上,我们搜索了 A 四次,B 三次。

虽然重复的工作增加了计算成本,但迭代加深法有其优点。它结合了深度优先搜索较低的内存需求和广度优先搜索在寻找短路径及避免陷入最坏情况问题上的能力。
第十六章:—16—
倒排索引:搜索变得更加精确
第二天早晨,随着铃声的轻响,弗兰克来到了位于首都中心的“斗篷与更多”商店。商店里几乎每一寸地方都被密密麻麻的斗篷架占据着。弗兰克穿过架子之间狭窄的缝隙,朝后方的柜台走去。
一位小个子秃顶男人透过厚厚的眼镜抬起头。“欢迎光临‘斗篷与更多’,我是吉尔伯特·克洛克斯沃斯。今天我能为您提供什么帮助?”
他上下打量了弗兰克一番。眼睛不断地扫向弗兰克那件破旧的风衣,当他注意到肩膀处的一个补丁时,店主不禁打了个寒战。
“我看你是打算买一件新斗篷了,”他带着那种世界各地高傲店主都擅长的虚假热情说道,“你来对地方了。我们刚收到了一批极好的森林绿旅行斗篷。”
“我在找信息,”弗兰克说道。他从 ArrayCart 中拿出线头,递给店主。“我需要知道这些线头来自哪件斗篷。”
克洛克斯沃斯没有动。“不是新斗篷吗?”
“只是一些信息。”

“真可惜,”克洛克斯沃斯冷冷地说,“但我猜您还是来对地方了。我是这座城市斗篷方面的顶级专家。”店主接过那根线头,仔细观察了一会儿。然后他从柜台下拿出一只大放大镜,仔细地检查了它们。
“黑色和橙色交织的十字形针脚,”克洛克斯沃斯嘀咕道,“质量还算可以。当然,比不上我的标准,但也算合理。”
“你能告诉我其他信息吗?”弗兰克继续问道,“也许有什么有用的?”
店主皱了皱眉,但继续研究着线头。“有烧焦痕迹,”他终于说道。
“是被火烧过吗?”弗兰克问道。
“不。这太复杂了。我一生中只见过几次这种烧焦痕迹。总是在巫师的斗篷上。这个斗篷上施了魔法。”
“你知道是哪种类型吗?”弗兰克问道。
克洛克斯沃斯摇了摇头。“你应该去问问巫师。我是本地区斗篷方面的顶级专家,不是魔法。”
“那颜色怎么样?”弗兰克问道,“我没见过多少斗篷是这种颜色的。你能告诉我它可能来自哪里吗?”
克洛克斯沃斯微笑道:“当然可以。我是王国斗篷方面的顶级专家。”
他转过身,从书架上抽出一本巨大的书,重重地放到柜台上。然后翻到了最后一页。
“你在做什么?”弗兰克问道。
“正在查阅《斗篷与纹章登记册,第五卷》中的这些颜色,”商店老板回答道,“你不是想知道它们来自哪里吗?”
“但是你为什么在读书的后面?”弗兰克说道,“你不应该从目录开始吗?”
克洛克斯沃思终于露出了真诚的微笑。“过去几年,在纹章学追踪方面有了巨大的进展!”他惊叹道,“传统上,我们正如你所建议的那样:扫描目录以寻找感兴趣的主题,然后翻到正确的页面——当然,使用的是二分查找。”
“确实,目录提供了对书本主题的索引。但是,目录的组织方式对于这种类型的搜索完全不合适。它按主题出现的顺序列出内容。如果你想知道接下来是什么,这种顺序很合适,但如果你正在寻找一个非常具体的主题就不行了。王国现在有超过 10,000 种披风样式!光是搜索目录就可能需要很长时间。”
“所以,阿曼达·克洛金顿,《披风与纹章登记册》的作者,也是我的个人英雄,发明了倒排索引。她追踪了重要的术语,比如披风的颜色,并将它们编入书的后面——几乎就像是第二个目录。”

“那有什么帮助呢?”弗兰克问,“她只是在重复目录里的信息。”
“是的。她重复了信息,但用了不同的顺序。她按术语的顺序组织了书后面的索引。然后,对于每个术语,她列出了它出现的页面。”
弗兰克盯着他,等待着。但店主似乎已经讲完了。“那么?”弗兰克促使道。
“你只需要查找你想要的术语,索引会引导你到相应的页面!”他惊呼道,“再也不用在目录中翻来翻去了。你的搜索就变成了查找。”
“但是你还是需要查找索引来找到正确的术语,对吧?”弗兰克问道。
“嗯,是的。但是,由于索引是按术语的字母顺序排序的,你可以直接使用二分查找。”
“如果某个条目出现在很多页面上呢?”
“你需要检查所有的。”克洛克斯沃思承认道。
“如果你要查找几个术语呢?”弗兰克问道,“比如三种不同颜色的线?”
“啊!这才是有趣的地方,”克洛克斯沃思说道,“你只需要检查它们共同的页面。你可以通过交集页面号的集合来找到这些页面。这意味着需要逐一浏览列表,找出在每一个列表中都出现的元素。如果你有足够的术语,通常可以将它们所在的页面缩小到只有一两页。”
“前几天我不得不查找一件海军蓝和皇家蓝的披风,带有木质扣子。我可以告诉你,这样的披风在世界上不多。事实上,只有一个群体使用这种组合——业余气象预报员协会。直到去年,他们还在使用海军蓝和深绿色,但是他们被迫更换了颜色。半专业气象预报员协会合理地声称,这些颜色与他们的海军蓝和浅绿色太相似了。”

弗兰克思索了一下,点了点头。“有趣的想法,”他表示认可。他立刻看到了如何将这种倒排索引应用于其他信息源。警察记录总是按日期排序。使用这种新技术,你还可以按犯罪类型或地点对其进行索引。这些索引可以使研究效率提高数倍。
“我想知道它是否会被其他书籍采用,”弗兰克沉思道。
“不太可能,”店主嗤笑道。“这个世界上很少有话题复杂到需要索引的程度。不是每个话题都像风衣学那样丰富。”
他说着,店主翻阅着索引,迅速翻动着书本。“警察风衣,”他最后说。“布尔和函数尼亚的部门使用这些颜色。首都警察局的几个部门也使用这些颜色。会计、薪资、记录和咨询标识部门,我记得是这些。设计当然各不相同。我猜这件风衣是新款,考虑到线头的状态。警察往往穿坏风衣,尤其是在咨询标识部门。”
“一件新的警察风衣?”弗兰克确认道。
“很可能吧,”克洛克斯沃斯说道。“我怀疑它是定制设计的。那些颜色 20 年前很流行,但随着浅色调的兴起,它们渐渐失宠了。真的很可惜——那时候有些风衣真是美丽极了。我曾经做过一件骑行风衣,有双重扣带——”
弗兰克打断了他,“你能告诉我关于这些线头的更多信息吗?”
店主看着他说:“除了它们可能来自四个部门中的新警察风衣,而且上面施了魔法咒语之外?”
弗兰克等待着。
“呃……不,”克洛克斯沃斯最终说道。“就这些。”
弗兰克点了点头。“谢谢,”他说。他捡起线头,转身准备离开。门一开,他听到一声轻微的倒吸气声,知道店主已经看到他风衣下摆的破损边缘。
警察算法 101:倒排索引
德雷克教授讲座摘录
倒排索引是一种计算数据结构,类似于书籍的索引。它提供了一种从目标值到数据中该值出现位置的映射。倒排索引特别有用,当一个特定的值在数据中多次出现或可能出现时。
考虑我们在二分查找讲座中的一个例子——检查会计账簿中与特定商家的交易。账簿按交易号递增排序,表示交易记录的顺序。

虽然这种排序可以帮助我们高效地找到某个交易 ID 的相关信息,但它并不能帮助我们追踪交易到特定的商家。一种选择是按商家名称重新排序条目。然而,这需要我们复制整个账簿,这可能是很昂贵的。
相反,我们可以构建一个额外的数据结构:一个倒排索引,以商户名称为键。对于每个商户,我们只需存储所有相关交易 ID 的列表。由于我们已经知道如何高效地查找任何给定 ID 的交易,现在我们可以通过在索引中进行额外的查找,从商户名称获取交易信息。

倒排索引是一个很好的例子,展示了运行时间与内存使用之间常见的权衡。一个算法可以通过增加内存使用(例如新增一个索引),来换取在新维度上显著更高效的搜索。
第十七章:—17—
一个二叉搜索树陷阱
离 Cloaks and More 不到一个街区的地方,弗兰克发现有个女人在跟踪他。尽管心中涌上了烦躁,他不得不承认她的确很厉害。她始终保持在街道另一侧,保持至少 30 英尺的距离。她更多的时候是依靠商店窗户的反射来观察他。而且她穿了一件毫无特色的旅行斗篷——一种森林绿的颜色,街上的一半人都穿着类似的颜色。
弗兰克突然停下,单膝跪地,假装在系鞋带。这是识别尾随者的第二老方法,第一种是疯狂地朝一个随机方向冲去,看看谁跟着。尽管可以说这种假装系鞋带的方式比疯跑略显低效,但它的好处在于隐蔽,最重要的是,不需要奔跑。
尾随者继续向前走,停在约 10 英尺远的地方,停下来通过一面特别光亮的商店窗户仔细观察一堆卷心菜。
弗兰克站起身,开始朝另一个方向走。走了半个街区后,他穿过街道到她那一侧,忽视了马车司机愤怒的喊声,进入了一条小巷。一旦离开了主街,他转身停下了脚步,等待着。
尾随者几乎撞上了他,她匆忙绕过拐角。
“嗨,”弗兰克说道,“你为什么跟着我?”他尽力让自己的语气听起来像是在闲聊,冷淡自然——即使在正常情况下他也做不到这一点。结果最多也不过是低吼,但他成功避免了大喊大叫。
职业间谍通常会花费相当多的时间来计划如果被发现时该如何反应。他们为各种情况制定复杂的背景故事,解释从跟踪某人到在皇宫里被发现并携带窃听设备和假海龟的所有情节。他们梦想着平稳的收场和轻松的谎言。然而,现实中很少如此顺利,惊讶的喘息反而是相当常见的。在这个案例中,弗兰克甚至曾希望能抓住一瞬间的恐慌来加以利用。
然而,正如预料的那样,尾随者依然保持专业。没有慌乱,也没有惊讶的喘息声。唯一表明出了问题的迹象是她眼中一闪而过的愤怒,紧接着她丢下一颗烟雾弹,消失在空气中。
即使没有烟雾弹,间谍也足够快,弗兰克根本追不上。等他伸手去抓她时,他已经听到她的脚步声在街道上回响。他咒骂一声,扑进烟雾中追了上去。
半个街区之内,弗兰克已决定接受另一次深度优先搜索。他追赶过足够多的小偷,知道情况会如何发展。间谍很可能很快会离开主路,试图消失在人群中。这个策略在大多数情况下并不差,但在这里行不通。这个城市的这一部分几乎没有旁道,而且大多数都是死胡同。
当他奔跑时,他将街道想象成一个图,交叉口和死胡同是节点,他需要在这些地方做出决定,街道之间的路则是图的边——将他从一个决策点带到另一个的路径。

经过快速计算,他认为自己有时间探索五六条小街道,直到落得太远,线索消失。 不幸的是,这就是用深度优先搜索追踪时的风险之一。
前两条街道证明是浪费时间。他所能发现的最接近犯罪活动的,是一群孩子在墙上涂鸦。 他们用一根烧焦的木棍写下了“团队递归”和“递归 4eva”。 他继续搜寻。
几次碰壁之后,弗兰克正在考虑放弃,这时他发现了泥地上一个通往开放排水井盖的脚印。 他靠在墙上,喘了口气。这一定是她的逃生路线。
弗兰克窥视着黑暗的洞口,但什么也看不见。 他把自己放进排水井盖,落在一个木平台上。 他低下身,尽可能地让自己变小,扫描着黑暗的房间。 平台固定在一面粗糙的石墙上,俯瞰着一个至少 50 英尺深的宽阔房间。 唯一的光线从上方的开口射入,像一个巨大的聚光灯照亮远处的地面。 当他观察时,那个间谍快速穿过了光圈,朝着对面墙跑去。 现在她已经远远领先于他。
弗兰克打量着周围的选择。 他脚下有其他平台,相隔约 20 英尺,通过铁梯相连。 当他注意到地板上嵌着一个小铜牌时,他气愤地咒骂了一声。 他站在一座二叉搜索梯子的顶部。

二叉搜索梯子最初由一个古怪的艺术画廊老板阿莱娜·布兰奇设计,用来组织画作。 它们实际上是巨大的二叉搜索树——一种旨在实现高效搜索的数据结构。 该结构像一个倒置的大树,顶部有一个单一平台,技术上称为根节点。 每个平台下面最多有两条梯子,分别通向不同的子节点——一个位于下一层的另一个平台。 整个结构逐渐分支,提供了多条路径。
对细节狂热的阿莱娜最初使用这一结构来按画作中描绘的草叶数量来组织画作。她实施了一种简单的组织方案:站在任何平台上,你可以保证所有位于左侧梯子下方(在左子树中的画作)都比当前平台的画作含有更少的草叶。而所有位于右侧梯子下方(在右子树中的画作)则含有更多的草叶。从顶部开始,你甚至可以选择向下的路径,以便找到一幅具有特定草叶数量的画作。

不幸的是,这种二叉搜索梯子在艺术界并未真正流行起来,原因既在于它们的过于庞大,又在于需要不断攀爬,但它们很快被犯罪世界所采纳并改编。二叉搜索梯子陷阱,阿莱娜创作的这一危险实现,是由年轻巫师卡蒂娅·拉德费尔(Katia Ladderfell)在为维内提家族工作时开发的。与画作不同,卡蒂娅在每个平台上放置了一个单一的数字标签,并创建了一个数字密码,允许安全通过这棵树。在设计这棵树并放置标签时,她保持了二叉搜索树的特性——任何节点的左子树中的值总是小于节点本身的值,右子树中的值总是大于节点本身的值。通过这棵新型的武器化二叉搜索树,只有一条路径是安全的,那就是通向最底层节点密码值的路径。如果你知道密码,就可以像执行该值的搜索一样下降树。每一层,你可以将密码的值与当前平台上的值进行比较,然后选择左侧或右侧梯子。因此,维内提家族的打手们只需要记住一个密码,而不需要记住一系列的梯子选择。考虑到这个组织成员的素质,这种简单性至关重要。
如果你不知道密码并选择了错误的梯子,你会触发有时致命、有时只是情感上令人创伤的陷阱。典型的危险包括被诅咒的梯子、有毒的蜘蛛、落石、飞镖枪、摆动的刀片,以及在某些情况下的魔法侮辱——入侵者会因为五层逐渐恶化的侮辱性言辞而感到沮丧,这些言辞针对的是他们的外貌、气味或智力水平。
上一次弗兰克面对维内提家族的二叉搜索梯子时,一个告密者给了他密码——数字 10。这个数字让弗兰克能够悄悄进入藏匿处,出其不意地抓住了三名维内提成员。只有丽贝卡逃脱了。
如果他知道这个陷阱的密码,他或许还有机会抓住那个间谍。
一连串的想法迅速在弗兰克的脑海中闪现。首先,为什么维内特家族的人不重用密码?他们的爪牙通常是些愚笨的人。弗兰克怀疑他们根本记不住几个数字。其次,现在几乎没有恶魔巫师了,所以这个二分查找梯子陷阱肯定是多年前建造的。在巫师 Exponentious 失败图谋推翻王国之后,次等的恶魔巫师们要么已经改邪归正,要么隐匿了起来。事实上,卡蒂亚·拉德菲尔自己已经逃离小镇去开设椰子农场了。最后,弗兰克所在的位置非常高,他的膝盖开始感到虚弱。
弗兰克瞥了一眼他站立的根节点平台上的标签:50。如果他没错,而且维内特家族确实重用了密码,那么他需要沿梯子往下查找 10。由于 10 小于 50,他需要走下左侧子树。

一边低声咒骂,弗兰克朝左边的梯子走去并开始下行。整个过程令人满意地毫无波折。没有蜘蛛。没有摆动的刀片。甚至没有一句侮辱。

在下一个平台上,弗兰克发现了一个标签,标记为 5。由于 10 大于 5,他知道这次应该走右边的路径。他跑向右侧的梯子,信心越来越足。相信他人的无能往往是有回报的。

下一个平台位于地面上方一层,约 20 英尺的高度。弗兰克注意到标签上写着 25,便立即转向左侧。
他下到一半时才意识到有什么不对劲。他听到一声轻微的尖叫声,左脚上方的梯级开始移动。他低头看去,恰好看到梯级撞到下面的横杆,导致他的左脚被梯级夹住。他惊叫了一声。当他观察时,梯级又滑上去,然后又滑了下来,给他的脚带来了一阵新的剧痛。梯子简直是在咬他。他甚至能感觉到金属齿条开始从每个梯级间突出来。
他在一瞬间做出了决定,迅速跳开,避开了梯子在他手指上咬下的一刻。他笨拙地落在了自己被咬过两次的脚上,踉跄了几步后才缓缓地跌坐在地上。
他转过身,怒视着梯子底部的标签。标签上清楚地写着 10。他走对了路。然后,他注意到附近地面上的一小段粉笔标记。上面写着“不要使用,密码已更改。”弗兰克立刻恢复了咒骂。

警察算法 101:二分查找树
德雷克教授讲座摘录
二叉搜索树是一种数据结构,它以类似于普通二叉搜索的方式组织数据。每个树节点存储一个值,并可以有最多两个子节点:左子节点和右子节点。树节点按照它们包含的值进行组织。左节点(以及所有其子节点)中的数据值将小于当前节点的值。类似地,右节点(以及所有其子节点)中的数据值将大于当前节点的值。

我们可以通过从最上层的节点开始向下遍历来高效地搜索二叉搜索树——这个节点也被称为根节点。在每一步中,我们通过比较当前节点的值与目标值,决定是探索左子树还是右子树。如果目标值小于当前值,则搜索会向左进行:

如果目标值大于当前值,则搜索会向右进行:

当目标值被找到或我们到达死胡同时,搜索结束。在后者情况下,我们可以断言目标值不在树中。
我们说,当二叉搜索树是完全平衡的时,每个节点的左子树和右子树中包含的节点数量相同。在这种情况下,每次我们大致将树中的节点数量加倍时,树的深度会增加一。
搜索的计算成本与目标值在树中的深度成正比。树越深,我们需要进行的比较就越多。
第十八章:—18—
构建二分查找梯子
“你难道不认为我们会更改密码吗,运行时先生?”一个声音从弗兰克身后传来。
弗兰克猛地转过头。间谍正悠闲地朝他走来,冷静而不慌不忙。弗兰克试图站起来,但他的左脚突然传来一阵剧痛,他只好屁股着地,绕着爬动,直到面对她。
“我没想到维内特家族居然有能力改变它,”弗兰克承认道,“如今很难找到邪恶的巫师了。我听说他们的上一个巫师现在在卖椰子。”
“确实困难,很多最优秀的邪恶巫师都逃走了,或者选择了中立的商业化位置。然而,找到帮助并非不可能。可以这么说,维内特家族已经达成了一个合适的安排。他们找到了一个愿意提供某些魔法帮助的巫师,以换取他们在其他领域的服务。”
她朝梯子挥了挥手。“坦白说,他在二分查找树陷阱上的才能不如卡蒂娅。她才是真正的艺术家。但他的作品也足够了。”
“信不信由你,你差点就下去了。你差了一个梯子。密码是 26\。显然我们不希望他更改太多梯子——只需要足够多来困住任何试图重用旧密码的人。真是差一点。”

弗兰克保持沉默,但当她没有继续下去时,他问道:“你是谁?”
“我的名字不重要。我为维内特家族工作。”
“间谍?”
她耸了耸肩。“我收集情报。随你怎么称呼。”
“你想对我做什么?”
“我们显然是想让你离开这里。”
弗兰克考虑着这件事的含义。要让他停下来,光靠咬伤的脚是不够的,而间谍肯定知道这一点。这意味着她计划要么杀了他,要么将他囚禁。尽管这两种选择都不让弗兰克感兴趣,但他总是更倾向于选择不涉及死亡的那一个。

仿佛能读懂他的想法,间谍回应道:“我原本希望二分查找梯子陷阱能完成任务,但没有。至少现在还没有。”她走到 26 号梯子旁边,抬手用掌心击打了一根梯级。一声沉闷的钟声充满了房间。接着,她又击打了两下。钟。钟。
“再见了,运行时先生,”她说道。说完,毫不回头地大步走出房间。
弗兰克困惑地看着她离去。突然,他眼角的余光吸引了他的注意。当他仔细观察时,三根梯级松动并掉落到地上。片刻之后,它们开始向他蠕动,轻轻发出嘶嘶声。蛇形梯级。
弗兰克迅速行动起来。蛇形梯级危险但很慢。如果他能到达 26 号梯子,他还有机会。他趴在地上爬行,依然不敢依赖那只脚。他用梯子拉自己站起来,身体沉重地靠在金属上。毫无疑问,这次爬升会非常疼痛。
蛇形梯级现在离他只有几步之遥了。
弗兰克烦躁地呻吟了一声,伸手开始爬梯子。与其说是爬,更像是跳跃;他必须用健康的脚跳起,拉自己上到下一个梯级。每次移动,他的左脚都剧烈地疼痛。
弗兰克爬上顶端,倒在 25 号平台上,仰躺着喘气,诅咒着二分搜索梯子。曾经他还觉得这些结构很美,甚至很优雅。他在警校时曾去过阿莱娜的几次展览,甚至参加过世界上唯一一次的二分搜索树行为艺术表演。
展览被命名为抬起梨子。阿莱娜雇佣了三位巫师,利用魔法实时抬起二分搜索梯子,将一系列画作按其中的梨子数量进行排列。那一年,梨子主题的静物画风靡一时,这一现象后来被归因于苹果作物的质量差。虽然不如次年对吐司雕塑的痴迷那般尴尬,但梨子热潮至今仍只在艺术史课程中提及,且常常是低声讨论,带着相应的厌恶表情。
七位助手,每人拿着一幅描绘梨子的粗糙画作,走进了画廊。他们按照从 1 到 23 个梨的顺序排列,画作挡在脸前,遮掩着他们因这荒谬景象而感到的羞耻。

第一位巫师走了出来。他数了数画作,找到了中间的元素。这幅画描绘了八个梨和一杯牛奶,摆在一张木桌上。
“树根上升,”巫师大喊道。画作的排列立即分成了三组。左侧是少于八个梨的画作;右侧是多于八个梨的画作。而在它们上方,是二分搜索树的新根节点。他已经做出了第一分支。

随后,另外两位巫师递归地将画作分成左右两部分。这个过程总是一样的。巫师选择中间的元素,并根据这个元素将画作分开。在他们工作时,树形结构从根节点向上升起,下面则是分支。


当时,这个展示看起来令人惊叹。现在,坐在这个武器化的数据结构上,弗兰克觉得整个概念相当愚蠢。他不禁想,自己当时是怎么觉得递归分割画作很美的。
嘶嘶的声音把弗兰克拉回了现实。蛇形铃铛的末端探出平台,四处旋转,寻找弗兰克。鉴于它只是一个活动的金属物件,弗兰克不太确定这蛇形铃铛是如何搜索的。它没有眼睛、鼻子或嘴巴,也许是通过震动来感应的。
他考虑踢掉那根蛇形梯子,但最终决定不这么做。蛇形梯子不知为何有毒,尽管它们没有嘴巴。
相反,弗兰克决定继续撤退。他挪动到向上的梯子旁,将自己拉到站立的位置。他脚踝的疼痛已经减轻,下一次的攀爬更像是一个传统的梯子体验,而不是疯狂的跳跃动作。
弗兰克直接向上爬上了下一层,回到了那个刻有 50 的小铜牌的根节点平台。
他停下来低声咒骂了一下二分查找梯子。他现在看来,这些梯子简直是愚蠢的装置。尽管没有任何实际的理由,弗兰克仍然对自己的评估充满信心。满意地点了点头,他把自己拉回到街道上,远离了那条蛇形的梯子。
警察算法 101:二分查找树
德雷克教授讲座摘录
你可以通过递归地将已排序的数组元素分割成更小的子集来创建二分查找树。在每一层,选择中间值作为该层的节点。如果元素数量为偶数,你可以选择中间的两个元素中的任意一个。

一旦根节点被创建,左子集和右子集将独立地以相同的方式进行拆分。从概念上讲,我们将已排序的数组拆分为左右数组,并在每个数组上应用相同的算法。

注意,在构建过程中,实际上并不需要拆分或复制数组。算法可以通过简单地跟踪当前分支中最低和最高值的索引,使用一个单一的数组。

第十九章:—19—
《可疑者的二叉搜索树》
Frank 一瘸一拐地回到办公室,看到 Socks 在等着。他发现年轻的巫师坐在 Frank 的椅子上,懒散地转着椅子。Frank 怒视着他,直到 Socks 意识到自己的错误。他低声道了个歉,跳下了椅子。
“你找到什么了吗?”Frank 问道。
Socks 耸了耸肩。“没什么有用的。”
“没有吗?”Frank 催促道。
“没有一个巫师听说过任何新的联盟,”Socks 快速补充道。“上一个成立的巫师联盟是‘魔法糖果商联盟’,他们是在去年为了解决劣质薄荷糖的涌入而成立的。你还记得那些餐厅提供的粗糙薄荷糖吗?最开始它们尝起来像薄荷,但过了几分钟后,嘴里却留下了松针的后味,持续六个小时。就像是在开玩笑。‘魔法糖果商联盟’解决了薄荷糖问题,接着又拓展到了巧克力和太妃糖。现在联盟里有六家糖果店和四辆小推车——”
“没有其他的吗?”Frank 打断道。
Socks 摇了摇头。“我还问了俱乐部和协会的事情,”他提供道,“唯一的新情况是‘巴贝奇维尔巫师保龄球协会’,不过他们只存在了不到一个月。显然,巴贝奇维尔的巫师没几个喜欢打保龄球的。”
Frank 叹了口气。他并没有指望从 Socks 的调查中得到什么好消息,但完全没有进展还是让他感到失望。
“那你呢?”Socks 问道。
“是的,”Frank 回答,“我有了一个新线索。”
“真的吗?那是什么?”
就在 Frank 准备回答时,Notation 警官大声砸门进来,手里抱着一大摞书。她走到 Frank 的桌前,把书堆放下去。桌子在重压下下沉了。
“过去一年的所有转账和分配账目,”她气喘吁吁地说,“现在,你能告诉我为什么我要把这些带来吗?”
“我们需要找到一个转账,”Frank 说道。
“我猜到了,”Notation 说,“不过如果你告诉我是哪一笔转账,我可以直接查出来。”
“我不知道是哪一笔转账,”Frank 解释道。这有一半是真的。即使他知道,他还是会要求 Notation 带来所有记录。他需要在搜索过程中在场,确保没有遗漏任何人。
“好,”Notation 说,“我们在找什么?”
“我们从 50 到 70 天前的任何可疑转账开始,”Frank 说。那些大约是监狱账本中被撕掉的页面的日期。“这是一个范围搜索。我们想找到所有在这一时间段内的转账。”
Notation 呻吟道。“这些请求是按照申请警官的原始位置排序的,然后按警官名字排序。它们没有按请求日期索引。我们必须逐个检查所有请求,得花好几个小时。”
“不会的,”Frank 向她保证,“因为我们会用魔法。”
Socks 惊讶地抬起头。“魔法?”他问,“我不知道什么是范围搜索法术。”
“你知道二叉搜索树,”Frank 回答。
“我是二叉搜索树的专家,”Socks 同意道,“但我不明白这有什么帮助。”
“我们可以构建一个转账请求的二叉搜索树,每个节点的值等于转账发生的时间(从现在算起)。然后我们可以在树上进行范围查询。”
“在树上做范围查询?”Socks 问道。
“为什么要使用树?”Notation 问道。“如果我们只是做一次查询,建树的时间可能比扫描数据还要长。”
Frank 耸了耸肩。“我猜最终我们会进行不止一次搜索。如果 Socks 用魔法构建了这棵树,我们可以多次搜索它。”
“但我不知道怎么做范围查询,”Socks 抗议道。
“把树建起来,我就给你看看。”
“好的,”Socks 说。“这需要一点时间;我只习惯于操作按钮,真正的物理按钮。我以前从未需要组织事实。事实看起来好像会很不稳定。我需要修改这个咒语。”
当 Socks 弯腰伏在 Frank 的桌子上,在一张羊皮纸上写下修改后的咒语时,Notation 面对面地向 Frank 询问。“发生了什么?”她问道。
“没什么,”Frank 说道。
“哦,别装了,”Notation 厉声道。“从监狱出来之后,你一直隐瞒着什么东西。为什么我们要看转账请求?你怎么之前从没提到过它们?你发现了什么?”
“就像我说的,这是个直觉。”
“我不信。你一定瞒着我什么。”
Frank 没有回答。

“明白了,”Socks 喊道。“至少我想是的。我们马上就知道了。”
Socks 转向那堆账本,开始低声念起咒语。他夸张地挥舞着手臂,完全没有必要地在纸上挥舞。随着一道闪光,一棵巨大的二叉搜索树出现在空中,树中的每个节点代表着自那次转账日期以来的天数。节点悬浮在空中,通过电蓝色的线条连接。

“现在我们进行范围查询,”Frank 说道。
“我告诉过你,我不——”Socks 开始说,但 Frank 挥了挥手示意他停止。
“我们将使用修改版的深度优先搜索,”Frank 解释道。“从最上面的节点开始,根节点,沿着树向下探索。”
“怎么探索?”Socks 问道。
“在每个节点,您需要遵循三个步骤。首先,检查节点本身是否落在范围内。如果是,比如说这里的 55 天,那么我们会将其保存到结果列表中。否则,我们忽略它。”
“等一下,”Socks 说。“我会让我们列表中的节点发出不同的颜色光。深绿色怎么样?”

“当然,随便,”Frank 回答道。“在检查当前节点之后,我们检查是否需要探索任何一个子节点。只有当它们可能包含在正确范围内的节点时,我们才会递归地探索左子树和右子树。”
“递归探索?”Socks 问道。
Frank 等待 Notation 插入她的正式定义,但她坚持沉默。Frank 叹了口气,解释道,“递归意味着我们将相同的算法应用于子问题。在这种情况下,我们对每个子节点应用相同的搜索方式。我们对它们的处理方式和根节点一样。”
“只需检查是否需要探索子节点,如果需要,按照相同的步骤进行。我们会用一个简单的测试,将当前节点的值与我们的范围进行比较。如果当前节点的值小于范围的下限,那么我们知道左子树中的所有内容都会低于我们的范围,因此可以跳过左子树。反之,如果当前节点的值大于范围的下限,我们就需要继续在左子节点上进行搜索。”
“右子树也适用相同的逻辑。如果当前节点的值大于我们范围的上限,那么我们可以跳过右子树。否则,我们会继续在右子节点上进行搜索。”
“在这种情况下,我们的范围是 50 到 70,而左子节点的值最多可以到 55,那个子树中的节点可能会落在我们的范围内,所以我们需要探索左子节点。右子节点的值可以超过 55,这也与我们的范围重叠,因此我们也需要探索右子节点。从左子节点开始。”
“现在我们有 22 天的时间,”Frank 继续说道,“我们不会把它列入我们的列表。因为左子树中的所有内容都必须小于 22,所以我们也不需要沿着这条路径继续探索。”

“我们称这为修剪探索,”Notation 补充道。“因为它就像是剪掉树枝。”
当 Frank 转过头看时,她皱起了眉头,意识到自己并不是在和他交谈,便沉默了下来。
“所以我们只探索右子节点,”Frank 说道。
“递归地!”Socks 高兴地补充道。
“是的,递归地,”Frank 干巴巴地同意道。“现在我们得到 38。它不会被加入到列表中,我们可以跳过左分支。”

“但是我们需要递归地探索右分支,”Socks 说道,他完全享受着这个新算法。
Frank 点了点头。

下一个节点没有子节点。这是一个死胡同。
“接下来怎么办?”Socks 问道。
“和深度优先搜索中的情况一样,”Frank 说道,“我们回溯并探索未被探索的路径,直到我们搜索完整个树。在这个案例中,我们已经修剪了很多路径,所以我们需要回溯到根节点。”
搜索继续沿着根节点的右子树进行。新的匹配项被添加到结果列表中,不兼容的路径被修剪,兼容的路径则递归地进行探索。


到最后,他们已经确定了几个符合目标范围的传输。Frank 专心研究着列表,寻找任何可疑的内容。
“什么都没有,”他不敢相信地低声咆哮。“这里什么都没有。”
警察算法 101:二叉搜索树
来自 Drecker 教授讲座的摘录
在二叉搜索树上的范围搜索算法类似于搜索单一值。算法从顶节点开始,递归地向下探索树。在每个节点上,它根据三个问题做出决策:
-
这个节点应该被添加到结果中吗? 当前值应该被添加到结果中,前提是它在范围内。
-
应该探索左子树吗? 如果当前节点有左子节点且当前节点的值大于范围内的最小值,算法应该递归地探索左子树。在这种情况下,左子树中可能有一个节点在范围内。
-
应该探索右子树吗? 如果当前节点有右子节点且当前节点的值小于范围内的最大值,算法应该递归地探索右子树。在这种情况下,右子树中可能有一个节点在范围内。
使用二叉搜索树进行范围搜索的优势在于,通过剪枝大量的搜索空间,你可能能够节省计算量。
考虑以下二叉树:

如果你正在搜索范围 [8, 20] 内的所有值,你只需要访问并评估 25 个节点中的 7 个节点(已访问的节点用阴影标出):

类似地,如果你在搜索范围 [70, 80] 内的值,你只需要访问并评估 25 个节点中的 6 个节点:

需要注意的是,访问一个节点并不一定意味着它会出现在结果列表中。两个示例搜索仍然需要访问范围外的节点,因为这些节点的子树可能包含范围内的值。
与单一值搜索一样,只有在你进行多个搜索时,使用二叉搜索树进行范围搜索才高效。构建二叉搜索树的成本比进行线性扫描数据更高。然而,构建树的成本可以在多次搜索中分摊,从而降低每次搜索的平均成本。
第二十章:—20—
向搜索树中添加嫌疑人
弗兰克再看了几分钟转账列表,但并未发现任何可疑之处。最近的转账目的地甚至离首都都远得很,而大多数盗窃案件发生在首都。最近的一次转账是去东斯特维尔市——50 英里远——年轻官员队长列出的“转账原因”是“持续的脚臭。”
“就这些人吗?”弗兰克问,转向标注。
“是的,”标注 defensively 地说。“这是过去一年中所有在不同站点之间调动的官员。”
弗兰克皱了皱眉。那个定义在他听来不对,感觉不完整。“那初步转账怎么办?”弗兰克问。
“是学院的人吗?”标注问。
“是的,”弗兰克说,“那学院的调动呢?”
“嗯,”标注说,“这些是试用期的初步转账,会在不同的账簿中记录。”
弗兰克缓缓点了点头,脑海中飞快地思考着。
“我可以去拿——”标注开始说。
“不需要,”弗兰克打断道。“我答应了队长今天下午会更新情况,到时候我可以拿到账簿。”
“你要去见队长?”标注惊讶地问。
“更新客户情况是私家侦探生活的一部分,”弗兰克说。
“你还可以告诉队长关于格雷琴的理论,”索克斯补充道。
“什么理论?”标注问,眼睛在弗兰克和索克斯之间转来转去。
“弗兰克没告诉你吗?”索克斯问。
“没有,”标注咬牙切齿地说,“他没有。”她的双手紧握成拳,放在身侧,但她的脸上明显写着她更想用其中一个拳头打弗兰克的鼻子。
“我的导师格雷琴认为明晚城堡会遭到袭击,”索克斯说。
“她真这么认为?”标注问道。然后她转向弗兰克,补充道,“那似乎是个有用的信息,为什么你没告诉我?”
“这只是推测,”弗兰克耸耸肩回答,但他避开了她的目光。
“我应该和你一起去见队长,”标注说。
弗兰克愣住了,他没想到会这样。除非能以“好消息!”或“你猜不到我们发现了什么!”来开场,否则人们通常都很害怕向多诺万队长报告进展。带着一个包含无果结局、未探索线索和危及生命的情况的更新去见他,简直是自找麻烦,必定会迎来一番响亮而色彩斑斓的训斥。如果他自己不需要信息,他根本不会考虑去见队长。
“我需要你跟进别的事情,”弗兰克在短短的停顿后说。“平行搜索,记得吗?”他伸手进兜里摸了摸,但只摸到笔记本、几个食品包装袋和一个空的老蜗牛壳。那只壳是他上次案件的纪念品,一个涉及打击乐队和众多噪音投诉的棘手案件。他把它从口袋里拿了出来。
“看看你能不能查到什么关于这个的线索,”他说。
“一个壳?”标注问,“这和案件有什么关系?”
“我不知道,”弗兰克支吾道,“但是玻璃盒比利或许知道。”
不情愿地,Notation 拿起了贝壳,仔细研究了起来。她翻转着贝壳,低声嘀咕:“为什么偏偏是比利?他总是找不到。”
弗兰克转向袜子,只见他盯着贝壳,神情极为困惑。“你能在我们去警局的路上保持二叉搜索树吗?”
“嗯,当然,”袜子说道,“不过直接建一个新的会更简单。”
弗兰克摇了摇头。“我们需要这些账簿才能做到这一点,而我可不想把它们拖到城市另一头去,它们看起来很沉。”
这个时候,Notation 中断了对贝壳的检查,投给他一个不友好的眼神。
五十七分钟后,弗兰克和袜子站在警察记录办公室外面。通常这段路程不到 20 分钟,但他们因为袜子前面漂浮的大型发光二叉搜索树而显著变慢。它不仅阻挡了袜子的视线,导致他摔了好几次,还引起了不少路人驻足观看并问个不停,直到弗兰克大喊:“让开!这里有危险的不稳定魔法”,这才得以平静下来。这个警告效果相当不错。

“你不需要和队长说话吗?”袜子问道。
“首先我们找到我们的转移,”弗兰克说。他从不喜欢空手去见客户。
每个警察学院的班级大约培养 20 名新警员,每个警员会被分配到王国的某个警局。因此,过去 10 个班级的初始分配账簿至少有 10 磅重。与其他转移账簿一样,它是按姓名排序的,而不是按日期排序的。
“看起来我们有几百个转移要加到树上,”弗兰克一边说,一边在文件共享室找了个空桌子。由于安保需求和大量文件工作的标准组合,没人被允许在记录库内工作。相反,所有警察局都会有一个或多个紧挨记录室的工作间,里面除了长木桌和个人学习隔间外什么都没有。
“我们不能随便加节点!”袜子大叫道。
“当然可以,”弗兰克说,“向二叉搜索树添加节点很简单。从顶部开始,向下搜索,像是在寻找元素一样,当你遇到死路时,就把新条目加到那个节点下。”
“看这个第一次转移,57 天前。我们从顶部开始。因为 57 大于根节点的值 55,我们往右走。然后我们往左走,因为 57 小于 67。接着我们再往左走,因为 57 小于 61。最后,比较 57 和 59,我们应该继续往左子节点走,但由于没有左子节点,我们无法继续。所以我们将新节点添加为 59 的左子节点。”

袜子看起来十分惊恐。
“看这里,”弗兰克说道,“我再给你演示一次。这次转账发生在 89 天前。我们先往右走,然后再往右走,再走一次右。接着,我们将它作为 91 的左孩子,因为 89 小于 91。”

“这不是我想表达的意思,”袜子坚持道。“如果树失衡了怎么办?”
“这可能会发生,”弗兰克承认道。“当你向二叉搜索树中添加元素时,结果可能会导致树失衡。不过,我们的搜索算法仍然可以正常工作。”
“但是,在失衡的树上,搜索可能会低效,”袜子反对道。
“的确是这样,”弗兰克承认道。
向树中添加节点可能会抵消平衡二叉搜索树的一个主要优势:它使得许多算法变得高效。每当你大约将平衡二叉搜索树中的节点数量加倍时,你只需要增加一个级别。这意味着,对于简单的搜索任务,比如查找一个元素,你可以加倍数据量,同时只增加一步搜索的操作。然而,袜子是对的:这种高效性只适用于树是平衡的情况。在最坏的情况下,树会形成一条长链,你必须沿着它搜索。而当你插入任意值时,无法保证树会保持平衡。
“我们必须冒这个险,”弗兰克终于宣告道。
“可是——”
“如果我们的搜索效率没有达到最佳,我可以接受这个。我愿意为此付出一点代价,而不用带着其他那些书来从头开始构建树。它们看起来很重。”
警察算法 101:二叉搜索树
德雷克教授讲座摘录
向二叉搜索树中添加节点类似于查找目标值。我们从树的根节点开始,像是查找插入的值一样向下移动。我们根据插入值是小于还是大于当前节点的值来决定是往左走还是往右走。直到我们到达一个死胡同:一个在正确方向上没有子节点的节点。此时,我们可以创建一个新节点,并将其作为(左或右)孩子。

插入单个元素的成本与树的深度呈线性关系。然而,我们无法保证树在插入新节点时会保持平衡。事实上,树很容易因为插入顺序的不同而变得失衡,出现深分支。例如,如果我们按排序顺序插入数字,所有的插入都会沿着同一条分支下去。

第二十一章:—21—
二叉搜索树的特性
“等一下,”弗兰克说道,“这不对。”
袜子刚插入完一个节点,惊讶地抬起头。“什么?”
“你刚刚插入的节点,”弗兰克说道,“它的位置错了。”
袜子盯着树看。“但是 63 大于 60,所以它应该放在右子树里。”

“但它大于它的曾祖父 61,所以应该进入那个节点的右子树。你把它放到了左子树。二叉搜索树的一个关键特性是,所有左子树中的节点都比当前节点小,所有右子树中的节点都比当前节点大。”
“我知道,”袜子低声说道。
“那么,为什么它在左子树中?”弗兰克问道。
“我犯了个错误,”袜子说道。
“你怎么没看出来 63 大于 61?”弗兰克问道。
“我...我从 60 开始的,”袜子承认道。
“什么?”
“嗯,我最近插入了节点 60...而 63 离 60 很近...所以我从 60 开始,把它放到下面。”
“你没有从根节点开始?”弗兰克厉声道。
“我以为这样会更快,”袜子说道,“我得跳过树的大部分。”
“你最终把它放到了错误的位置。你还采取了多少其他捷径?”
“有几个,”袜子承认道。
弗兰克呻吟了一声,然后,为了更有分量,他低声咒骂了一长串脏话。袜子低头看着地面,明智地什么也没说。
当他终于冷静下来后,弗兰克深吸了几口气,仔细审视着树。
“我们必须做一次穷举搜索,”他说着咬紧牙关,“如果树没有保持二叉搜索树的特性,我们就不能安全地做任何修剪。我们必须检查每一个节点。”
“嘿,”袜子突然说道,“我们在将每个节点放入树中时都要检查,为什么我们不直接做一次穷举搜索呢?”
“摊销成本,”弗兰克说道。“我希望用这棵树来进行未来的多次搜索。我怀疑 50 天到 70 天的范围不会是我们唯一要搜索的范围。随着我们得到更多的证据,可能会进行不同的范围搜索。也许我们甚至需要做一些精确的搜索。树的构建成本会在许多搜索中摊销,整体的工作量会更低——可能会低得多。摊销成本考虑的是多次搜索的总成本,因此将构建树的成本分摊到许多搜索中。”
“哦,”袜子说道,“就像我的魔法按钮树一样。”
弗兰克强忍住摇晃年轻巫师并大声喊道“当然就像按钮树一样!它们都是二叉搜索树。通过一次性的构建成本,它们都能让后续的许多查找变得更快。”相反,他只得出了一个讽刺的回答:“当然。”
“好主意,”袜子说道,“我们以后可以节省很多时间。”
“本来可以节省的,”弗兰克纠正道。
“哦,”袜子说,“对了,我弄坏了树,是吧?”
警察算法 101:二叉搜索树
德雷克教授讲座摘录
正如我们在本讲中所见,我们可以利用二叉搜索树的结构信息进行高效搜索。不仅如此,我们还可以在树中添加和删除节点。然而,每当我们改变数据结构时,确保不违反我们使用的性质是至关重要的。
对于二叉搜索树,维护二叉搜索树性质非常重要。该性质规定:(1)左节点(及其所有子节点)中的数据值小于或等于当前节点的值,(2)右节点(及其所有子节点)中的数据值大于或等于当前节点的值。如果违反此性质,我们就不再拥有一个二叉搜索树,而且在搜索过程中也无法修剪树的分支。
第二十二章:—22—
试图处理文书工作
经过两次完整的转职文件搜索后,弗兰克仍然没有找到任何可疑的人。更准确地说,他没有找到任何明显参与阴谋的人。弗兰克至少对每个人都有一点怀疑。
“嘿,标注在这里。”Socks 在他们第二次翻阅时说道。
弗兰克叹了口气。“当然,她在这里,Socks。她刚从学院毕业。这是学院警员的账册。”
“她在学校表现得不错,不是吗?”Socks 一边浏览她的转职文件,一边问道。
“集中注意力,Socks。”弗兰克说道,“记住,我们是在寻找任何可疑的东西。”
“三个最近毕业的学生转到了城堡。”Socks 说道,“也许我们应该调查其中一个。格雷琴认为——”
“不。”弗兰克摇头打断了他。他已经看过这些转职文件了,都是完全干净的。在他们三个人之间,甚至没有任何一条关于不符合规定长度的脚趾甲的警告。
“这里什么也没有。”弗兰克终于说道。当 Socks 开始抗议时,弗兰克再次打断了他。“你应该回到我的办公室。我 brief 完队长后会在那里见你,然后我们可以一起查看剩下的线索。”
弗兰克觉得他看见 Socks 脸上掠过一丝松了一口气的表情,但不确定是不是自己想象的。他知道有些新手甚至会伪装阑尾炎,做到真的动手术,只为了错过一次每周的简报。
弗兰克并没有直接上队长办公室,而是回到了记录室。队长已经把正式报告交给了他,但弗兰克自己还没来得及调查犯罪现场。也许他会幸运地找到线索。
记录员是一个名叫约翰·凯奇的新手,他不情愿地允许弗兰克进入房间,紧随其后用警觉的目光盯着他。也许是在盗窃事件发生后,车站加强了安全措施,尽管凯奇的行为可能只是由于新手的过度热忱。每个新手都会幻想着每周都有一次破案并拯救局面的机会。

在以寻找丢失的龙为借口的幌子下,弗兰克扫视着书架上的书籍。正如预期,这样规模的车站文书工作量巨大。文书工作似乎随着政府机构的规模呈平方增长,而首都警察局的警员人数比任何两个其他局的总和还要多。即使没有被盗的卷轴,这个房间也已经堆得满满的。
幸运的是,记录员们把信息整理得很好。根据国王的规定《大于 10 人以上机构的文书及其他平面工作产品存储管理》,每份文件都按照主题分类并储存。大部分的书架上专门存放着如逮捕报告、费用报告、转职文件、守卫轮班表以及噪音投诉等主题的资料。
房间本身就像一个巨大的字典树。字典树,也叫做前缀树,是一种数据结构,允许高效地对字符串集合进行查找。它在概念上类似于二叉查找树,字典树从根节点开始,向下分支。不同的是,字典树是针对字符串查找而优化的,而不是数值。每个节点根据“字符串中的下一个字母是什么?”来分割数据。因此,字典树中的每个节点可以有许多子节点,每个字母对应一个子节点。这种结构让你通过沿着字典树的单一路径查找目标字符串,高效地进行搜索,每次根据下一个字母来选择下一个节点。
弗兰克曾在一次巫师大会上看到过一个魔法字典树的演示。那个霓虹橙色的树悬浮在空中,存储着卖家携带的千种药水成分的名字。为了简单起见,字典树只显示了非空的分支。顾客可以使用字典树快速确定哪些成分有库存。例如,他们可以通过依次选择 B、A、T、N、I、P 分支看到卖家有蝙蝠草。他们还可以迅速发现婴儿爽身粉缺货,因为 BA 下的子树没有与 B 对应的分支。

记录室采用了字典树的概念并将其应用于书架。墙上排放着 26 个巨大的书架,每个书架上存放着以某个字母开头的记录。它们是字典树的第一层节点。首先是 A 架子,接着是 B 架子,依此类推。

接下来,每个书架都包含独立的架子,每个架子对应一个学科的第二个字母。这些架子构成了字典树的下一层。

因为大多数两字母组合并没有对应现有的条目,所以书架不需要 26 个独立的架子。弗兰克曾听说过记录员通过创造新主题来填补空白消磨时间的故事。显然,一位高级官员曾因建议将限速文件归档到“极速政策”下,而被队长长时间训斥,强调警察工作的严肃性。今天,Z 书架仍然缺少一个 ZO 架子。
这些架子随后被水平排列,带有标签的书挡代表字典树的第三层。
在走动时,弗兰克扫了一眼 V 架子。在他加入警察队伍的那段时间,他成功地争取到让 Vinettees 拥有一个独立主题的机会。他曾花了许多夜晚钻研 V 书架、I 架子、N 区的文件。
弗兰克停在了 D 书架,找到了 R 架子,A 区。他拿出一本关于龙类注册的书,假装快速翻阅,同时观察着房间的其他部分。
队长对犯罪的描述是准确的。整排的记录架被清空,所有与某些两字母前缀相对应的卷轴都被剥离。其他书架则完全没有受到影响。从他当前的视角,弗兰克能看到 AS、CE、EX、NO、PR 和 RO 的空架子。他在脑中列出了这些前缀。无论小偷在找什么信息,都与这些前缀相关。弗兰克得到了另一个线索。
他放下关于龙注册的书籍,声音洪亮地宣布:“好消息!在首都注册的鸽子食者很少,而且鸽子很多。至少这个可怜的家伙在我找到它之前不会饿死。”
约翰·凯奇对他投来同情的目光,但什么也没说,弗兰克从记录办公室大步走出。
警察算法 101:Tries
德雷克教授讲座摘录
Trie 是一种基于树的数据结构,允许用户根据字符串的前缀高效地查找字符串。像二叉搜索树一样,Trie 从根节点开始,并随着向下的推进而分支。在 Trie 中,每一个分支决策都基于字符串的下一个元素。因此,Trie 中的节点可以有超过两个子节点。

和二叉树一样,我们只需要包含有数据的节点。这个例子是由单词 zap、zen、zone 和 zoom 构建的。由于我们没有包含 zonk,所以在 ZON 的 K 分支下不需要有一个子树。
请注意,我们不需要在节点中存储实际的前缀;这些前缀可以从树中的路径中重建。然而,在每个节点中存储一个额外的信息位是有用的:该节点是否表示一个有效单词的结尾。这使我们能够区分插入的单词和插入单词的前缀。例如,我们可以判断单词 zoo 是否已被插入到一个同时包含单词 zoom 的树中。
搜索 Trie 类似于搜索二叉搜索树。算法从 Trie 的顶部开始,向下推进。在每个节点,算法选择与目标字符串中下一个字母对应的分支。例如,如果我们在搜索 zen,我们将从 Z 开始,沿着 E 的分支走,然后是 N 的分支。

如果没有这样的元素,我们就知道感兴趣的值不在树中。因此,如果我们在这棵树中搜索 zany,我们将在 ZA 后遇到死胡同。
在执法中,Tries 的一个出人意料的常见应用是编制可能的嫌疑人名单。你会惊讶地发现,举报人拒绝提供完整姓名,却很乐意提供前几个字母。在这种情况下,我们可以在 Trie 中搜索该前缀,并列出对应子树中的所有姓名。根据字母的数量和稀有性,这可能足够限制搜索范围。
第二十三章:—23—
最佳优先搜索:侦探最信赖的工具
多诺万队长的办公室在过去五年里变化不大。桌子依旧坐落在房间中央,表面上除了四个标有“收件”、“外出”、“归档”和“销毁”的文件篮,几乎没有东西。墙上多了一些新的表彰奖状,除了弗兰克已经认得的那一大堆。只有那些家庭照片——队长两个孩子的木炭素描画和一张全家福,才让人感觉到时间的流逝。
“坐下吧,弗兰克。”队长说,头也不抬地继续处理文件。
弗兰克看着队长办公桌前的椅子,回想起上次坐在这个办公室里的情景——他作为警察的最后一天。那时他终于积累了足够的证据,再次准备追查丽贝卡·维内特,尽管他的一些手段确实有些接近危险的随机性。可是队长并不这么看,他关注的是弗兰克对规章的公然无视,而不是新出现的证据。将记忆抛到脑后,弗兰克瘫坐在右边的椅子上,等待着。
最终,队长签署了表格,把它放进“外出”篮里,然后与弗兰克对视。“你发现了什么?”
“这远不止是几份丢失的文件。”弗兰克回答道。

队长看起来并不惊讶。“多大?”
弗兰克花了一个小时在脑海中排演这次会面的不同版本,权衡各种可能的应对方式。最终,他决定发挥自己的强项——而圆滑并不在其中。
“你为什么不先告诉我你知道什么?”弗兰克说。“你真正知道的,换句话说。没有更多的谎言,没有更多的遗漏。”
队长看起来毫不动摇。“我从没对你撒过谎,弗兰克,”他说。“我来见你时,把我知道的一切都告诉你了。”弗兰克刚想反驳,但队长举起手制止了他。“不过,后来有了一些新的进展,”他继续说,“最新的消息今天早上才传到我这里。”
“其他的盗窃事件呢?”弗兰克问道。
“除了这些,”队长说。
队长翻阅了他的“外出”文件篮,取出一个标有“交给弗兰克·运行”的信封,递给了弗兰克。“给你,”他说,“我们可以省下跑腿的时间。”
弗兰克打开信封,浏览了文件。他发现了四份事件报告——三份是关于其他警察局的盗窃事件,第四份则是关于军事车队遭遇袭击的报告。他知道这些事件,但细节让他感到吃惊。
“加速腐烂的法术?”他问道。
“我们找来了巫师确认,”队长说。“窃贼们腐蚀了档案室的门,强行闯了进去。”
“那辆护送车队呢?”
“腐烂的弩,”队长说道。“还有一辆马车的车轮——应该是后左侧的。应该在报告里有。”
“那剑呢?斧头呢?”
“你从来没去看过报告。窃贼们也用了加速生锈的法术。这并不是特别先进的魔法,但很有效。”
“这上面没有写他们偷了什么,”弗兰克注意到。“没有提到面具。”
队长挑了挑眉。“你知道面具的事?那可是高度机密。”
弗兰克耸了耸肩。“你雇我来找信息的,”他说,没提袜子只是偶然透露了这个信息。告诉客户成功的原因中有多少是因为别人犯了错,通常没有好处。
“这让人担心,弗兰克,”队长说道。“那是一个强大的神器。再加上我们收到的匿名线索,说城堡可能会遭到袭击,整个警力都处于高度戒备状态。我们已经不得不从其他岗位调人来加强安全。”
“我也听说过关于城堡的事,”弗兰克承认道。他再次翻阅了文件。“关于袭击者的线索呢?”
“我们听到了一些传闻,但没有确凿的证据。至少没有足够确凿的证据能写进报告里。”
“那你的警员呢?”弗兰克问。“你那里有任何嫌疑人吗?”
队长在回答之前仔细观察了他。弗兰克知道,如果队长没有怀疑是内鬼,他绝不会找上自己。但从一般的怀疑到列出一份简短的名单之间,仍然存在相当大的差距。
“不知道,”队长说道。“我还没有找到任何线索。盗窃案之间没有共同的特征。不同的站点、不同的值班警员,甚至不同的部门。”
弗兰克默默点了点头,心里回想着那些盗窃案。他在内部调动的事情上也曾走到过死胡同。每次事件发生时,至少有一名新的调动警员在值班,但从来没有同一名警员重复出现过。弗兰克曾短暂考虑过整个新兵班级有一个广泛的阴谋,但立刻就否定了这个想法。任何能够策划如此大规模背叛的人,应该能够组织出更加微妙的盗窃案,至少是那种不会引起一百英里内每个站点警觉的盗窃。
“那诺泰申警员呢?”弗兰克问。
队长看起来确实有些吃惊。“她呢?”
“她在你的名单上吗?”弗兰克问。
“她当时值班,所以她在我的名单上,”他说。“但她排在最后。诺泰申是个好孩子。她经验不足,但她对警察工作充满热情。”
“你知道她一直在自己调查盗窃案吗?”
队长皱了皱眉。“我想这不应该让我感到惊讶。正如我所说,她很投入。你是在哪儿遇到她的?”
“克兰诺克的农场,”弗兰克说道。“她之后一直跟着一段时间。”
“她说了?然后呢?”
“我不信任她,”弗兰克说道。
“你不信任任何人,弗兰克。”
弗兰克叹了口气。“不仅仅是这些,”他说。“如果你发现有什么异常,告诉我一声。”
“那你呢?”队长问道。“你发现了什么?”
弗兰克简洁地给了他一个总结。他告诉队长,他是如何跟随一个线索来到克兰诺克的农场,并找到一连串的线索,这些线索引领他来到了 Usb、维内特号船和丽贝卡·维内特。
听到这个消息,队长带着笑声打断了他,“又和Vinettees撞上了,弗兰克?还有Rebecca,我真惊讶你还能走得动。你把多少人关进了监狱?”
“还不够,”弗兰克说道。
“公平,”队长承认道,“你是怎么逃出来的?”
“一个巫师出现,开始扔腌鳗鱼的桶,”弗兰克说得好像这是一件再正常不过的事情。
队长瞪大了眼睛,“什么?”
“一个叫Socks的初级巫师,”弗兰克解释道,“他是名为格雷琴的巫师的学徒。显然,国王请来了几位资深巫师来协助处理此事。”
“我没听说过格雷琴,但国王请来巫师的事并不让我感到惊讶,”队长说道,“在对车队的攻击之后,国王动员了他能调动的所有人。连安妮公主也从她最近的任务中回来。她明天就会回到城堡。”
那句话透露了情况的严重性。安妮公主几乎总是外出执行任务、进行探险或关键谈判。如果她回来了,那就说明形势非常严峻。
“安妮公主要回来了?”弗兰克问道。
“她认为这次的攻击可能与不必要复杂联盟有关。”
“不必要复杂联盟?”
“这一切都归结于那个邪恶的巫师Exponentious,”队长解释道,“你知道,就是那个试图摧毁王国的家伙。”
弗兰克点了点头。他清楚地记得,当Exponentious进攻时,恐怖席卷整个王国。这些日子里,关于他征战的故事总是在篝火旁低声讲述,用来吓唬年轻的巫师和骑士们。
队长继续说道,“自那以后,他被安全地关押在王国监狱,但安妮公主担心他不是孤军作战。也许他有追随者、同伙、仰慕者,类似那种人。她一直在追踪这个神秘的新联盟,显然他们的目标是摧毁王国。到目前为止,他们一直隐藏在阴影中,只进行一些小规模的攻击。但王室很担心。”
弗兰克茫然地盯着队长。难道这就是Vinettee暴徒在Retry Loop上提到的那个联盟?如果是的话,他究竟陷入了什么样的困境?然后又有一个念头涌上心头。
“对城堡的攻击!这和安妮公主的回归有关系吗?”弗兰克问道,“如果她一直在追捕这个不必要复杂联盟的成员,他们可能会寻找反击的机会。”
“我们考虑过这个,”队长同意道,“她回来的时候,我们会再调集一百名警卫来值班。那样一来,军械库、监狱和警察局的人手就会减少。但我们不能冒险。”
“那面具呢?有人可能会利用它来潜入警卫中。”
“是的,这是一个潜入城堡的绝佳机会,”队长承认道。“即使没有面具,增加一百个新守卫也几乎无法识别所有人。但我们采取了预防措施。皇家巫师马克斯为城堡的守卫们制作了魔法身份徽章。这些徽章几乎无法伪造,如果佩戴者的名字或照片不匹配,徽章会闪烁明亮的红光。”
弗兰克的思绪飞速运转,试图寻找安全漏洞。
“继续说,”队长催促道。“你还发现了什么?”
弗兰克加快了速度,回忆起他们在 TCP Flyer 上的一天,以及他们对 Mudwall 和 Frayed Cable Island 的搜查。他描述了监狱中的袭击以及文件丢失的情况。最后,他详细说明了从线索和调动日志中得到的最新进展。
“这就是你为什么需要了解标注(Notation)的原因,”队长说。“她是最近从学院调来的。”
“这也是其中一个原因,”弗兰克承认。“她的名字出现在我的范围搜索中。”
队长思考了片刻,然后说道:“我觉得她不是那种人。直觉告诉我她是个好警察。但现在我不确定该相信谁。她本不该负责这个案件。这个任务不属于她。”
“谢谢你,”弗兰克说道。
“还有其他调动过来的人员我应该留意吗?”
弗兰克摇了摇头。“一些新兵曾在不同的犯罪现场,但没有什么共同点。没有人可能参与过不止一次盗窃。所以除非你有一整个叛徒小组,否则我觉得这就是死胡同。”
“干得不错,弗兰克,”队长说。“这是我这些年来见过的最好的最优先搜索(best-first search)示例之一。”
弗兰克笑了笑。即使在警局里,很少有人会把这种调查方式称作最优先搜索。更多时候,人们会说“我在调查事情”或者“我在跟进一些线索。”
尽管这种方法没有得到广泛认可,最优先搜索却是每个警察的必备工具,与笔记本和舒适的鞋子一样重要。在最优先搜索中,你会列出当前的所有线索,选择最有前景的一个去跟进,然后继续跟进。跟进完这个线索后,你就选择下一个最好的线索,继续调查。途中发现的任何新线索都会被加入列表。这是解决许多案件的最佳工具。
“还有别的事情吗?”队长问道。
弗兰克摇了摇头。
“好吧,”队长说。“那就继续下去。如果‘不必要复杂性联盟’真的存在并且是幕后黑手,那么我们就比我想象的还要深陷其中。弗兰克,你最好小心点。”
“我一直都做得好,”弗兰克回答。他站起身,但停了下来。“再问一个问题。你知道标注(Notation)是怎么知道 ArrayCart 的吗?”
“我不清楚,”队长说,目光越过弗兰克。“为什么不问问她?看起来她正站在我办公室门外。”
警察算法 101:最优先搜索(Best-First Search)
来自德雷克教授讲座的摘录
如果你从本课程中只记住一个算法,那应该是最佳优先搜索。这一算法将成为你在执法工作中的得力工具。你将会在每个案件中都需要用到它。当然,它也会出现在期末考试中。
最佳优先搜索是一种根据评分或评分函数选择下一个要探索的状态的算法。每个新发现的搜索状态都会获得一个分数,表示算法认为它有多好。例如,你可以根据状态包含目标值的概率(如果你能估算出来)或调查中的线索质量来标记状态。在每一步中,算法都会选择探索得分最高的状态。你可以将最佳优先搜索的操作可视化为保持一个排序过的待试状态列表。
最佳优先搜索也可以用来最小化与每个状态相关的成本,例如状态到目标的估计距离。在这种情况下,算法会选择一个能最大程度减少成本的下一个状态。
考虑一个非常简单的迷宫,你知道起点和终点的位置坐标。你可以为搜索空间中的每个状态标注一个成本,成本等于从该状态到目标的距离。例如,你可以使用曼哈顿距离——到目标的总垂直距离加上到目标的总水平距离。这个标签并不一定意味着这个状态能够带你到达目标,但它为搜索算法提供了一个信号。

随着搜索的进行,它将探索不同的状态(显示为阴影圆圈),发现新的未探索状态,并将未探索状态加入待试状态列表(用虚线圆圈包围)。在每次迭代中,搜索会根据分数或成本选择最优的未探索状态。在这个例子中,这意味着选择成本最小的状态。


一旦找到终态,我们就可以终止搜索。在这个例子中,我们只需要探索略多于一半的状态。例如,我们从未选择探索距离为 4 的第二个状态,因为我们总是有更好的选择可以先尝试。

当你在现场时,你需要确定如何优先处理线索。根据具体情况,你可能想从最新的线索或最具体的信息开始。无论如何,你应该始终优先考虑你的搜索。
第二十四章:—24—
调查的优先队列
“唐纳文船长,”Notation 闯入办公室时说。“我想亲自为在工作时间以外进行未经授权的调查道歉。但这与弗兰克一样是我的调查,如果他正在汇报—”
“那么这究竟是如何变成你的调查,Notation 警官?”船长打断道。“我记得我已经把你分配到了假冒溜溜球案件。你为什么会在克拉诺克农场调查被盗文件?”
“我在追踪一个线索—”Notation 开始说。
“你在追踪线索?”船长打断道。“我在你的任何报告中都没有看到有关 ArrayCart 的内容。”
“那天早上我突然想到的,”Notation 解释道。
“你决定追踪这个线索而不是向负责调查的侦探报告?”
弗兰克畏缩了一下。船长痴迷于遵循调查的正确程序。在船长的个人可怕违规清单中,未报告线索的错误排名高于使用启发式数据结构,低于拒绝洗澡。从 Notation 近乎恐慌的表情中,他可以看出她也在考虑同样的事情。
“我已经接近农场了,”她说。“而且—”
“那个线索是什么?”弗兰克问道。
Notation 惊讶地转向他,可能是因为他打断了船长的审问,也可能是因为她简单地忘记了他在那里。
“我想起了盗窃当晚的一些事情,”她说。“我刚刚完成了我的夜间报告,当我看到窗外有一辆奇怪的车。当时我没有想太多,因为鱼贩们总是用奇怪的车。我以为那是早晨的鳗鱼送货。”
她转向船长,眼神恳求。“这似乎是一个很大的机会,”她解释道。“我觉得这可能是一条死胡同,那辆车只是一辆送货车。在我了解更多之前,我不想报告它。”
“然后你和弗兰克一起进行了将近两天的调查,”船长说。
“我们找到了一些有希望的线索,”Notation 提供道。
“Notation 警官,”船长厉声说。“我不在乎你是否被前警长的幽灵引导。程序是有原因的。你没有遵循它们。”
Notation 盯着地面看了看。“我明白了,长官。”
“不,”船长说。“我不确定你是否明白。但你会有足够的时间去思考。在另行通知之前,你将被安排在办公桌上。”
Notation 打了个寒颤,但没有抗议。
船长转向弗兰克。“弗兰克,你还有工作要做,”他说,结束了会议。
当她转身离开时,Notation 的目光落在墙上悬挂的弗雷德里克国王肖像上。她似乎陷入了沉思片刻。
“船长,”她突然说。“你有任何优先队列吗?”
弗兰克花了一会儿时间来联系起来,但最终勉强想起了模糊的记忆。他的一位教授曾在课堂上滔滔不绝地讲述弗雷德里克国王是如何推广优先队列的。

在成为国王之前,弗雷德里克会接见并听取王国公民的投诉和关切。由于他日程紧张且民众的投诉数量庞大,他被迫制定了一个优先级排序方案。一个王子一次能容忍的投诉数是有限的。
第一王子弗雷德里克曾试过用投诉栈,先听最新的投诉,但他错过了重要的老投诉。然后他又试过用投诉队列,先听最老的投诉,但结果他错过了重要的最新投诉。最后,他采用了一种新的数据结构——优先队列——使他能够首先处理最重要的投诉。
“优先队列?”队长问道,显然被这个突然的问题弄得有些不知所措。几乎没有人敢在队长的讲座后再开口。他们只是谦虚地走出办公室,或者在某些情况下,花了接下来几个小时蜷缩在黑暗的扫帚柜里。
“数据结构,”Notation 解释道,看起来像是在自动驾驶模式。“它们就像常规队列:你入队和出队。但它们还要求每个项目都有一个优先级分数——即重要性的衡量。当你出队一个项目时,优先队列总是会给你下一个最重要的项目。”
看到队长和弗兰克两人一脸茫然,Notation 继续举了个例子:“如果我插入了四个项目,优先级分别为 1、2、4 和 3,那么我会按照 4、3、2、1 的顺序提取它们。”
“我知道优先队列是什么,”队长说道,“我们用它来存储噪音投诉列表。噪音越大,优先级越高,所以我们总是先处理最严重的情况。我听说他们也在用它来处理污水沼泽附近的异味投诉。虽然似乎那里的所有东西都有一样高的优先级——令人难以忍受。但是你的意思是什么?”
“你有吗?”Notation 问道。
队长摇了摇头,困惑仍在压抑着他的怒气。“没有多余的,”他说,“我们已经在用所有可用的优先队列——一个用于噪音投诉,三个用于不同类型的犯罪,一个用于最想要的名单,还有一个用于度假请求。怎么了?”
“最佳优先搜索,”Notation 说道。
“最佳优先搜索?”队长问道。“弗兰克告诉我他已经在使用最佳优先搜索。”
“没错,”弗兰克确认道。
“优先队列会让它更高效,”Notation 解释道。“每次我们找到一个新线索时,我们可以将其放入优先队列,并附上一个分数,表示这个线索有多重要。然后,当我们准备好去探索下一个线索时,我们从优先队列中取出一个。我们将始终获得下一个最重要的线索。”
Frank 叹了口气,摇了摇头。他足够了解船长,知道这事情将如何发展。船长擅长用他独特的方式进行教育时刻。他不会大声喊叫或骂人,而是冷静地引导新人意识到自己的愚蠢。“你之前是怎么做的?”船长用耐心的语气问道。这事儿注定不会有好结果。
“我把线索记在一本笔记本里,”Notation 回答道。“每次准备探索一个新线索时,我会翻遍所有线索,找出最好的那个。”
“那你有多少条线索?”船长问道。“假设平均来说。”
“平均而言?”Notation 想了一下。“大约在两到五个之间吧。”
“你想让我用部门的优先队列来帮你扫描两个到五个元素的列表?”如果船长用他惯常的低吼语气说这话,可能会显得不那么尖锐。相反,他冷静的耐心让这个问题显得异常刺耳,清楚地表达了他对整个讨论有多么觉得愚蠢。
Notation 脸红了。“嗯,优先队列其实也不贵……”她开口说,但话语渐渐消失。
“看,Notation,”船长说。“我同意优先队列在最佳优先搜索中很有效。会后我可能会为每个侦探配备一套新的优先队列。但你现在不需要一个。首先,你没有足够的线索。更重要的是,你根本不在这个案子里。”
Notation 的脸在讲座过程中逐渐变红,现在已经红得像是甜菜汤一样。她深吸一口气,直视着船长的眼睛,低声说:“我明白了,长官。”
Frank 感到一阵同情。Notation 犯了一个经典的新手错误,过度优化解决方案。他不得不佩服她——使用优先队列来追踪线索的想法完全合理。事实上,他这段时间一直在用优先队列。然而,她提这个问题的时机简直糟糕透了。
“Notation,”船长继续说道,“你很有潜力。你聪明、有干劲,而且直觉很好。但你得学会服从命令。不要像 Frank 这样。”
Notation 张口想要反驳。然后她瞥了一眼 Frank,露出苦笑,闭上了嘴巴,没有再争辩。她简短地点了点头,立正敬礼,随后大步走出了办公室。
“而你,Frank,得做点事。开始吧。”
Frank 转身跟着 Notation 走,连点头都懒得做。
Frank 等到他们走到楼梯前才开口。
“你知道吗……Orb 区有个巫师可以帮你做一个便宜的优先队列。他的名字是 Heaperous。事实上,我觉得这也是他最初接触数据结构的原因。名字叫 Heaperous,估计也是必然的。无论如何,这个优先队列不太好看,但能用。不过他只在早上工作,所以你得等到明天。”
诺泰森停下脚步,给了他一个怀疑的眼神。“弗兰克,为什么告诉我这些?”
弗兰克强迫自己露出最同情的表情。“我可是经历过许多队长的演讲。更重要的是,我知道在调查过程中,好的数据结构是多么宝贵。”
“如果好的数据结构这么重要,那你为什么不使用优先队列呢?”她反驳道。
弗兰克投给她一个恼火的眼神。“当然我用优先队列。我从一开始就用了。你以为我一直在脑海里记住所有线索吗?我已经老了,做不来那种事。”
“什么?”诺泰森惊叫道。“你一直都在用优先队列?为什么不告诉队长?”
弗兰克对此笑了。“你有很多东西要学,菜鸟。首先,你永远不要在队长大发雷霆时打断他。曾经有个侦探因为打断了队长关于豆腐的漫谈,被安排了一个月的文职。我告诉你,那根本不是一场什么精彩的演讲。他一直在夹杂着豆腐的软乎和没味道之间反复说。”
诺泰森盯着弗兰克,显然不知道该说什么。
“重点是,”弗兰克接着说,“有时候你得亲自动手。如果优先队列能帮上忙,那就别等采购流程了,直接去买一个。”
诺泰森考虑了这些建议。最终,她点了点头。“我想,买我自己的设备应该不会违反任何政策。谢谢你,弗兰克。”
诺泰森脸上的兴奋几乎让弗兰克感到内疚。镇上的任何一家魔法商店都能做出优先队列,而且大多数的价格会和赫帕罗斯的店相匹配。但赫帕罗斯位于市区最远的轨道区,而弗兰克需要确保诺泰森再待一段时间,免得她乱来。
警察算法 101:优先队列
德雷克教授讲座摘录
在你在警察局的职业生涯中,所有你会遇到的数据结构里,我敢保证,优先队列是最有价值的。像栈和队列一样,优先队列也是一种数据结构,允许你插入数据并按特定顺序移除。栈和队列通过插入元素的顺序来决定顺序,而优先队列则按优先级从高到低排列数据。下一个被移除的元素是队列中优先级最高的元素,无论它是何时插入的。
每个插入优先队列的项都必须有一个优先级或分数。这可以是元素本身的值,或者是由其他函数计算出的值。
考虑这个基于噪音投诉严重程度进行优先排序的例子。如果你按以下顺序插入投诉:
“指数化浓缩咖啡馆的人群” (score = 3)
“螃蟹夹海上歌谣比赛” (score = 6)
“斯温森农场主的兔子” (score = 1)
“斯温森农场主的公鸡” (score = 5)
“斯温森农场主” (score = 7)
你可以从优先级队列中检索它们:
“斯文森农夫”(得分 = 7)
“螃蟹的夹子海哨比赛”(得分 = 6)
“斯文森农夫的公鸡”(得分 = 5)
“指数表达式咖啡馆的人群”(得分 = 3)
“斯文森农夫的兔子”(得分 = 1)
请注意,不能保证数据在优先级队列内部会被排序,只能保证按顺序提取。正如你将在后续讲座中看到的,称为堆的数据结构是实现优先级队列的一种高效方式,它不会保持数据完全排序。
首都的车站使用各种优先级功能。正如你所预料的那样,最受争议的队列是度假优先级队列。这个队列仅按照官员未使用的假期天数排序。尽管之前有过请求,但并没有为度假地点的美好程度提供额外的优先级。即将到来的前往冰川、海滩和沼泽的旅行被同等对待。相反,队列根据公平度量来确定优先级。这有助于确保下一个休假的官员是今年休假天数最少的那个。
第二十五章:—25—
锁匠开锁优先队列
一小群恶棍正等在 Frank 办公室门外。他们试图伪装自己,但 Frank 在一条街外就发现了他们。其中一个坐在长椅上,假装看报纸,但眼睛不停地扫视着街道。其他三个站在角落附近,大声争论着最近的一场体育比赛。Frank 走近时,意识到他们讨论的竟是完全不同的体育项目。一个恶棍在抱怨上一次皇家马球比赛的裁判,另一个插话讨论即将到来的赛马比赛,第三个则只是时不时地说“体育”这两个字。
只有间谍成功地保持了低调。她正站在街对面,悠闲地靠在墙上。如果他之前没有追过她,Frank 可能完全没有注意到她。她很厉害,或者更准确地说,恶棍们实在太差劲了。
Frank 没有停下脚步,他转身并走进了一条旁边的街道。他不能回办公室,那里有一队 Vinettee 的恶棍在等着他。经过片刻思考,他决定去一个旧的警察安全屋。那儿很近,而且已经很多年没人用了。如果他够幸运,或许还能记得锁的密码。

Frank 走了半个街区,听见身后传来喊声和急促的脚步声。间谍一定是已经通知了其他人。
“Frank!”一个特别壮硕的恶棍喊道。“我们只是想谈谈。”
就在这个声明本就足够荒谬的时候,其他恶棍的笑声更是打消了任何怀疑,说明他们的意图不简单。Frank 开始加速跑步,身后的脚步声紧随其后。
Frank 猛地左转,进入了一条狭窄的小巷。他对这片城区很熟悉,确信自己能甩掉追兵。他只需要足够的时间进入安全屋。如果幸运的话,他可能一试就记得密码。
Frank 从小巷出来,走到旗街,迅速做了个 U 型转弯,进入了街角的商店。他假装在浏览婴儿衣物架子,同时通过窗户观察外面的情况。Tike’s Tunics 的衣架提供了一个理想的观察位置。
一分钟之内,恶棍们涌到街上,站成一圈,看上去一脸困惑。间谍跟着出来,开始大声下达命令。她把恶棍们分成两组,分别朝街道两端走去——每边一组——然后她在小巷口等待。
Frank 没有浪费时间。他迅速穿过商店,从一扇通向小巷的后门出去。间谍离他只有 10 英尺远,背对着他。Frank 尽量悄悄地撤退,沿着小巷走向安全屋。如果他足够幸运,而且恶棍们没有想到去问店主有关他的事,那他就争取到了一些时间。
在安全屋的门前,弗兰克摸索着组合锁。他先转到 1,再转到 1,然后最后再转回到 1。说实话,这个密码很简单。锁却没有打开。
弗兰克咒骂了一声。肯定是有人在他上次来这里之后更改了组合密码。他考虑了自己的选择。他可以尝试找到密码,这可能会花很长时间,或者他可以找到另一个藏身之处。由于想不到任何不需要再次经过恶棍的安全地方,他又转向了锁。
由于时间紧迫,弗兰克需要提高效率。锁需要三个数字,每个数字在 1 到 20 之间,因此他面临着 8000 种可能的组合。他没有时间进行广度优先搜索或深度优先搜索。相反,他不得不依赖有限的最佳优先搜索和一些大胆的猜测;他得相信自己的直觉。他从口袋里拿出优先队列,擦干净后开始写下组合。每写一个组合,他就加上一个优先级——他认为成功的可能性有多大。他从一些常见的警用组合开始:
1-2-3
1-1-2
1-3-5
他给这些组合都设置了优先级 10。
接着他转向了其他相同数字的三位组合。如果密码曾经是 1-1-1,为什么不试试 2-2-2 呢?安全屋使用得不够频繁,所以保持密码简单是值得的。他列出了 19 个未尝试的三位数字组合,给它们的优先级设为 5。此时优先队列中总共有 22 种可能性。
然后他迅速回忆起负责安全屋的警官们的生日。这又增加了 6 个组合,他将它们的优先级设为 8。他又想起了其他警官的生日,并将它们的优先级设为 2。
最后,他添加了单词RUN,并将其优先级设为 1。他深知,如果他到达了这个选项,那就意味着是时候放弃了。他得找一个新的藏身地方。
现在,优先队列中有 32 个选项。结果是,一长串可能的组合等待尝试。最上面的是优先级最高的组合 1-2-3。弗兰克尝试了它,但什么也没发生。
他咒骂了一声,将组合从优先队列中移除。一个新的最高优先级组合浮现在队列顶部。
在他尝试下一个组合之前,一个念头突然闪过。他想到,或许他们使用了旧组合的某种变体?他知道有不少警官会把自己储物柜的组合密码和行李的密码反过来用。负责安全屋的警官是否也做了类似的事?弗兰克在列表底部加上了 3-2-1,优先级设为 9。
1-1-2 和 1-3-5 都没能打开锁,这也消耗了所有优先级为 10 的组合。弗兰克将它们划去,并将它们的反向组合 2-1-1 和 5-3-1 加到列表的底部。依旧使用优先级 9,他认为反向组合对人们来说容易记住。
他从列表顶部读取了下一个优先级最高的组合 3-2-1。这是他最近添加的一个反向组合。这就是优先级队列的魔力:无论你按什么顺序添加项,下一次总是能得到最优的条目。
锁在 5-3-1 时终于“咔嚓”一声打开了,这是他添加的优先级为 9 的代码之一。弗兰克松了一口气,环顾四周。仍然没有看到 Vinettees 的踪影。现在他暂时是安全的。
警察算法 101:数据结构与搜索
来自德雷克教授讲座的摘录
正如我们在整个学期的讲座中讨论的那样,我们使用的数据结构不仅会影响算法的功能,还会影响其效率。在关于深度优先搜索和广度优先搜索的讲座中,我们探讨了栈和队列的差异如何影响搜索顺序。使用优先级队列进行最佳优先搜索是另一个很好的例子,展示了数据结构的影响。
从概念上看,最佳优先搜索与广度优先搜索和深度优先搜索相似。在算法的每一步,我们都选择一个新的状态进行探索。关键的区别在于我们如何排序这些新状态的探索顺序。使用优先级队列使得我们能够高效地选择最有前景的下一个状态。最佳优先搜索和优先级队列是完美的互补,形成了一个极其高效的数据结构与算法组合。
第二十六章:—26—
搜索中的启发式
弗兰克整晚交替着回顾线索、在窗户旁警惕着 Vinettee 的暴徒,并哀叹安全屋缺乏食物。很快,他意识到,不仅仅是食物缺乏。公寓里缺少了每个警察局都有的标准设备——空白笔记本、羽毛笔和结实的家具。一旦他想到要找这些,弗兰克很快在窗户上发现了一个大大的“出租”标志。幸好警察还没有租出去或者更换密码。
几个小时后,弗兰克说服自己,Vinettees 应该找不到他。他放弃了窗户,开始在空荡荡的公寓里踱步,集中精力思考案子。“何时”和“何地”很简单——线索表明明晚将会对城堡发动攻击。不幸的是,除了“何时”和“何地”,弗兰克依然没有答案。特别是“谁”、“为什么”、“怎么做”以及“我能偷偷出去找食物而不被 Vinettees 看到吗”这些问题,依旧是悬而未解的重要问题。
在努力填补剩余细节的一个小时内,弗兰克开始怀疑起了“何时”和“何地”。对城堡的攻击似乎太过明显,警察已经做好了准备。甚至连 Socks 也在告诉他认识的每一个人要注意这件事。

弗兰克停下脚步,骂了声脏话,顿时意识到了事情的真相。Socks 肯定参与其中。带着熟悉的我早知道不该相信任何人的感觉,弗兰克在脑海中回放了过去几天的事件,这次他认出了其中的迹象。他本该在 Socks 的手下“偶然”点燃牢房里的文件并销毁证据时就发现问题。他本该意识到,是有人泄露了他去斗篷店的消息给了间谍。他本该至少对那次荒谬且恰到好处的救援——用腌鳗鱼桶——产生疑问。但最重要的是,从 Socks 错误地插入二叉树节点的那一刻起,他就应该绝对确信。没有哪个二叉树专家会不小心犯这种错误。虽然他一直有些怀疑,但说到底,弗兰克总是怀疑每个人。
这一发现让他有了更多的疑问。“何时”和“何地”再次没有答案。如果 Socks 一直在给他们提供虚假的信息,弗兰克不得不重新审视一切。巫师们要做什么,他们又怎么做呢?知道“为什么”也不错,但弗兰克发现,每当他破坏了精心策划的阴谋时,作案者总是会在没有任何挑衅的情况下滔滔不绝地说出“为什么”。到此时为止,他也已经放弃了偷偷外出找食物的念头,暂时把这个问题搁置在了一个咕咕叫的肚子里。
“面具是怎么回事?”Frank 自言自语。如果小偷们计划攻击这座城堡,他们还会用到面具吗?或者 Marcus 的身份证明是否会使其失效?他们是否只是需要它来闯入警察局?小偷们究竟想要什么记录?Frank 开始在笔记本上列举这些问题。很快,问题的数量超过了线索。
Frank 思考着他的下一步。由于时间非常有限,他需要深入使用启发式方法——这些经验法则能帮助算法朝正确的方向推进。例如,当寻找丢失的乌龟时,Frank 使用了常见的“先检查附近”这一启发式方法,因为乌龟行动缓慢。当他要在车站找到最新的咖啡时,他依赖“检查最满的锅”这一方法,因为那通常是最新冲泡的。而当他在一个陌生的城市寻找一座高塔时,“先朝塔的方向走”通常能让他在绕过几次死胡同后顺利找到目的地。启发式方法并不完美,但它们提供了有用的信息。

在警察局的日子里,Frank 逐渐信任了一个优于其他所有方法的启发式规则:首先追踪最具体的线索。具体的名字和实物证据总是比一般的怀疑和谣言更有价值。
这是 Frank 唯一一次在职业生涯中忽略的启发式方法,当时 Glass Box Billy 提供了关于即将发生的抢劫案的多个线索。首先,Billy 告诉 Frank 逃逸车的确切等待位置、型号和车轮的吱吱声频率。其次,Billy 传递了一则谣言,这是他在观看一场飞镖比赛时听到的,传闻中 Rebecca Vinettee 亲自参与了此次抢劫,而目标与鱼有关。
Frank 忽视了所有好的警察算法,决定直接追查 Rebecca Vinettee。他知道在他们装载逃逸车之前,她会消失,可能会用另一条路返回藏匿处。他必须在她消失之前抓住她。他在距离逃逸车位置仅两个街区的首都鱼类仓库附近守候。
正如队长后来会大声解释的那样,鱼类仓库恰好是在错误的方向走了两个街区。另一方面,Orb Emporium 离逃逸车的地点只有四分之一街区远。一个完全与 Vinettee 家族无关的团伙偷走了 64 颗高质量的圆形玻璃球和 2 颗原型立方体玻璃球,将它们装上逃逸车,然后带着车轮发出和 Glass Box Billy 描述的同样令人烦恼的吱吱声驶离。Frank 对 Billy 提供的线索的描述,以及他坚持增加一个新的启发式方法“总是怀疑 Vinettee 家族”的观点,并没有说服队长。
然而,在当前的情况下,Frank 已经几乎没有任何模糊的线索了。他已经用尽了大部分具体的线索,进入了推测和怀疑的领域。如果他想取得进一步的进展,他需要更多的信息。他转向了自己第二个最信任的启发式方法:当遇到死胡同时,收集更多的信息。他需要更多了解面具的情况,了解它如何使用,以及有哪些魔法防御可以阻止它。在这种情况下,这意味着他需要找到一位专家。
警察算法 101:启发式方法
德雷克教授的讲座摘录
启发式方法是帮助算法指引正确方向的经验法则。虽然你无疑会听到一些警员将启发式方法视为随机猜测,但你也会看到这些警员会依赖过去曾经有效的技巧和经验法则。重要的是要意识到,启发式方法和所有信息一样,其质量是不同的。
启发式方法最清晰的例子之一是在物理世界中的导航。无论你是在迷宫中漫游,还是在陌生的城市中寻找,或者仅仅是要找到去食堂的路,你都会发现自己使用启发式方法来指导搜索。当面对两条路径时,你会选择哪一条先走?一个常见且通常可靠的启发式方法是根据简化的距离衡量优先选择选项。我最喜欢的是使用“鸟飞的距离”来衡量:如果没有任何障碍物,目标有多远?实际上,这个启发式方法意味着我总是选择看起来能让我更接近目标的路径——至少从一开始就是朝着正确的方向走的路径。我可能会遇到一些死胡同,但总体而言,我发现这个方法是一个不错的启发式方法。
当然,也有许多糟糕的启发式方法。那些在没有充分验证启发式方法的情况下使用新方法的警员,可能会陷入深深的麻烦。几年前,一名年轻的警员创造了一个特别糟糕的启发式方法。在破获了一起走私案件后,他开始认为所有的调查都必须从码头开始。问题是,这个启发式方法是错误的。它并没有帮助他将调查引向正确的方向。事实上,它经常直接把他带到了死胡同。经过 18 次失败的调查后,他的队长把他安排去永久性地巡逻码头。
启发式方法不应该是随机的猜测。它们需要包含一定量的有用信息,并且要针对正确的问题量身定制。
第二十七章:—27—
政治与学术中的堆积
第二天一早,弗兰克悄悄从安全屋溜出来,穿过城镇前往警察学院。一到校园,周围是警察、学员和退休警员,他感到自己放松了下来。他甚至在穿过校园广场前往学院教职工办公室大楼时,露出了灿烂的笑容。
弗兰克已经好几年没进这座大楼了。通常,教授们都保持着开放的办公室政策,允许学生随时进来问问题。实际上,很少有学生利用这种机会,大多数人更喜欢等到考试前一天才意识到自己什么都不知道。弗兰克通常也拖得更久,直到坐下来参加考试时才发现自己的无知。
快速浏览一下教职员工名录,弗兰克发现卢普博士占据了大楼顶层的唯一办公室。他并不感到惊讶。那座教职工大楼的独特设计使得办公室分配成了一个争议话题。在最好的时候,每一层的办公室数量都刚好是下一层的一半,这意味着往上走,不仅视野更好,每个办公室的面积也翻倍。经过多年的激烈争斗,院长最终实行了基于资历的严格办公室优先分配制度——任何办公室的使用者,必须在直接下方的所有办公室使用者中拥有更长的资历。实际上,这使得教职工大楼变成了一个资历堆积的大楼。

卢普博士,魔法犯罪学教授,已经在警察学院教授了 70 年。只有巴布尔顿博士,浮点运算教授,在教职年限上接近她,已有 61 年。
当弗兰克到达顶层时,他已经气喘吁吁,并且在想,一个 95 岁的教授是如何每天爬那么多次楼梯的。再说了,她的确有持续锻炼的好处。
“进来,进来,”卢普博士透过她开着的门喊道。“坐下,别站着,楼梯有点陡,像你这样年轻人都能喘不过气。”

弗兰克走进办公室,感激地瘫坐在卢普博士桌前的一把硬木椅子上。他又喘了几口气,卢普博士静静地看着他。
“不错的办公室,”弗兰克终于开口说道。
“真是美妙,不是吗?”卢普博士说道。“我等了 70 年才来到这里——70 年!伊特雷托教授简直不肯退休,等了好久。但我耐心等着。你知道伊特雷托教授宣布退休那天发生了什么吗?”
弗兰克摇了摇头,仍然因为气喘吁吁而无法做出恰当的回应。
“那个年轻的冒失鬼,Lambda 博士,竟然想偷走我的办公室!”
“真的吗?”弗兰克喘着气问道。
卢普博士耸耸肩。“你知道的,警察学院的退休总是充满了激情。由于我们的终身教职制度,只有资历最深的教授才能申请退休。一旦发生这种事,没人能抵挡偷偷溜进更好办公室的诱惑。”
“老实说,完全是 Iterator 教授的错。待了 75 年后,他就收拾东西冲出去,嘴里嘟囔着什么麻烦的孩子们。按照惯例,他唯一告诉的人就是离门最近的那位——Lambda 博士,他只在这里待了 11 年。”

“Lambda 博士根本不顾我们早已建立的办公室分配系统,他把自己简陋办公室的东西收拾好,直接搬了上来。哈!每次有人离开都会发生这种事。楼里最底层的教授会跑上楼来,试图占据最上面的办公室。每次都是这样,我跟你说!

“当然,一听说 Iterator 博士要离开,我就立马跑上楼去争那个办公室。它本该是我的,你看。我是唯一合法的竞争者,毕竟我已经在这里待了 70 年。但 Babbleton 博士听见我跑上楼的声音,也决定去争一争。你知道,每次都是这样。

“一旦某个办公室空了,住在下面的两位教授会立刻争先恐后地跑上来争夺这个办公室。除非有某个资助申请的截止日期临近——那时可能需要几周才有人注意到这个空办公室。在这种情况下,我和 Babbleton 博士还得与 Lambda 博士的可预测尝试竞争,争取最好的办公室。
“总之,我们在那儿——Lambda 博士、Babbleton 博士和我。我们就终身职位的规则争论了足足一个小时。Lambda 博士没有任何理由,我们都知道这一点,但他固执地坚持了好一会儿。争论最终还是集中在了已经在这里待了可怜的 61 年的 Babbleton 博士和我之间。不可避免地,我赢了——迫使 Lambda 博士搬进了我以前的办公室。Babbleton 博士则继续待在楼下的办公室里。

“Lambda 博士收拾好自己的东西,搬到了我的办公室,但可怜的他发现那里已经有两位教授在等着。他们曾住在我楼下的办公室,现在正寻找升级的机会。

“他们俩对我的旧办公室有更好的争夺权,一个在这里待了 30 年,另一个待了 40 年。这次,Lambda 博士没怎么反抗。Variable 博士赢得了那个办公室。他也确实配得上,因为他已经待了 40 年。

“幸运的是,Lambda 博士终于在下一层找到了一个机会。那里,下面的两位教授比他资历更浅。我觉得他特别高兴能胜利并把门关在了他们脸上。

“从某种意义上说,Lambda 博士算是幸运的,”Loop 博士解释道,“当他试图窃取顶层办公室时,他最终被安排到了大楼的另一侧,和一些资历较浅的教授一起。他升了整整一层楼。规则只规定,任何办公室的占用者必须比下方直接办公室中的任何人任职时间更长。所以,纯粹是运气,Lambda 博士现在有了第二层的办公室,而他的部分资深同事仍在一楼。”
Frank 礼貌地等待着,看故事是否会继续。见没有动静,他试探性地说道:“Loop 博士,如果能占用您一点时间?我有几个问题。”
“当然,”Loop 博士说道,“我猜你是来问这周的作业吧?”
Frank 犹豫了,打断了思绪。“什么?不,我不是这里的学生。”
“你不是吗?那你应该考虑加入警察部门。这是个崇高的职业。”
“我已经毕业超过 10 年了。”
“是吗?”Loop 博士又耸了耸肩,“过了一段时间,所有的学生看起来都差不多。”
“好吧,”Frank 说,拼命地想要找回思路。“对,我需要了解安全魔法。”
“哦,我不教魔法,”Loop 博士解释道,“我教的是巫师犯罪学,这是研究——”
“我上过你的课,”Frank 打断她,“我不想知道如何施法。我想知道有哪些类型的安全魔法。特别是在警察局里。”
Loop 博士的表情突然变得严肃。“那是非常敏感的信息,”她说,声音冷冰冰的,“只有少数人知道。”
“正是为了这个,我才来找您的,”Frank 说道。
“那么,你到底为什么需要这些信息?”她问道。
“我正在调查首都警察局的盗窃案,”他反击道。先是那长篇大论的故事,现在她又开始盘问他了?他没时间浪费在这些上面。
“我需要查看你的证件,”Loop 博士提示道。她做了一个招手的动作。
Frank 伸手进他的风衣,从中拿出了私家侦探的证件。他将证件丢在她的桌子上。
“私家侦探?”Loop 博士笑了笑。然后,她的语气又变得冷硬。“滚出我的办公室。”
“Loop 博士——”Frank 开口说,但在听到弩箭上弦的声音时停住了。
警察算法 101:堆
摘自德雷克教授的讲座
最大堆是一种基于二叉树的数据结构,它保持节点与其子节点之间的特殊顺序关系。具体来说,堆按照堆属性存储元素,对于最大堆来说,树中任何节点的值都大于(或等于)其下面所有元素的值。这种结构使得最大堆能够高效地支持几个重要操作:(1)高效找到最大元素,(2)移除最大元素,以及(3)插入任意元素。这三种操作使得堆成为实现优先队列的理想数据结构。

堆通常被可视化为树形结构,尽管它们很容易作为数组实现,其中数组中的每个元素对应树中的一个节点,根节点位于索引 0,如下图所示。子节点的索引是相对于父节点的索引定义的。具体来说,索引为 i 的节点的子节点位于索引 2i + 1 和 2i + 2。因此,索引为 1 的节点将有一个子节点位于索引 (2 × 1) + 1 = 3,另一个子节点位于 (2 × 1) + 2 = 4,如图所示。

另外,一些堆的实现为了简化,直接跳过数组索引 0。根节点被放置在索引 1 的位置。在这种情况下,索引为 i 的节点,其子节点位于索引 2i 和 2i + 1,这使得索引计算更加简单。无论哪种方式,这种索引方案都允许算法根据父节点计算子节点的索引,或根据子节点计算父节点的索引。
由于根节点(数组中的第一个元素)总是对应最大堆中的最大值,因此你总是可以在常数时间内找到这个值(即,无论数组中有多少个值,都在相同的时间内找到)。这使得用户能够高效地查找优先队列中值最大的项目。
如果你想添加一个元素或移除最大元素,过程会更加复杂,因为它们需要先破坏堆的性质,再恢复堆的性质。
你通过首先将新元素添加到数组的末尾(树的底层的第一个空位置)来将新元素添加到堆中。这个新值可能大于它的父节点,这样就会破坏堆的性质,因此你需要将这个节点向上移动,直到它不再大于它的父节点,并恢复堆的性质。更正式地说,如果新值大于父节点的值,你就将它通过与父节点交换位置的方式“冒泡”到上面。例如,如果我们要将 60 添加到前面的堆中,我们会将它插入到底部并向上交换两次,因为在两个层级中,它都大于它的父节点。

移除最大元素的过程类似。原始的最大值与数组的最后一个元素交换位置,使得最后一个元素成为新的根节点。

然后删除原始的最大值(当前最后一个元素)。我们现在已经删除了正确的节点,但在此过程中可能破坏了堆的性质。

从新的根节点开始,我们将该节点沿着树向下走,以恢复堆的性质。在每一层,我们将该节点的值与其两个子节点进行比较。如果它小于任一子节点,我们将新的根节点向下移动,通过与较大的子节点交换位置来恢复堆的性质。向下交换会在没有更大子节点时终止。

插入新元素和删除最大元素都要求我们在树的顶部和底部之间最多走一条路径。由于在向堆中添加一个新的节点层时,我们大致可以将堆中的节点数量翻倍,因此即使是对于大型堆,这些操作也能非常迅速。具体来说,我们可以将节点数量翻倍,并且这两种操作都只需要增加一步!此外,这两种操作还保证了树的平衡,因此未来的操作将会非常高效。
第二十八章:—28—
难解的搜索问题
“离开,”卢普博士重复道。
“我是来处理公务的,”弗兰克回答道,依然没有从椅子上动弹。他听到身后传来轻微的脚步声,显然持弩的人正在向前走。
“不太可能,”卢普博士说,“我认识多诺万队长很久了,他不是那种依赖私人侦探的人。据我所知,他特别喜欢创造新型威胁,以此来让外人远离正在进行的案件。”
弗兰克开始伸手进衣袋。
“不要做出突然的动作,”身后传来一个粗哑的声音。
弗兰克感觉到怒火上涌。他讨厌有人将弩对准自己,虽然这在他的职业生涯中是个常见的经历。“我要去拿一张羊皮纸,”他说着咬紧牙关。
“慢慢来,”声音说道,“如果你动作太快,我会开枪。”
弗兰克默默地呻吟了一声。只有一种人说话像这样——布尔人。布尔人以黑白分明的世界观,使他们在做保安时特别有效。你无法通过言辞说服布尔人脱困。你要么违反了规则,要么没有。
弗兰克在取出队长的信时动作格外缓慢。他前倾身体,花了整整一分钟将信放到桌子上。他没有后退。动作越少越好。
卢普博士研究了那张便条一会儿,点了点头示意她的警卫。弗兰克听到弩的保险卡上了。他松了口气,靠在椅背上。卢普博士挥手让警卫离开了房间。
“你让多诺万队长给你写了介绍信?”卢普博士问道。
“正如你所说,这是一个敏感话题,”弗兰克回答道。
“你大概听说过我的名声。”卢普博士笑了。
“我上过你的课,”弗兰克嘀咕道,但卢普博士并没有听到。
“所以你在调查盗窃案件。你想知道什么?”
顿时,她的态度完全改变了。她的眼睛迅速扫过弗兰克的脸,好像在分析他的每一个反应。所有的客套话瞬间消失了;她的声音变得完全是工作式的。弗兰克不禁想知道,她那番和气的闲聊究竟有多少是演出来的。魔法犯罪学是一个危险的领域,而卢普博士已经在其中生存了很长时间。
“警察局有什么魔法保护?”弗兰克问道。
“基本警报咒语,”卢普博士说道,“它们会告诉你是否有人在局内使用魔法,但不会阻止魔法的使用。”
“为什么不用魔法屏障?”
“不实际,”教授解释道,“警察局的魔法顾问太多了。他们需要能够对证据施展占卜魔法,对囚犯施展真相魔法,对档案施展魔法镜像咒语,还需要为午餐加热。”
弗兰克点了点头,回忆起关于魔法优先队列的对话。
“反正太贵了,”卢普博士补充道,“只有皇宫和监狱有比基本警报咒语更高级的保护魔法。皇宫有一堆保护魔法,但没有抑制魔法。马库斯在那儿为国王工作太多了。”
“所以有人可以在城堡里使用魔法?”弗兰克确认道。

“可以,”卢普博士回答,“但那可不明智。城堡有十几道保护咒语可以阻挡攻击性魔法。弗雷德里克国王还雇佣了六名巫师守卫。虽然他们级别较低,但足以解开基础的诅咒。还有马库斯。没有多少巫师愿意与他为敌。”
“那神器呢?”
“啊,这个问题问得好。它取决于神器。城堡当然有防御武器化神器的保护,但没有任何东西能防得了所有神器。神器种类实在太多了。由于大多数神器不需要新的魔法(因为之前已经被施过魔法),连监狱里的防魔法咒语也无法起作用。”
“什么?”弗兰克问道,“我以为监狱对所有魔法免疫。难道他们不关押邪恶的巫师吗?事实上,‘指数者’不也在那吗?他曾试图用魔法摧毁整个王国。为什么他们不干脆阻止所有魔法神器的作用?”
卢普博士干笑了一声。“抱歉让你失望了,但你不能阻止所有的神器。别担心,皇家巫师会把最强大的神器严密保护着。而且所有囚犯在到达时都会接受彻底的搜查。”
“监狱还有什么其他保护措施?”弗兰克问道,随着真相逐渐明朗,恐慌感不断加剧。
“让我们看看,”卢普博士说。她开始用手指列举。 “石墙、一条充满暴躁獾的护城河、一百名守卫、沉重的橡木门、几扇装饰性的松木门、每条走廊都有‘轻微恶心咒’、‘困难搜索咒’,还有—”
“‘困难搜索咒’?”弗兰克打断道。
“并不是新的,”卢普博士解释道,“他们在施下‘阻止咒’之前,已经在监狱里施下了许多保护性魔法。”
“‘困难搜索咒’有什么作用?”弗兰克问道,胸口涌起一股不安的感觉。
“这让找到某个囚犯的囚室变得困难。更准确地说,它会神奇地交换囚室。如果你把囚室想象成一个巨大的数组,那么‘困难搜索咒’就像是一个交换数组索引的算法。它每天午夜都会随机交换一切。”
“它让任何人都很难帮朋友越狱。没有结构的数组值让闯入者只能依赖穷举法进行搜索。而且,由于囚室每天都会被洗牌,你不能把搜索工作分几晚来做。守卫们抱怨说这种随机性很烦人。他们每天要花好几个小时点名。虽然我听说他们最近做了个游戏,叫做‘下一扇门后面是谁?’每扇门的赌注最高可以达到一枚银币。”
“他们会记录下新的囚室位置吗?”
“那样做就完全违背了目的!如果他们保持着囚犯与牢房的映射关系,就像倒排索引一样,那么你就可以直接查找囚犯。假如你能够闯入首都警察局的档案室偷走牢房分配记录,那咒语有什么用?目标是让闯入者在监狱中搜索几个小时,而这显然是不可能的。所有的守卫彼此都认识。”
弗兰克看到最后一块拼图落到位。队长曾说过,无用复杂性联盟是那个邪恶巫师指数(Exponentious)的追随者或同谋——他是皇家监狱中最危险的罪犯。大家都担心联盟再次出击,甚至可能攻击城堡。但计划远比这简单得多。联盟打算越狱;他们要解救他们的头目。他从椅子上跳了起来,庆幸带弩的布尔埃恩不再站在他身后,冲向了门口。
“谢谢,”他在下楼梯时对她喊道。
当弗兰克找到诺泰先时,他气喘吁吁,腰侧剧烈地疼痛。他几乎只能喘着气说出“需要你的帮助……越狱……今晚……巫师。”
诺泰先带着关切、兴趣和轻微的恼怒,看着他整整五分钟。“快说,弗兰克,”她终于说道。
弗兰克依然弯着腰,双手撑在膝盖上,朝她投去警告的眼神。
“是你自己跑到这里来的,”她反驳道。“我记得听到你喘着气说需要我的帮助。”
弗兰克忽略了她的评论。“我搞明白了,”他最终脱口而出。“那些巫师今晚要闯进监狱。”
诺泰先看起来很惊讶。“监狱?袜子在监狱里想做什么?”
“等等,你怎么知道袜子(Socks)的事?”弗兰克被问得愣住了。
诺泰先给了弗兰克一个困惑的眼神。“什么意思?我以为你怀疑他有一段时间了。不是吗?他可一点也不隐晦。”
“是的,确实有线索,”弗兰克含糊其辞。
诺泰先沉默了,低头盯着地面,她的脑海中飞速转动,拼接着剩下的线索。突然,她的表情变得阴沉,抬头看着弗兰克。“你为什么告诉我这些?”她问。“我已经退出案件了,记得吗?”
弗兰克瞪着她,难以置信。“你怎么能让这种事阻止你?”他问道。“难道你不想抓住那些小偷吗?”
“当然想,”诺泰先锐利地说。“但是队长说——”
“忘了队长,”弗兰克打断道。“这是你的案子,不管他说不说。不是吗?”
诺泰先看起来很矛盾,弗兰克抓住了这个机会。
“听着,”他补充道,“我需要帮助抓住小偷,但我不能在这件事上调动力量。至少现在不能。我几乎确定其中一个罪犯伪装成了警察,但我不知道是哪一个。在弄清楚这一点之前,我不能相信任何人。”
“那为什么要相信我?”诺泰先问道。
“因为你在这里,”弗兰克说道。
“这不是个好理由,”Notation 显然被这个暗示激怒了。
“不是那样的,”弗兰克挥挥手,打断了她的反驳。“我是说,因为你在这家店里,自己用钱买了一个价格过高的堆。显然你在乎你的工作。更重要的是,如果你参与了某个巫师越狱的事情,你完全可以让邪恶巫师联盟为你做一个魔法堆。要是有更好的选择,谁会走到 Orb 区呢?”
Notation 盯着弗兰克怒视。他后退了一步,看到她的表情从“非常生气”转为“你犯了个大错”。
“你告诉我来这里的,”她咬着牙说道。
“幸好如此,”弗兰克说。“我知道在哪里能找到你。这是世界上最简单的搜索问题——就像在知道索引的情况下在数组中查找值。我直接来到 Heaperous 的店里。不过我确实跑了些,”他承认道。“我以为你会早到,我不想错过你。”
Notation 看起来并没有松口气。“你把我送到这里是为了测试我是否参与了阴谋?”
“不,我是把你送到这里来把你弄开,”弗兰克承认道。“这个测试是在我跑到这里的时候才想起来的。我当时想——”
“你不信任我?”Notation 问,声音冰冷。每个字似乎都是一个独立的指控。
“别往心里去,”弗兰克说道。“我不信任任何人。”
“你……你……”Notation 哽咽着,脸色越来越红,显然无法完成侮辱的话语。
过了几分钟,弗兰克问道:“你加入吗?”
她僵硬地点了点头。
“很好,”弗兰克说道。“两个小时后在监狱见面,带上十字弓。”
Notation 又点了点头。
“还有一碗,”弗兰克补充道。
“一碗?”Notation 问,愣了一下,暂时从愤怒中解脱出来。
弗兰克露出灿烂的笑容。“监狱走廊里有一些轻微恶心的法术。你可能需要一个地方呕吐。”
警察算法 101:期末考试复习课
摘自德雷克教授的讲座
如果你从这门课中只学到了一件事,那应该是高效算法的关键是信息。在解决新问题时,花时间理解问题及其数据的结构。问题的结构越清晰,你就越有机会利用这些信息。正如你所见,在一个已排序的数组中查找一个值比在一个完全随机的数组中查找要容易得多。有时候,你甚至可以构建辅助数据结构,如堆或倒排索引,以提供所需的结构。无论如何,你的第一步应该始终是理解问题。
第二十九章:—29—
搜索终止
凌晨 12:01,门吱呀一声打开,一个警卫探出头,窥视着黑暗中的监狱牢房。他慢慢地用火把扫视着房间,直到他的目光落在睡着的囚犯身上。
“主人?”警卫轻声叫道。
囚犯动了动,坐起来,注视着警卫。
“主人,我——”警卫开口,但突然哽咽了一下,停住了。
弗兰克从监狱床上向他挥手。
警卫转身准备逃跑,但 Notation 走到门口,手持一把大十字弓。
“我……我只是做巡逻,”警卫说。
弗兰克咳嗽一声笑了笑,摇了摇头。“把火把交出来,慢慢的,”他指示道。“我的朋友刚从警察学院毕业,我听说她的十字弓使用成绩是全班第一。”
“实际上,我排第二,”Notation 在门口说道。
弗兰克叹了口气。“真是的?就这一次,你决定谦虚了?”
“对不起。我只是想准确一些。”
“这是威胁,Notation。你可以夸大威胁的程度。”
“对不起,”她重复道。
“不过,无论如何,重点不变。她有一把十字弓,而且比她所有同学中的除一个外,使用得都更好。请把火把交过来。”
警卫的眼睛四处打量,寻找逃生的机会。没有找到,他慢慢向前倾身,将火把递给弗兰克。当弗兰克伸手接过时,警卫突然转身,挥火把向弗兰克的头部砸去,火焰在空中发出嘶嘶声。
弗兰克向左倾斜,避开了燃烧的火把尖端。警卫转身准备再次挥动,但弗兰克站起身,迅速从他的手中拿走了火把,而 Notation 从后面推开了警卫。警卫踉跄了一下,跌倒在床上,姿势尴尬。
弗兰克摇了摇头。“很明显的举动,孩子,”他说。“想用火把打我。执行得很差,甚至差得离谱。虽然没打中,但我得承认你敢尝试。”
警卫呆滞地眨了眨眼,看着弗兰克。
“现在,摘下那个愚蠢的面具。我们知道是你,Socks。”
“Socks?那是谁?我从没听说过有人叫 Socks,”警卫完全不真诚地说。
“Socks,摘下面具,”弗兰克重复道。
警卫犹豫了一会儿,伸手到脖后解开了一个扣环。突然,一阵奇怪的旋转声充满了牢房,他的面容开始融化并变形成了一个复杂的面具。警卫随后将面具从脸上摘下,露出了 Socks 的面容。
“你怎么知道的?”Socks 问道。

“许多小事,”弗兰克说。“首先,你跟踪了我们,但直到 Vinettees 把我们逼到角落时才现身。他们虽然有很凶狠的名声,但在对囚犯得意忘形时并不聪明。事实上,他们更可能无意中泄露你的计划。但你的故事可怜得足够可信。”
“不过你在 Frayed Cable 岛上的表现就不那么可信了。是你丢掉的法杖毁掉了证据。你在监狱外的门前看起来很困惑,但当你自己的生命处于危险时,立刻施用了一个金属削弱的咒语。你甚至使用了加速生锈的咒语,正是那次攻击车队时用的咒语。”
“你还拒绝帮忙撬开监狱外面的锁,直到我提议让你单独爬过去,”Notation 补充道。“我猜你是想在我们找到文件时能在场。”
“那你为什么不直接 confront 我呢?”Socks 问道。
“我当时还是不确定该信谁,”Frank 承认道。他朝着仍站在门口持弩的 Notation 示意。“Notation 反应得稍微快一点。她告诉我,她在监狱事件后开始怀疑你,但当时没有确凿的证据。当我提议并行搜索时,阻止了她继续监视你。”
“老实说,我真的以为 Frank 提议并行搜索是为了暂时把你搁置一边,”Notation 承认道。
Frank 决定再也不提目标是把他们两个人都搁置在一边的事了。那时,他对 Notation 的怀疑比对 Socks 的怀疑更重。他提议并行搜索,是为了能单独跟进那些线程。
“我得承认,”Frank 继续说道,“你在整个监狱事件中的表现相当不错。我当时真的以为有人袭击了你。那时,我心里很困惑,为什么攻击者没有留下来,但我忽视了直觉。早该早点发现的。”
“不过有一件事我不明白,为什么你关了门?为什么不直接‘绊倒’然后丢掉法杖?”
Socks 耸了耸肩。“门是个意外。我并不是故意把我们锁在里面的。我的袖子卡在门上了,假装绊倒时碰到了门。”
“不过你演得不错,”Frank 说道。“而且把我们困住确实帮忙卖出了攻击的故事。”
Socks 耸了耸肩,但脸上闪过一丝骄傲。
“但最重要的是,你在二叉搜索树上的捷径,”Frank 说。“任何研究过二叉搜索树的人都知道,插入节点时应该从根节点开始。你的错误意味着你要么不是你所声称的专家,要么就是在故意破坏这棵树。”
Socks 笑了笑。“还不错,”他说。“我以为我的不称职巫师表演足以把你蒙混过去。”
“你做得不错,”Frank 承认道。“足够让我一次次忽视了明显的线索。”
“谢谢你,”Socks 说道。“我在学校的时候演过几季 Babbageville 社区剧场的戏。”
“看得出来,”Frank 说。“我猜你就是被选中去伪装成新转校生去抢劫档案室的那个人。虽然我得承认,我始终没搞清楚你到底伪装成了谁。”
“我用了最适合任务的人选。可选的转校生那么多。”
弗兰克点点头。说得通。“可惜车站没有保存监狱囚室分配的记录。我猜你清除了那些可能包含分配信息、囚室分配、指数、通知、监狱、囚犯和房间分配的书架。你白白偷了那些文件。”
“值得一试,”袜子说道。“这里有很多囚室。但你是怎么知道我要去监狱的?我甚至没提过。”
弗兰克笑了。“你们在关于袭击城堡的提示上并不隐晦,”他解释道,“你们是在试图把守卫从监狱引开——不是一个坏计划。如果我没有和鲁普博士聊过,可能我会错过监狱这个选项。”
袜子皱起眉头提到鲁普博士的名字。“她?她多年来一直是邪恶巫师的眼中钉。你知道她帮马库斯设计了监狱的安保吗?谁会在走廊里放一个轻微恶心的法术?这太恶劣了。”
“看起来没那么糟糕,”符号插话道,“他们给守卫戴上防止恶心的护符,只要囚犯在守卫身边或在自己的房间里,就会安全。”
“我已经呕吐两次了,”袜子反驳道。
“你们在破坏进入!这就是——”符号开始说。
弗兰克插话道,“袜子,你的同谋在哪里?”
“他一个人,”符号在袜子回答之前说道。
“什么?”弗兰克看着她。“你怎么知道这些?”
“外面停着另一个六座阵列车,”她回答道。“我不知道他为什么会用同一辆车,但它在那里,而且是空的。如果他和别人合作,应该会有其他人在守卫它或者等着逃跑。”
袜子耸了耸肩。“只有我们其中一个能用面具潜入监狱,而且大群人聚集在监狱外面通常会被认为是可疑的。所以我自愿一个人来。”
“我在哪儿能找到你的朋友们?”弗兰克问道。
袜子笑了。“你不需要找到他们,运行时先生。一旦我被捕的消息传出去,他们会找到你。今晚你得罪了不少强敌。”
“哦,真的?”弗兰克说道,“我似乎很擅长树敌。也许你可以把他们的名字告诉我,我可以把他们加入我的粉丝俱乐部。格雷琴也在其中吗?”
袜子笑了。 “格雷琴?你竟然还认为我是她的学徒?在你们揭露了其他所有谎言之后?”
“那是谁在幕后?”符号问道。
“不必要复杂性的联盟,”弗兰克回答道。他心里补充道,“显然并不包括一个叫格雷琴的巫师。”
“我很佩服你,运行时先生,”袜子说道。“没有多少人知道我们的小组织。”
“不必要复杂性的联盟?”符号问道。
“他们与巫师指数有关:追随者、同谋,或者可能只是崇拜者,”弗兰克解释道。
“指数!”符号惊叫道,“那个邪恶的巫师?他不是因为试图摧毁整个王国而被关进监狱的吗?”
“是的,就是他——那个真正坏的巫师。我不是提过他是袜子要去救的人吗?”弗兰克问道。
符号瞪回了他。
“你误解了,”袜子说道,“王国不会毁灭的。它会被拯救。一旦指数人(Exponentious)掌权,我们将迎来一个新的黄金时代。他——”
“他疯了,”弗兰克打断了他,“他会毁掉王国的。”
站在一旁的诺塔申(Notation)点头表示同意。“我得同意弗兰克的看法。”
袜子(Socks)的眼睛充满了愤怒。他猛地站起身,甩开斗篷,举起法杖,开始念诵一段长长的咒语,法杖在空中旋转成复杂的图案。袜子将法杖直接指向弗兰克,完成了咒语。
弗兰克冷静地看着,完全没有任何反应。诺塔申翻了个白眼。
“现在完了吗?”弗兰克问道,“你应该知道,魔法在这里不起作用。”
“是啊,我只是——”袜子开始说,带着挫败的语气。话音突然中断,他猛地朝门口冲去。在诺塔申准备开火之前,袜子挥动法杖攻击十字弩。她躲开了攻击,重新调整了十字弩,略感失望于这可怜的逃脱尝试。同时,弗兰克抓住袜子袍子的后摆,把他拉住。袜子的手臂像风车一样转动,拼命挣脱。
弗兰克发出一声哼,拉扯着袍子,袜子踉跄着倒退回牢房,跌倒在床上。弗兰克蹲下去捡起掉落的法杖。那把法杖对魔法毫无用处,但仍然能够打出难看的伤口。然后他大步走了出去。一旦他离开,诺塔申便猛地关上了门。
第三十章:后记
Notation 正在大厅里等,当 Frank 从队长办公室出来时。她在他关门的瞬间就问道:“怎么样?”
“还行,”Frank 说道。“他没有对我吼,我又多了三个月的办公室租金。”他举起一袋小硬币。
“就这些?”Notation 问,声音中带着失望。
“你期待什么?”Frank 问。“奖状?私家侦探是不会得到奖状的。但我听说你得到了一个,还升了职。做得好,侦探 Notation。这可算是晋升了。”
Notation 脸红了。“谢谢,”她说道。“那你呢?我以为……也许……”
“你以为队长会让我回来,”Frank 填补道。“事实上,他告诉我,是你推荐的。”
Notation 脸红得像火一样。“你是个出色的侦探,”她说道。
Frank 笑了。“他没给我工作机会。”看到 Notation 脸上的表情,他补充道,“别太在意。他根本不会再给我机会回到警队。队长和我有着很长的历史,单单一个案件是无法抹去的。总之,私家侦探的生活更适合我。”
“所以你就回去……”
“去找某人丢失的龙?”
“是啊,”Notation 说道。“那个。”
“事实上,我得到了一个新机会。”他举起手制止她在她太兴奋之前继续说下去,并补充道,“作为一名独立承包商。”
“那是什么?”Notation 问。
“看起来外面有一个巫师联盟,试图解救 Exponentious 并接管王国。”
Notation 微笑着。“我很熟悉他们。我似乎记得我最近帮忙抓住了他们一个更活跃的成员。”
“他们其中一个更活跃的成员,没错,确实是个忠诚的成员。但也有可能他是其中一个能力较弱的成员。安娜公主担心,Socks 尝试越狱会激励联盟中一些更资深、更有能力的成员。”
“所以你在调查‘不必要复杂性联盟’?当然,是作为独立承包商。”
Frank 点了点头。“这是一个棘手的搜索问题——找出所有一个秘密邪恶组织的成员。但恰好,搜索问题正是我的专长。”



浙公网安备 33010602011771号