`

百度面试题b。zz

阅读更多

 

15.

数学的逻辑有时会导致看来十分怪异的结论。一般的规则是,如果逻辑推理没有漏洞,那么结论就必定站得住脚,即使它与你的直觉矛盾。 19989月,加利福尼亚州帕洛阿尔托的Stephen M. Omohundro寄给我一道难题,它恰好就属于这一类。这难题已经流传了至少十年,但是Omohundro对它作了改动,使它的逻辑问题变得分外复杂了。

 

先来看看此难题原先的形状。10名海盗抢得了窖藏的100块金子,并打算瓜分这些战利品。这是一些讲民主的海盗(当然是他们自己特有的民主),他们的习惯是按下面的方式进行分配:最厉害的一名海盗提出分配方案,然后所有的海盗(包括提出方案者本人)就此方案进行表决。如果50%或更多的海盗赞同此方案,此方案就获得通过并据此分配战利品。否则提出方案的海盗将被扔到海里,然后下提名最厉害的海盗又重复上述过程。

 

所有的海盗都乐于看到他们的一位同伙被扔进海里,不过,如果让他们选择的话,他们还是宁可得一笔现金。他们当然也不愿意自己被扔到海里。所有的海盗都是有理性的,而且知道其他的海盗也是有理性的。此外,没有两名海盗是同等厉害的——这些海盗按照完全由上到下的等级排好了座次,并且每个人都清楚自己和其他所有人的等级。这些金块不能再分,也不允许几名海盗共有金块,因为任何海盗都不相信他的同伙会遵守关于共享金块的安排。这是一伙每人都只为自己打算的海盗。

 

最凶的一名海盗应当提出什么样的分配方案才能使他获得最多的金子呢?

 

为方便起见,我们按照这些海盗的怯懦程度来给他们编号。最怯懦的海盗为1号海盗,次怯懦的海盗为2号海盗,如此类推。这样最厉害的海盗就应当得到最大的编号,而方案的提出就将倒过来从上至下地进行。

 

分析所有这类策略游戏的奥妙就在于应当从结尾出发倒推回去。游戏结束时,你容易知道何种决策有利而何种决策不利。确定了这一点后,你就可以把它用到倒数第2次决策上,如此类推。如果从游戏的开头出发进行分析,那是走不了多远的。其原因在于,所有的战略决策都是要确定:“如果我这样做,那么下一个人会怎样做?”

 

因此在你以下海盗所做的决定对你来说是重要的,而在你之前的海盗所做的决定并不重要,因为你反正对这些决定也无能为力了。

 

记住了这一点,就可以知道我们的出发点应当是游戏进行到只剩两名海盗——即1号和2号——的时候。这时最厉害的海盗是2号,而他的最佳分配方案是一目了然的:100块金子全归他一人所有,1号海盗什么也得不到。由于他自己肯定为这个方案投赞成票,这样就占了总数的50%,因此方案获得通过。

 

现在加上3号海盗。1号海盗知道,如果3号的方案被否决,那么最后将只剩2个海盗,而1号将肯定一无所获——此外,3号也明白1号了解这一形势。因此,只要3号的分配方案给1号一点甜头使他不至于空手而归,那么不论3号提出什么样的分配方案,1号都将投赞成票。因此3号需要分出尽可能少的一点金子来贿赂1号海盗,这样就有了下面的分配方案: 3号海盗分得99块金子,2号海盗一无所获,1号海盗得1块金子。

 

4号海盗的策略也差不多。他需要有50%的支持票,因此同3号一样也需再找一人做同党。他可以给同党的最低贿赂是1块金子,而他可以用这块金子来收买2号海盗。因为如果4号被否决而3号得以通过,则2号将一文不名。因此,4号的分配方案应是:99块金子归自己,3号一块也得不到,2号得1块金子,1号也是一块也得不到。

 

5号海盗的策略稍有不同。他需要收买另两名海盗,因此至少得用2块金子来贿赂,才能使自己的方案得到采纳。他的分配方案应该是:98块金子归自己,1块金子给3号,1块金子给1号。

 

这一分析过程可以照着上述思路继续进行下去。每个分配方案都是唯一确定的,它可以使提出该方案的海盗获得尽可能多的金子,同时又保证该方案肯定能通过。照这一模式进行下去,10号海盗提出的方案将是96块金子归他所有,其他编号为偶数的海盗各得1块金子,而编号为奇数的海盗则什么也得不到。这就解决了10名海盗的分配难题。

 

Omohundro的贡献是他把这一问题扩大到有500名海盗的情形,即500名海盗瓜分100块金子。显然,类似的规律依然成立——至少是在一定范围内成立。事实上,前面所述的规律直到第200号海盗都成立。 200号海盗的方案将是:从1199号的所有奇数号的海盗都将一无所获,而从2198号的所有偶数号海盗将各得1块金子,剩下的1块金子归200号海盗自己所有。

 

乍看起来,这一论证方法到200号之后将不再适用了,因为201号拿不出更多的金子来收买其他海盗。但是即使分不到金子,201号至少还希望自己不会被扔进海里,因此他可以这样分配:给1199号的所有奇数号海盗每人1块金子,自己一块也不要。

 

202号海盗同样别无选择,只能一块金子都不要了——他必须把这100块金子全部用来收买100名海盗,而且这100名海盗还必须是那些按照201号方案将一无所获的人。由于这样的海盗有101名,因此202号的方案将不再是唯一的——贿赂方案有101种。

 

203号海盗必须获得102张赞成票,但他显然没有足够的金子去收买101名同伙。因此,无论提出什么样的分配方案,他都注定会被扔到海里去喂鱼。不过,尽管203号命中注定死路一条,但并不是说他在游戏进程中不起任何作用。相反,204号现在知道,203号为了能保住性命,就必须避免由他自己来提出分配方案这么一种局面,所以无论204号海盗提出什么样的方案,203号都一定会投赞成票。这样204号海盗总算侥幸拣到一条命:他可以得到他自己的1票、203号的1票、以及另外100名收买的海盗的赞成票,刚好达到保命所需的50%。获得金子的海盗,必属于根据202号方案肯定将一无所获的那101名海盗之列。

 

205号海盗的命运又如何呢?他可没有这样走运了。他不能指望203号和204号支持他的方案,因为如果他们投票反对205号方案,就可以幸灾乐祸地看到205号被扔到海里去喂鱼,而他们自己的性命却仍然能够保全。这样,无论205号海盗提出什么方案都必死无疑。206号海盗也是如此——他肯定可以得到205号的支持,但这不足以救他一命。类似地,207号海盗需要104张赞成票——除了他收买的100张赞成票以及他自己的1张赞成票之外,他还需3张赞成票才能免于一死。他可以获得205号和206号的支持,但还差一张票却是无论如何也弄不到了,因此207号海盗的命运也是下海喂鱼。

 

208号又时来运转了。他需要104张赞成票,而205206207号都会支持他,加上他自己一票及收买的100票,他得以过关保命。获得他贿赂的必属于那些根据204号方案肯定将一无所获的人(候选人包括2200号中所有偶数号的海盗、以及201203204号)。

 

现在可以看出一条新的、此后将一直有效的规律:那些方案能过关的海盗(他们的分配方案全都是把金子用来收买100名同伙而自己一点都得不到)相隔的距离越来越远,而在他们之间的海盗则无论提什么样的方案都会被扔进海里——因此为了保命,他们必会投票支持比他们厉害的海盗提出的任何分配方案。得以避免葬身鱼腹的海盗包括201202204208216232264328456号,即其号码等于2002的某一方幂的海盗。

 

         现在我们来看看哪些海盗是获得贿赂的幸运儿。分配贿赂的方法是不唯一的,其中一种方法是让201号海盗把贿赂分给1199号的所有奇数编号的海盗,让202号分给2200号的所有偶数编号的海盗,然后是让204号贿赂奇数编号的海盗,208号贿赂偶数编号的海盗,如此类推,也就是轮流贿赂奇数编号和偶数编号的海盗。

 

结论是:当500名海盗运用最优策略来瓜分金子时,头44名海盗必死无疑,而456号海盗则给从1199号中所有奇数编号的海盗每人分1块金子,问题就解决了。由于这些海盗所实行的那种民主制度,他们的事情就搞成了最厉害的一批海盗多半都是下海喂鱼,不过有时他们也会觉得自己很幸运——虽然分不到抢来的金子,但总可以免于一死。只有最怯懦的200名海盗有可能分得一份脏物,而他们之中又只有一半的人能真正得到一块金子,的确是怯懦者继承财富。

 

二叉搜索树

二叉查找树(Binary Search Tree),或者是一棵空树,或者是具有下列性质的二叉树:

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

它的左、右子树也分别为二叉排序树。

 

寻找二叉树中任意两个结点的LCA

题目要求返回二叉树中任意两个结点的最近公共父结点的指针。

 

首先要确定二叉树是不是“二叉搜索树”,如果不是的话,没有什么技巧,只能使用LCA的通用办法,先深度遍历,然后就转化成了RMQ问题。对于RMQ问题,使用SparseTable技巧的话,可以达到O(nlogn)的预处理,O(1)的查询。

 

如果是二叉搜索树,就有一些技巧了。一般直接的想法就是分别求出根到两个结点的路径,然后取其最长公共前缀。然后再从根结点沿着刚才找到的公共路径顺序而下,找到该公共父结点,返回它的指针。如果题目求的是两个“叶结点”的最近公共父结点,则在树高度比较大的时候,这种方法效率比较低。

 

有两种比较巧妙的办法:

从根结点开始,如果两个结点ab都小于它,则递归处理它的左子树;如果ab都大于它,则递归处理它的右子树;如果有一个大于它、一个小于它,则说明它就是ab两个结点的最近公共父结点,直接返回它本身即可。

 

编程判断两个链表是否相交

给出两个单向链表的头指针,比如h1h2,判断这两个链表是否相交。这里为了简化问题,我们假设两个链表均不带环。

 

分析与解法

 

这样的一个问题,也许我们平时很少考虑。但是在一个大的系统中,如果出现两个链表相交的情况,而且释放了其中一个链表的所有节点,那样就会造成信息的丢失,并且另一个与之相交的链表也会受到影响,这是我们不希望看到的。在特殊的情况下,的确需要出现相交的两个链表,我们希望在释放一个链表之前知道是否有其他链表跟当前这个链表相交。

 

解法一 直观的想法

 

看到这个问题,我们的第一个想法估计都是,“不管三七二十一”,先判断第一个链表的每个节点是否在第二个链表中。这种方法的时间复杂度为O(Length(h1)*Length(h2))。可见,这种方法很耗时间。

 

解法二 利用计数的方法

 

很容易想到,如果两个链表相交,那么这两个链表就会有共同的节点。而节点地址又是节点的唯一标识。所以,如果我们能够判断两个链表中是否存在地址一致的节点,就可以知道这两个链表是否相交。一个简单的做法是对第一个链表的节点地址进行hash排序,建立hash表,然后针对第二个链表的每个节点的地址查询hash表,如果它在hash表中出现,那么说明第二个链表和第一个链表有共同的节点。这个方法的时间复杂度为O(max(Length(h1)+Length(h2)))。但是它同时需要附加O(Length(h1))的存储空间,以存储哈希表。虽然这样做减少了时间复杂度,但是是以增加存储空间为代价的。是否还有更好的方法呢,既能够以线性时间复杂度解决问题,又能较少存储空间?

 

解法三

 

由于两个链表都没有环,我们可以把第二个链表接在第一个链表后面,如果得到的链表有环,则说明这两个链表相交。否则,这两个链表不想交。这样我们就把问题转化为判断一个链表是否有环。

 

判断一个链表是否有环,也不是一个简单的问题,但是需要注意的是,在这里如果有环,则第二个链表的表头一定在环上,我们只需要从第二个链表开始遍历,看是否会回到起始点就可以判断出来。最后,当然可别忘了恢复原来的状态,去掉从第一个链表到第二个链表表头的指向。

 

这个方法总的时间复杂度也是线性的,但只需要常数的空间。

 

解法四

 

仔细观察题目中的图示,如果两个没有环的链表相交于某一节点的话,那么在这个节点之后的所有节点都是两个链表所共有的。那么我们能否利用这个特点简化我们的解法呢?困难在于我们并不知道哪个节点必定是两个链表共有的节点(如果它们相交的话)。进一步考虑“如果没有环的链表相交于某一节点的话,那么在这个节点之后的所有节点都是两个链表共有的”这个特点,我们可以知道,如果它们相交,则最后一个节点一定是共有的。而我们很容易能得到链表的最后一个节点,所以这成了我们简化解法的一个主要突破口。

 

先遍历第一个链表,记住最后一个节点。然后遍历第二个链表,到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交,否则,不想交。这样我们就得到了一个时间复杂度,它为O(Length(h1)+Length(h2)),而且只用了一个额外的指针来存储最后一个节点。这个方法比解法三更胜一筹。

 

16.

【摘要】有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。1、如何判断一个链表是不是这类链表?2、如果链表为存在环,如果找到环的入口点?扩展:判断两个单链表是否相交,如果相交,给出相交的第一个点。

有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。

 

问题:

 

1、如何判断一个链表是不是这类链表?

2、如果链表为存在环,如果找到环的入口点?

 

R16.

附一种易于理解的解释:

 

一种On)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然):

关于这个解法最形象的比喻就是在操场当中跑步,速度快的会把速度慢的扣圈

 

可以证明,p2追赶上p1的时候,p1一定还没有走完一遍环路,p2也不会跨越p1多圈才追上

 

我们可以从p2p1的位置差距来证明,p2一定会赶上p1但是不会跳过p1

 

因为p2每次走2步,而p1走一步,所以他们之间的差距是一步一步的缩小,43210 0的时候就重合了

 

根据这个方式,可以证明,p2每次走三步以上,并不总能加快检测的速度,反而有可能判别不出有环

 

既然能够判断出是否是有环路,那改如何找到这个环路的入口呢?

 

解法如下: p2按照每次2步,p1每次一步的方式走,发现p2p1重合,确定了单向链表有环路了

 

接下来,让p2回到链表的头部,重新走,每次步长不是走2了,而是走1,那么当p1p2再次相遇的时候,就是环路的入口了。

 

这点可以证明的:

 

p2p1第一次相遇的时候,假定p1走了n步骤,环路的入口是在p步的时候经过的,那么有

 

p1走的路径: p+c n         cp1p2相交点,距离环路入口的距离

 

p2走的路径: p+c+k*L = 2*n   L为环路的周长,k是整数

 

显然,如果从p+c点开始,p1再走n步骤的话,还可以回到p+c这个点

 

同时p2从头开始走的话,经过n步,也会达到p+c这点

 

显然在这个步骤当中p1p2只有前p步骤走的路径不同,所以当p1p2再次重合的时候,必然是在链表的环路入口点上。

 

扩展问题:

 

判断两个单链表是否相交,如果相交,给出相交的第一个点(两个链表都不存在环)。

 

比较好的方法有两个:

 

一、将其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个点。

 

二、如果两个链表相交,那个两个链表从相交点到链表结束都是相同的节点,我们可以先遍历一个链表,直到尾部,再遍历另外一个链表,如果也可以走到同样的结尾点,则两个链表相交。

 

这时我们记下两个链表length,再遍历一次,长链表节点先出发前进(lengthMax-lengthMin)步,之后两个链表同时前进,每次一步,相遇的第一点即为两个链表相交的第一个点。

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics