本文始发于我的公众号:TechFlow算法
最近看到一道颇有意思的问题,分享给你们。框架
仍是老规矩,在咱们聊算法问题以前,先来看一个故事。函数
传说中,有5个海盗组成了一支无敌的海盗舰队,他们在最后一次的寻宝当中找寻到了100枚价值连城的金币。因而,很天然的,这群海盗面临分赃的问题。为了防止海盗内讧,残忍的海盗们制定了一个奇怪的规则:测试
他们决定按照功劳大小对五我的进行编号,由编号小的海盗先提出分配方案。若是方案可以获得大多数人的赞成,那么就按照他提出的方案进行分配。若是不能经过,说明他已经失去了威望,海盗们会残忍地将他投入海中喂鲨鱼。code
在一个朦胧的早上你一觉醒来,忽然发现本身成了一号海盗,那么你应该如何分配才能得到最多的金币,又不会被喂鲨鱼呢?orm
在咱们思考以前,咱们先完善一下题意,增长几个条件。blog
首先,每个海盗都很是残忍。这意味着,在不影响收益的状况下,他们会更倾向于杀人。递归
其次,每个海盗都极其聪明,都能想到最佳答案。io
这两个条件一出来,问题就比较明显了,这是博弈论题目才有的架势。form
既然这是一道博弈论的问题,那么咱们经过常规的思路是没法找到答案的,咱们须要另辟蹊径才行。
那么,怎么另辟蹊径呢?
一个比较常规的作法是先不考虑原问题,先假设一个和原问题差很少,可是规模小不少的子问题。经过对子问题的求解来摸索原问题的解法。
举个例子,在这题当中,咱们须要计算5个海盗分金币的状况。一时之间咱们有些无从下手,那么咱们简化问题,问题的规则仍是不变,可是咱们把海盗的数量减小,减小到只有一个海盗。那么根据规则,很显然,最后的结果是这个海盗独吞全部的金币。
这个时候的分配方案是:[0, 0, 0, 0, 100]
咱们从这个点开始往回倒推,假设这个时候多了一个海盗,一共是4号和5号两个海盗的时候,会怎么样?
显然由于要求要一半以上赞成提案,提案才能够经过。因此在这个时候,不管4号海盗如何提议,5号都不会赞成,要将他投下海喂鲨鱼。因此若是只剩下4和5的时候,4号海盗必死无疑。
这个时候的分配方案是: [0, 0, 0, -1, 100],-1表示必死无疑。
那若是再加一个海盗呢?
再加一个海盗的话,是3,4,5三个海盗的状况。由于只剩4和5的时候4号必死,因此他为了活命必定会赞成3号的提案(海盗对其余人残忍,对本身不残忍)。这个时候,3号不论如何提议,都必定能够经过。由于算上他本身的一票,和4号的一票,已通过半了,因此他的提案必定能够经过。
这个时候的分配方案是: [0, 0, 100, 0, 0]
咱们再加入一个海盗,考虑一共剩下4个海盗的状况。若是2号死去,那么3号能够独吞全部金币,因此显然3号必定不会赞成2号的方案。4我的的时候,至少须要3我的赞成才能够经过方案,那么2号必需要争取4号和5号。若是2号死去,4号和5号一无全部,因此2号只须要分配给4号和5号一枚金币,就能够拉拢他们。
这个时候的分配方案是: [0, 98, 0, 1, 1]
最后,咱们再加入1号海盗。同理,1号海盗的提案须要至少3我的经过。算上他本身,他还须要争取2票。因为1号死去2号能够得到98枚金币,因此1号必定没法争取2号,仍是只能从3,4,5三我的下手。能够给3号1枚,4号两枚(比2号的方案多一枚),也能够给3号1没,5号两枚。
这个时候的分配方案是: [97, 0, 1, 2, 0] 或者是 [97, 0, 1, 0, 2]。
到这里,这个问题就结束了。可是咱们的思考并无结束,不知道你们从刚才的解法当中有没有看出规律。咱们面临5个海盗这种错综复杂状况的时候根本无从下手,可是一旦当咱们试着将问题的规模缩小,从简单的状况开始思考,那么问题一会儿就豁然开朗了。
老子说:天下大事,必做于细,天下难事,必做于易。从这个问题来看,和这个道理相得益彰。
这种从最简单推导最复杂的算法就称为递归。
假设,获取n个海盗分配方案的函数是f。当咱们计算f(2)时,咱们须要根据f(1)的结果。咱们试着写成伪代码:
def f(n): if n == 1: return [0, 0, 0, 0, 100] else: allocation = f(n-1) # 新的分配 new_allocation = allocate(allocation) return new_allocation
咱们先忽略allocate这个方法内部是怎么实现的,单纯看这段代码,整个框架已经有了。
递归的精髓也就在这里,程序本身调用本身只是表象,内里的精髓实际上是问题的分割。整个递归从上到下的过程,其实是一个大问题化解成小问题的过程。若是还不明白,咱们再来看一个经典的例子来巩固一下,这个问题就是大名鼎鼎的汉诺塔问题:
在印度神话当中有一个大神叫作梵天,他在创造世界的时候创造了三根金刚柱。为了排解无聊,他在其中一根柱子上摆放了64个圆盘。这64个圆盘从上往下依次增大,他给僧侣出了一个问题。一次只能移动一个圆盘,而且圆盘只能放在比它大的圆盘上,该怎么作才能将圆盘从一根柱子移动到另外一根呢?
为了简化问题,咱们先观察摆放5个圆盘的状况。从图中能够看出来,一开始的时候圆盘都在A柱,若是咱们想要将圆盘移动到B柱应该怎么办呢?
咱们一样先来观察最简单的状况: A柱上只有一个圆盘,那很简单,咱们直接将它移动到B柱便可。若是有两个圆盘呢?咱们须要先将第一个移动到C柱,而后将第二个移动到B柱,最后再将C柱上的圆盘移动到B。那若是是三个圆盘呢,稍微复杂一些,但仔细列举一下,也能算得出来。
可是咱们怎么经过问题规模的缩小来化简问题呢?
这须要咱们对于题目进行深刻思考,找到其中的关键点。这题的关键点就是圆盘的限制,大的圆盘不能落在小的圆盘上面。因此若是咱们想要将n个圆盘从A柱移动到B柱,必需要将前n-1个圆盘先移动到C柱,这样才能够将最大的那块放到B,如此以后再将n-1块移动回B。
也就是说,咱们将n-1块圆盘当作是一个总体,这样n块圆盘的方案就和两块圆盘时同样了。这就经过递归完成了简化。
最后,也是最关键的,怎么移动n-1块圆盘呢?其实很简单,咱们套用一样的方法,再将这n-1块圆盘中的n-2块当作是总体,递归操做。理解了以后,不妨试着写出代码,其实只有几行:
def hanoi_tower(num, tower_start, tower_dest, tower_other): if num == 1: print('move plate {} from {} to {}'.format(num, tower_start, tower_dest)) return hanoi_tower(num-1, tower_start, tower_other, tower_dest) print('move plate {} from {} to {}'.format(num, tower_start, tower_dest)) hanoi_tower(num-1, tower_other, tower_dest, tower_start)
咱们调用一下这个方法,进行一下测试:
结果和咱们的预期一致,说明咱们的算法是正确的。
最后,咱们再回到海盗问题,又该怎么用代码实现呢?感兴趣的同窗不妨亲自动手试试,若是实在写不出代码,在公众号回复关键词”海盗分金“查看我写的代码。
今天的文章就到这里,扫码关注个人公众号:TechFlow,获取更多文章