这篇文章开始正式《algorithm puzzles》一书中的解谜之旅了!
狼羊菜过河:
谜题:一个人在河边,带着一匹狼、一只羊、一颗卷心菜。他需要用船将这三样东西运至对岸,然而,这艘船空间有限,只容得下他自己和另一样东西(狼、羊或卷心菜)。若他不在场看管的话,狼就会吃掉羊,羊就会吃掉卷心菜。此人如何才能把这三个“乘客”没有损失的送到对岸?
提示:看到这个谜题的谜面非常小,利用穷举解决是绰绰有余的。
解谜:首先搬运的一定是羊,将其搬运到对岸,将其放下,随后驾空船回来,这次带狼或者卷心菜都可以,将其带到对岸,放下狼或者卷心菜,然后将羊装上床,然后将其运回来,放下羊并装入卷心菜或狼,然后再回来把羊运到对岸。
手套选择:
在抽屉里有20只收到。其中,5双黑手套,3双棕色手套和2双灰色手套。你只能在黑暗中挑手套,并且只有将手套跳出之后才能检查其颜色。最少要挑几次才能满足以下条件?
(a)至少挑出一双颜色匹配的手套。
(b)所有颜色的手套都至少挑出一双匹配的。
提示:这里不仅要注意到题干所说最少,更关键的是意识到设问中的“至少”意味着什么。
解谜:设问中的“至少”体现出一个”算法最差情况分析“,对于(a),最坏的情况是拿到5只黑色左(右)手套、3只棕色左(右)手套、2只灰色左(右)手套,随后取任何一个手套,都必将得到一双同颜色的手套,因此至少需要拿11只。(这里默认手套分左右,而袜子不分左右,虽然现在的确有分左右的袜子……)。同理,对于(b),我们至少需要拿19只。
矩阵切割:
将一个矩形切成n(n>1)个直角三角形,有多少种方法?
提示:这里给出的谜面是不确定的,因此考虑解决问题的通法,考虑到构造直角三角形的方法,这里需要从递推的角度找到规律。
解谜:我们设置F[n]切割成n个直角三角形的方法数。当n是偶数,我们在F[n/2]的基础上,将每一个直角三角形一分为二,即F[n] = F[n/2];当n是奇数的时候,我们在F[n-1]的基础上,选择n-1个三角形中的任意一个,将其一分为二,即F[n] = (n-1)F[n-1]。而对于起始情况,有F[2] = 2.
士兵渡河:
25个士兵组成的小分队需要渡河,可是河宽且深,周围也看不到桥。他们发现河岸边有一个小船,两个12岁的男孩正在上面玩耍。船很小,仅能承载两个男孩或一个士兵的重量。士兵应怎样渡河?船从一个岸边到另一个岸边来回共计几次?
提示:模拟出正确可行的渡河方案是关键。
解谜:容易看到,第一次渡河是不可能让1个士兵驾船渡河的,因此考虑让2个男孩先渡河,然后将1个男孩留在对岸,另1个男孩驾船回来,然后一个士兵驾船过去,对岸的男孩再驾船回来,这回到了最开始的状态,但是士兵的数量少了1.那么很显然,对于25个士兵,我们需要100次。
数数的手指:
一个小女孩正在用左手手指数数,从1到1000.她从拇指算作1开始数起,然后食指为2,中指为3,无名指为4,小指为5.接下来调转方向,无名指算作6,如此反复。问如果她按照这种方式数下去,最后结束时停在哪根手指上?
提示:模拟出这个过程以期找到规律。
解谜:我们在纸上稍微多写几个数字,很容易发现落在拇指上的数字满足8n + 1,随后的三个数字是递减1的(当然除了1~5),因此当n = 125的时候,拇指上的数字是1001,所以1000这个数字出现在食指上。基于这个规律,也可以将这个谜题的谜面推广成一般形式。
硬币中的假币:
有8枚外观完全一致的硬币,其中的一枚是假币,并且知道假币要比真币轻一些,可以使用天平但不能使用砝码,问最少几次才能把假币辨别出来?
提示:对,3次一定会找到那个假币的,但是是最少的次数么?
解谜:我们任取6枚硬币,将其平分在天平的两侧,会出现如下两种情况。
1.平衡,则我们称剩余的两个硬币,得到假币。
2.不平衡,较轻的那三枚硬币中存在假币,我们任取其中的两枚平分在天平两侧,即可找到假币。
可以看到,整个过程只需要两次。
我们容易将这个谜题的谜面给一般化然后来思考问题,然后二分硬币堆来找到假币,但是这种通用的方法好像并不适合这个题目中的特殊谜面。
那么解决这种问题是否存在一个解决一般性谜面的方法呢?这里卖个关子,笔者会在以后的谜题中给出介绍。
假币堆问题:
有10堆10枚外观完全一致的硬币,其中有一堆全部是假币,其它各堆中的硬币都是真币。所有的真币重量都是10g,假币则重11克。你有一把示数可读的秤,可以称出任意数目硬币的实际质量。问最少称几次才能将全部都是假币的那堆硬币辨别出来?
提示:需要考虑如何充分利用秤显示实际质量的功能。另外这里称几次表示读几次数。
解谜:既然秤能够显示数目,为了充分利用这一功能,我们就不单单用它来定性的比较大小了,而应该将其用到定量的计算当中。我们从第i堆硬币中拿出i个硬币,假设均为真币,实际质量为(1 + 2 + 3…… + 10)*10 = 550,那么我们只需测出实际质量m = 550 + j,这个j便是假币堆了,因此只需要1次即可。这是一种“表示变更”的思想。
平铺多米诺问题:
能否用单位长度2x1的多米诺牌将8x8的方格阵铺满?里面不包含由两张2x1多米诺并行排列而成的2x2的正方形。
提示:直觉告诉我们是不能的,如何证明呢?
解谜:我们用一个二维数组map[i][j]来8x8个方格中第i行第j列的那个1x1小格子。
我们首先假设覆盖map[1][1]的是横置的多米诺牌。则map[2][1]是竖置的多米诺牌,则map[2][2]是横置的多米诺牌......容易看到规律,map[i][i]是横置的多米诺牌覆盖、map[i][i+1]是竖置多米诺覆盖,我们考虑map[7][7]、map[7][8]、map[8][8],容易看到,按照上面的规律,是无法铺满map[8][8]的,而如果用横置多米诺去铺map[7][8]、map[8][8],这显然又是与题设相矛盾的。类似的,我们可以分析map[1][1]被竖置多米诺覆盖的情况。
正方形的拆分:
将一个正方形拆分成n个小正方形,找出数字n的所有取值可能,并且将这种拆分方法归纳成算法。
提示:考虑从n的奇偶性来归纳算法。
解谜:首先考虑n是偶数的时候,n=2显然无解,而n = 4的解是十分明显的。对于n = 2k时的情况,我们首先将正方形划分成kxk的小方格,为了方便描述,我们用方阵来表示,则第一行和k列形成了k-1个正方形,而剩余的k^2 - k + 1个小正方形,恰好又构成了一个大正方形,因此对于所有n为偶数且n≠2,都是有划分方案的。
而n = 2k + 1的情况下,有n = 2k - 2 + 3,我们基于n = 2k - 2的划分方案,选择任意一个小正方形,将其分割成4份,便可以得到n = 2k + 1的划分方案。而这种模式适用于k≥3时,对于k<3,即n<6,即n = 3、5,我们需要单独考虑。而我们会发现,这两种情况是无解的。
页码计数:
一本书的页码从1开始计数。如果所有用于标记页码的十进制数总和为1578,那么这本书共有多少页?
提示:题目描述是摘抄《算法谜题》上的原话,但是这里感觉可能由于翻译问题导致问题描述有一些偏差。拿一个例子来说明题目想描述的意思,比如对于页码12,这里记录十进制总和的sum需要+2.
解谜:在充分理解题意的基础上,我们开始对页数n进行位数的讨论。
n是一位数,则sun = n。
n是两位数,则sum = 9 + 2(n-9)。
n是三位数,则sum = 189 + 3(n-99)
....
然后将sum = 1576带入这一系列等式中,求出有意义的n即可。
煎饼制作:
需要制作n≥1个煎饼,所用的煎锅一次只能同时煎两个煎饼。煎饼两面都需要烤,完成一面的煎炸需要1分钟,无论一次制作一个煎饼还是同时煎两个煎饼。煎饼两面都需要烤,完成一面的煎炸需要1分钟,无论一次制作一个煎饼还是一次同事只做两个煎饼,设计一个算法计算做这项工作的最短时间。
提示:这道题目需要充分利用题设给出的贪心策略,即是否能够尽可能每次都烤两个饼的一面。
解谜:我们从n的奇偶性来分析,当n是偶数的时候,很容易看到,我们能够使得每次在煎锅上的都有两个饼,正好能够烤完。
而当n是奇数的时候,正是这个问题充分利用贪心策略的关键所在,我们从比较简单的n = 3时入手分析,按照往常的思维我们可能会认为需要4次,即先煎好1、2号饼,然后单独烤3号饼。其实时间还可以更少,考虑到它有6个面,我们看一看能否仅仅用3次就完成,这样就保证了每次都烤两个面的最优策略。
仍然以n = 3为例,我们先烤1、2号的一个面,然后再烤2、3号的一个面,然后烤1、3剩余的两个面。这种方法可以推广到所有n取奇数的情况。
综合起来,当n = 1时,我们最少需要2分钟;其余情况,我们最少需要n分钟。
三个水壶:
有一个充满水的8品脱的水壶和两个空水壶(分别是5品脱和3品脱)。通过将水壶完全倒满水和将水壶的水倒空这两种方式,在其中的一个水壶中得到4品脱的水。
提示:在理解题意的基础上,这道题目利用穷举的思想去尝试,很快就能够得到方法。
解谜:我们记整数对(a,b,c)表示8品脱、5品脱、3品脱水壶中存水量,则其实状态为(8,0,0).
这得到4品脱水的方法可以是如下的步骤(方法并不唯一):
(8,0,0) -> (3,5,0) -> (3,2,3) -> (6,2,0) -> (6,0,2) -> (1,5,2) -> (1,4,3).
三色排列:
一个长方形的方格板,上面有3行n列的格子,有n个红色、n个白色和n个蓝色总共3n个筹码随机放在这些格子中,每个格子有且仅有一个筹码。现在需要重新调换这些筹码的位置,使得方格板上每一列都能有三种不同颜色的筹码。,唯一允许的操作是交换同一行筹码的位置,设计一个算法完成这项任务或者这名这样的算法并不存在。
提示:其实如果存在这种算法,你也应该先证明其合理性,而最好的方法就是给出一种可行的方案。
解谜:首先,我们容易看到,通过操作,存在一种方法,使得n列中每一列都有且仅有一个红色。而后我们遍历这n列,假设当前我们遍历到第i列,无非会出现如下的两种情况:
(i)该列三个颜色不同。那么符合要求,继续遍历。
(ii)该列有两个颜色同色,这里假设是1红2白。那么我们可以看到,必然存在第j列,是1红2蓝,则我们很容易证,第i列和第j列至少有一组白、蓝在一行当中,那么调换,第i列和第j列均符合要求,继续遍历。
棍子切割:
一根长度为100的棍子需要被结成100根长度为1的小短,如果一次可以同时切割多跟棍子,问最少需要切几次?设计一个算法,处理此类问题,即对长度为n的棍子,计算切割所需要的最少次数。
提示:关键要想清楚怎样的切割方法是最优的。
解谜:我们首先进行模糊化的思考,我们就简单的比较将100切成1、99和将100切成50、50两种方案,很容易看到后者方案是比较好的,因为我们为了达到最优,倾向于把我们一刀能砍尽可能多的点(假设这跟棍子上有99个100等分点)。那么我们能够猜想,在终点附近进行切割可能会是最好的方案,因为这样我们总是能够得到一系列长度相似的木棍,然后我们就可以将这些木棍并排起来然后锯开。顺着这层思路,100切成50、25、13、7、4、2、1,共计7次。
这其实就是程序设计当中非常重要的二分思想,有点类似于细胞分裂,我们切割棍子的最优策略就是将我们手头有的x根棍子一起来一刀,得到2x根棍子,而保证出现这样的切割方法就是在棍子的中点附近进行切割。为何更好理解我们将其与细胞分裂联系起来,刚开始的一根木棍相当于一个细胞,然后几个操作其实就是分裂,按照最优化的切割方法,对于长度为n的木棍,设最优策略为k次,则有2^k = n,即k = log2 n(这也是很多算法分析当中会出现log的原因,即它基于了二分思想)。考虑到n不一定刚好是2的整数次幂,我们接下来需要讨论向上还是向下取整的问题。
⌊log2 n⌋ < n,所以这里应该向上取证。
即,对于长度为n的棍子,最小的切割次数是⌈log2 n⌉。