UyHiP趣题:用最少的称重次数验证硬币的重量

    这是一个非常有趣的问题,它出自 UyHiP May 2013 的谜题。

    假设你有 n 枚外观完全相同的硬币,它们的重量分别为 1g, 2g, 3g, …, ng 。有意思的是,这一次,你已经知道了各枚硬币的重量,而且你也已经把重量值标在了这些硬币上。但是,由于我不知道各枚硬币的重量,因此我希望你能向我证明,你所标的重量值是正确的(我知道这些硬币的重量是从 1 克到 n 克,我只是不知道哪个硬币对应哪个重量)。

    你唯一能用的工具就是一架天平。每一次,你可以任意选择一枚或多枚硬币,放在天平的左侧,再从剩下的硬币中任意选择一枚或多枚硬币,放在天平的右侧(注意,你只能在天平上放硬币,不能放别的东西)。一个有意思的问题是,为了向我证明你所标的重量值都是对的,你最少需要使用多少次天平?

    显然,为了证明 n 枚硬币的重量标签的正确性,我们最多需要称 n – 1 次。先把硬币 1 放在左边,把硬币 2 放在右边,让对方看到硬币 1 确实比硬币 2 要轻。接下来,向对方验证硬币 2 确实比硬币 3 更轻,硬币 3 确实比硬币 4 更轻,等等。称完 n – 1 次后,我们就相当于给出了 n 枚硬币的轻重顺序,因而它们只有可能分别是 1 克 、 2 克 、 3 克……。

    我们还能做得更好吗?不妨让我们看看 n 比较小的情况。例如,当 n = 4 的时候,利用上述方法可以 3 次完成验证,那么只用 2 次可以完成验证吗?仔细一想,你会发现真的可以!其中一种方法就是,先把硬币 1 和硬币 2 放在左边,把硬币 4 放在右边。由于两枚硬币的重量之和小于第三枚硬币,这只可能是 1 + 2 < 4 ,因此对方会相信,左边两枚硬币分别是 1 和 2 ,右边那枚硬币是 4 ,没放上去的那枚硬币是 3 。对方唯一不知道的就是,在左边两枚硬币中,究竟谁是 1 ,谁是 2 。于是,我们只需要再称一下硬币 1 和硬币 2 ,问题就解决了。

    不妨把证明 n 枚硬币重量标签的正确性最少需要的称重次数记作 B(n) 。我们的问题就是:判断 B(n) 是以什么级别增长的。

Read more…

漫话二分(下)

    很多问题并不完全符合二分的模型。最常见的一个情况估计应该是无穷长的有序队列中的二分查找问题。例如,前文所提到的求解x^x=a实际上就是这样的问题,这里x的取值范围可以是大于0的所有实数。当然,这里的x明显有一个上界,比如x明显要比a小。但是,如果有什么二分问题,它没有一个明确的上界呢?比如,我们再玩一次猜数游戏,我想一个数,然后告诉你你的猜测是大了还是小了。不同的是,我不告诉你这个数是在什么范围内选的,我想的数可以是任意一个正整数。那你该怎么办呢?最初的想法当然是,不断往大的猜,直到某个时候它超过了目标值为止;然后以它为上界,剩下的就可以看作有限多了。关键是,我们以什么样的跨度来枚举这个上界?首先必须肯定的是,这个上界枚举序列必须是发散的,否则会有数我们一辈子也猜不到;同时,线性增长的策略也是很傻的:跨度太小了你可能得到猴年才能找出上界,跨度太大了的话一来就确定了上界,但待考虑的队列也可能远远超过所需。一个比较灵活的想法是:按照1, 2, 4, 8, 16, …, 2^n的序列来猜测上界。这是一种“相对大小”的思想:既然连前面N个数都小了,我们也就不在乎那么一两个了,不妨直接再跳过N个数直接考察2N。这样的话,整个猜测过程仍然保持log(n)的复杂度,整个算法也更加美观一些。

    假如你有一台时光机,可以让你到任意远的未来去,你打算怎么来使用它?或许你会说,先去100年后看看,生活一年之后再去200年后体验体验,然后再去300年后生活一年,再去400年后生活一年……其实,这种线性的时光旅行并不科学。当你已经跨越了数千年以后,相比之下100年的跨度已经不算遥远了,1000年后与1100年后的差异远远没有今天和100年以后的变化那样令人震撼。我说我和Stetson MM的思维如出一辙,那不是说着好玩的。一次MSN聊天时我们讨论到这个话题,两人同时想到了这样的时间旅行方式:先直接跳到1年后生活,再到2年后生活一段时间,再到4年后,再到8年后……用这种方式来体验未来,即使我在每一个时间点停留一年,我也能保证见到从我余生的感情生活到人类文明的各种不可思议的形态,甚至到文明的轮回与宇宙的毁灭等各种尺度下的牛B事。我的确能够看到充分远的未来,了解到足够宏大的文明史和宇宙史:假设我还能活80年,那么我可以看到2^80=1208925819614629174706176年内的种种,这么多年里估计就是宇宙热寂也该结束了。
    折纸后的高度、在棋盘里放米、Hanoi塔与世界末日……无数火星的例子告诉我们,倍增的力量是异常强大的。如果你不信的话,下次来北京时请我喝酒,不妨这样来试试:先我喝一杯,你喝一口;然后我喝一杯,你喝两口;然后我喝一杯,你喝四口……看咱俩谁坚持的久。

Read more…

漫话二分(上)

    二分思想真的是无所不在,即使在中文系的专业课中我们也能见到这个词。在语言学概论中我们提到,一个音位可以由一组区别特征确定下来,这些区别特征总是以只具有“是/否”、“有/无”等两种对立属性的“二元偶分组”形式存在,因为这样可以最方便最快捷地确定出一个元素。这有点像猜数字一样,我想一个数字后让你来猜,我告诉你你的猜测是大了还是小了。只是在这里,回馈的信息不再是大小,而是“辅音/元音”、“口音/鼻音”、“浊音/清音”、“送气/不送气”等形式逐层细分。这让人联想到5张卡片猜年龄的老把戏,一系列火星的称球问题,基于比较的排序算法的复杂度下界,或者经典的20q在线游戏
    一个有趣的事实是,相当多的人都错误地理解了“二分”这个词,但他们在生活中却拥有很强的二分意识。我们语言学概论的老师(这里就不说是谁了)在讲解二分时举了一个甚为荒谬的例子:如果你要在房间里找一根针,那么你可以把房间划分为两半,如果这一半找不到的话说明针一定在房间另一半,此时再把那一半分成两部分,不断分分分分分最后总能找到针的位置。这是这位老师无数荒唐的例子中的冰山一角,因为这个“二分”与搜索别无二致。这个“二分”的判断环节并不是即刻返回的,而且最关键的是它并不具有规模减半的功能,或者说一旦返回“真”后我们并不会再接着二分下去。如果让我来举例子的话,同样是拿找东西打比方,在合唱队中找出跑调了的人是一个绝佳的例子,因为在合唱中我们能轻易分辨出一个不和谐的声音(虽然无法准确判断这个声音是从哪儿传来的),不断叫当前的人的其中一半来合唱便可渐渐判断出那个人的位置。但讽刺的是,这老师在举这个错误例子的同时,竟然在不自觉地用二分法来调整课件的字号。他发现这一页ppt的字号太小了,我们可能看不清,于是希望让字号尽可能的大但又不致于大到显示不下。他开始尝试40号,发现字已经超出屏幕了;然后把字体改成20号,又觉得还能再大一些;进而又改到28号(工具栏上的字号调整以4为步长),最后确定到了24号字。

    如果真的叫一个课讲的好的老师来说二分,课程可以变得相当有意思。每次回我们高中时我都讲了很多次课,我最喜欢聊到的话题之一就是二分。从猜数游戏引入二分查询有序队列中的指定元素,然后提出一些标准的有序队列二分搜索的实际应用,比如解方程x^x=100一类的问题。紧接着提出二分的各种有趣的变形,例如如何在有序整数序列中查询A_i=i的元素。提出这些问题的目的就在于告诉大家,二分的思想不仅仅是用在猜数游戏一类的情况下。二分判断并不只限于“比目标值大/比目标值小”,只要能判断出目标值在哪边都行,例如在这里,A_i<i表明目标元素一定还在右边,A_i>i则表明目标元素在左边。

 i  =    1   2   3   4   5   6   7   8   9   10
A_i = -100 -20  -3   0   2   6  13  14  27  298

Read more…

趣题:经典二分问题的一个扩展

    SETI@home可以在杂乱的射电数据中搜寻独特的讯号,你能在大街上的嘈杂声中清晰分辨出一个尖细的女声大叫“亚美蝶”。这些现象都表明,有时对集合里的所有元素进行整体考察可以很快找出我们所要找的个体。去年我们搞合唱比赛时,我又想到了一个绝佳的例子:你可以在合唱声中清楚地听到是否有人跑调。考虑这样一个问题,假如合唱团里有一个人唱歌始终走调,但我听不出来是谁走调,只能听出当前正在唱歌的人中是否有唱走调了的人。那么,我如何才能迅速地揪出那个唱走调的人?利用经典二分法,我们可以在log2(n)次合唱后找出唱走调了的人。每一次,我都把剩下的人平均分成两组,然后选其中一组来合唱:如果听不到走调的声音,这一组的人就全部过关;如果听到有人走调,那另一组里的人都可以被排除了。递归地对剩下的组进行同样的操作,log2(n)次操作后必定可以找出那个唱歌走调的人。
    现在的问题变得有些麻烦了。假如我们知道合唱队里有一个人唱歌爱跑调,但他不是总会跑调。具体地说,他只有1/2的概率唱错,但其余1/2的时间里他却唱得很准。现在,传统的二分法不再适用了,因为没有走调声已经不能起到排除的作用了。你能想出多少种可行的算法来找出那个人?下面提出一些可行的方法,你认为哪种方法更好?你能求出这些算法所需要的检测次数的期望值各是多少吗?

    1. 不断地随机生成一个大小为n/2的子集并对其进行检测,直到某次不能通过检测为止,然后递归地对其进行操作。
    2. 所选的子集大小为n/2是最优的吗?把上面这种方法的n/2改成n/a,常数a的最优值是多少?
    3. 检测次数的期望值还可以更小吗?我们想到,每次都重新生成一个新的集合其实并不科学,新集合本身是否包含老鼠屎也是得碰碰运气的。因此,对方法1的一个合理改进是:把集合平均划分为两个部分,交替对它们进行检测直到某次检测没通过为止,然后对该组递归操作下去。这种方法真的比前两种好吗?它所需要的期望次数是多少?
    4. 尝试对方法3进行改进。如果把集合平均划分成3份并循环进行检测,效果会不会更好一些?

    1. 选取的子集有1/2的概率覆盖了我们要找的那个人,子集里有他而他这次恰好又唱走调了则有1/4的概率。因此,不管规模有多大,平均需要4次才能把规模缩小一半。因此,检测次数的期望值为4*log2(n)。为了方便比较期望值的大小,后面的答案我们一律表示成一个常数乘以log2(n)的形式。
    2. 类似地,平均需要2a次检测才能把规模缩小到原来的1/a,因此总共花费的检测次数为2a*log2(n)/log2(a)。对函数求导,可得当a为e时函数值达到最小。此时的检测次数期望值为2e*log2(n)/log2(e)≈3.7683 * log2(n)。
    3. 这个就经典了。设方法3里把规模缩小一半所需要的检测的期望次数为m,下面我们来看m应该等于多少。把n个人平均分成两组,我们要找的老鼠屎有1/2的概率在第一组,有1/2的概率在第二组。因此,第一次就测出问题来有1/4的可能,第二次就测出问题也有1/4的可能。对于剩下的1/2种情况,局面变得又和最开始一样,只是平均需要的检测次数比原来多了2。根据期望值的定义,有m=(1/4)*1 + (1/4)*2 + (1/2)*(m+2),解得m=3.5。总的检测次数就是3.5 * log2(n),它比前面两种方法都要好。你可能不同意上面求m的方法。这没啥,如果你不断对m进行迭代,你会发现展开出来的式子就是最标准的期望值定义。
    4. 类似地,有m=(1/6)*1 + (1/6)*2 + (1/6)*3 + (1/2)*(m+3),解得m=5。于是,把规模缩小到原来的1/3平均需要5次检测,总的检测次数为5*log2(n)/log2(3)≈3.1546 * log2(n)。

题目来源:IBM Ponder This Dec07
原文还从熵的角度探寻了问题的最优算法,感兴趣的读者可以去看一看

十个利用矩阵乘法解决的经典题目

    好像目前还没有这方面题目的总结。这几天连续看到四个问这类题目的人,今天在这里简单写一下。这里我们不介绍其它有关矩阵的知识,只介绍矩阵乘法和相关性质。
    不要以为数学中的矩阵也是黑色屏幕上不断变化的绿色字符。在数学中,一个矩阵说穿了就是一个二维数组。一个n行m列的矩阵可以乘以一个m行p列的矩阵,得到的结果是一个n行p列的矩阵,其中的第i行第j列位置上的数等于前一个矩阵第i行上的m个数与后一个矩阵第j列上的m个数对应相乘后所有m个乘积的和。比如,下面的算式表示一个2行2列的矩阵乘以2行3列的矩阵,其结果是一个2行3列的矩阵。其中,结果的那个4等于2*2+0*1:
    
    下面的算式则是一个1 x 3的矩阵乘以3 x 2的矩阵,得到一个1 x 2的矩阵:
    

    矩阵乘法的两个重要性质:一,矩阵乘法不满足交换律;二,矩阵乘法满足结合律。为什么矩阵乘法不满足交换律呢?废话,交换过来后两个矩阵有可能根本不能相乘。为什么它又满足结合律呢?仔细想想你会发现这也是废话。假设你有三个矩阵A、B、C,那么(AB)C和A(BC)的结果的第i行第j列上的数都等于所有A(ik)*B(kl)*C(lj)的和(枚举所有的k和l)。

经典题目1 给定n个点,m个操作,构造O(m+n)的算法输出m个操作后各点的位置。操作有平移、缩放、翻转和旋转
    这里的操作是对所有点同时进行的。其中翻转是以坐标轴为对称轴进行翻转(两种情况),旋转则以原点为中心。如果对每个点分别进行模拟,那么m个操作总共耗时O(mn)。利用矩阵乘法可以在O(m)的时间里把所有操作合并为一个矩阵,然后每个点与该矩阵相乘即可直接得出最终该点的位置,总共耗时O(m+n)。假设初始时某个点的坐标为x和y,下面5个矩阵可以分别对其进行平移、旋转、翻转和旋转操作。预先把所有m个操作所对应的矩阵全部乘起来,再乘以(x,y,1),即可一步得出最终点的位置。
    

经典题目2 给定矩阵A,请快速计算出A^n(n个A相乘)的结果,输出的每个数都mod p。
    由于矩阵乘法具有结合律,因此A^4 = A * A * A * A = (A*A) * (A*A) = A^2 * A^2。我们可以得到这样的结论:当n为偶数时,A^n = A^(n/2) * A^(n/2);当n为奇数时,A^n = A^(n/2) * A^(n/2) * A (其中n/2取整)。这就告诉我们,计算A^n也可以使用二分快速求幂的方法。例如,为了算出A^25的值,我们只需要递归地计算出A^12、A^6、A^3的值即可。根据这里的一些结果,我们可以在计算过程中不断取模,避免高精度运算。

经典题目3 POJ3233 (感谢rmq)
    题目大意:给定矩阵A,求A + A^2 + A^3 + … + A^k的结果(两个矩阵相加就是对应位置分别相加)。输出的数据mod m。k<=10^9。
    这道题两次二分,相当经典。首先我们知道,A^i可以二分求出。然后我们需要对整个题目的数据规模k进行二分。比如,当k=6时,有:
    A + A^2 + A^3 + A^4 + A^5 + A^6 =(A + A^2 + A^3) + A^3*(A + A^2 + A^3)
    应用这个式子后,规模k减小了一半。我们二分求出A^3后再递归地计算A + A^2 + A^3,即可得到原问题的答案。

经典题目4 VOJ1049
    题目大意:顺次给出m个置换,反复使用这m个置换对初始序列进行操作,问k次置换后的序列。m<=10, k<2^31。
    首先将这m个置换“合并”起来(算出这m个置换的乘积),然后接下来我们需要执行这个置换k/m次(取整,若有余数则剩下几步模拟即可)。注意任意一个置换都可以表示成矩阵的形式。例如,将1 2 3 4置换为3 1 2 4,相当于下面的矩阵乘法:
    
    置换k/m次就相当于在前面乘以k/m个这样的矩阵。我们可以二分计算出该矩阵的k/m次方,再乘以初始序列即可。做出来了别忙着高兴,得意之时就是你灭亡之日,别忘了最后可能还有几个置换需要模拟。

经典题目5 《算法艺术与信息学竞赛》207页(2.1代数方法和模型,[例题5]细菌,版次不同可能页码有偏差)
    大家自己去看看吧,书上讲得很详细。解题方法和上一题类似,都是用矩阵来表示操作,然后二分求最终状态。

经典题目6 给定n和p,求第n个Fibonacci数mod p的值,n不超过2^31
    根据前面的一些思路,现在我们需要构造一个2 x 2的矩阵,使得它乘以(a,b)得到的结果是(b,a+b)。每多乘一次这个矩阵,这两个数就会多迭代一次。那么,我们把这个2 x 2的矩阵自乘n次,再乘以(0,1)就可以得到第n个Fibonacci数了。不用多想,这个2 x 2的矩阵很容易构造出来:
    

经典题目7 VOJ1067
    我们可以用上面的方法二分求出任何一个线性递推式的第n项,其对应矩阵的构造方法为:在右上角的(n-1)*(n-1)的小矩阵中的主对角线上填1,矩阵第n行填对应的系数,其它地方都填0。例如,我们可以用下面的矩阵乘法来二分计算f(n) = 4f(n-1) – 3f(n-2) + 2f(n-4)的第k项:
    
    利用矩阵乘法求解线性递推关系的题目我能编出一卡车来。这里给出的例题是系数全为1的情况。

经典题目8 给定一个有向图,问从A点恰好走k步(允许重复经过边)到达B点的方案数mod p的值
    把给定的图转为邻接矩阵,即A(i,j)=1当且仅当存在一条边i->j。令C=A*A,那么C(i,j)=ΣA(i,k)*A(k,j),实际上就等于从点i到点j恰好经过2条边的路径数(枚举k为中转点)。类似地,C*A的第i行第j列就表示从i到j经过3条边的路径数。同理,如果要求经过k步的路径数,我们只需要二分求出A^k即可。

经典题目9 用1 x 2的多米诺骨牌填满M x N的矩形有多少种方案,M<=5,N<2^31,输出答案mod p的结果
    
    我们以M=3为例进行讲解。假设我们把这个矩形横着放在电脑屏幕上,从右往左一列一列地进行填充。其中前n-2列已经填满了,第n-1列参差不齐。现在我们要做的事情是把第n-1列也填满,将状态转