漫话二分

  二分思想真的是无所不在,即使在中文系的专业课中我们也能见到这个词。在语言学概论中我们提到,一个音位可以由一组区别特征确定下来,这些区别特征总是以只具有“是/否”、“有/无”等两种对立属性的“二元偶分组”形式存在,因为这样可以最方便最快捷地确定出一个元素。这有点像猜数字一样,我想一个数字后让你来猜,我告诉你你的猜测是大了还是小了。只是在这里,回馈的信息不再是大小,而是“辅音/元音”、“口音/鼻音”、“浊音/清音”、“送气/不送气”等形式逐层细分。这让人联想到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


    另一个经典的变形则是分段有序队列中的二分查询。假如有这么一个数列,它可以分为前后两个部分,两段各是一个递增数列,并且后一段的最大值比前一段的最小值还要小。比如说,数列12, 15, 19, 3, 6, 7, 9, 10就是这样一个数列。这相当于是一个有序数列循环移动之后的结果。如何在这个数列中查询指定的元素呢?事实上,这种“有序序列”虽然经过了变形,但丝毫不影响二分法的应用,因为我们依旧能判断出目标值在当前值的哪一边(这是很显然的,我不多解释了),这就已经足够了。
    不结合实际应用的话,这些似乎没有实用价值的理论会变得乏味。其实,只要仔细思考,生活中对应的现象总是有的。我的秘方就是,想不出例子就想MM,爱情的复杂性保证其蕴含了各种千奇百怪的数学模型。一想到爱情,分段有序队列就能用上了。不妨为恋爱前后的“愉悦程度”建一个简单的模型:在恋爱之前,你会为找不到MM而越来越难过;一旦开始热恋愉悦值瞬间达到极大;之后热情会慢慢减小,但愉悦值始终比恋爱前要大。好啦,如果你想出一道题的话,问题背景已经是现成的了,不妨再定义一个符合这个模型的且不能直接解出来的分段函数,编几句形如“科学家发现恋爱前的愉悦值以a减某某某的速度递减,恋爱后则变为曲线a加某某某”的话,然后就来看看有多少人还能想到二分法吧。一般说来,好的题目背景起到了一个很强的干扰作用:题目背景越顺理成章,问题描述越是简单,看清问题背后隐藏的算法障碍就越大。另外,如果你给的函数巧妙到还需要大家先证明它的单调及有界,那这题目就真的绝了。

    要比谁的题目更绝,那绝对比不过USACO。USACO月赛中的二分题是真牛B了。我对二分的热情是相当的高。为高一高二的几个人备战省选时,我出了好几套模拟题,前面四套题每套都有一道来自USACO月赛的二分题。这个二分题是越来越诡异,以至于大家越来越难看出这套题里面哪道要用二分了。
    第一套题里的二分题是一个简单题:把一个长为N的数列划分为M段,要求每段数之和的最大值最小。例如,把100 400 300 100 500 101 400分割成5块,则100 400 | 300 100 | 500 | 101 | 400是最优方案之一,最大值500已经不能再小了,这个题一看就知道是二分后贪心判断,“最大值最小”之类的关键词几乎成了二分题的信号灯。像什么最小权值最大的完全匹配、瓶颈生成树问题(求最大边最小的生成树:二分后判断连通)、寻找权值波动最小的路径(找一条从A到B的路径使得所经过的边的最大权值和最小权值相差最小:枚举下界二分上界判断连通,滑动窗口更好),都是二分的经典问题。
    第二套题中的二分也是“最大值最小”类的问题,只是要更复杂一些:在带权无向图中,选择一些边使得A、B两点连通,要求费用最小。费用是这样算的:先从图中选出K条边(K值是给定的),免费;然后从图中选择其它你需要的边,费用为这些边的最大权值。这个题里,选边过程的先后顺序有一个很强的误导作用。事实上,正确的算法应该是先二分这个最大权值,再来判断把K条免费边的机会用上能不能把A、B连通,换句话说就是要想把A、B连通还差的边数超没超过K。当时做这个题时,不少人二分法是想到了,但却怎么也想不到该如何计算最少还需要多少条边才能连通A、B两点。其实方法很简单,把不超过当前二分出来的那个“最大权值”的所有边权值设为0,其它边的权值设为1,然后找一下最短路就可以了。
    第三次的二分题就不容易看出来了。给定一个有向图,每条边都有两种权值,时间和愉悦值。叫你寻找一条回路(不经过重复的边),使得愉悦值之和与时间总和之比最大。能想到这个题目是二分的话,那就真的很厉害了。二分最优比率C,然后给每条边设置一个新的权值,它等于C倍的时间减去愉悦值,再来判断是否有负权回路。这是怎么来的呢?不妨这样来看:对于任意一条回路,所求比率等于(Σ愉悦值)/(Σ时间)。如果二分出来的最优比率C偏小了,那说明满足(Σ愉悦值)/(Σ时间)>C的回路多得是,移动一下便有C*(Σ时间)-(Σ愉悦值)<0,即在新的权值设定下存在负权回路。如果没有负权回路的话,说明所有回路的比率都比C小,这就说明我们的C取大了。同样的思路也可以用来解决最优比率生成树问题。

    第四次的二分题可就是真的牛B大发了。假设有一个长度为N的数组A_i,里面的每个数都不一样。但是呢,你不知道数组里的数是多少。给出若干个形如“从A_i到A_j中的最小值是x”的命题,问你第一个和前面有矛盾的命题在哪里。例如,给你四个命题:A_1到A_10的最小值是7,A_5到A_19的最小值是8,A_3到A_12的最小值是5,A_11到A_15的最小值是4。第三句话显然是错的,否则前两个区间中至少有一个的最小值也达到了5。
    这个题难就难在,我们需要挖掘出“矛盾”的本质。究竟每一条命题给我们带来了什么信息呢?假设我告诉你,A_1到A_10的最小值是7,你仍旧不能推断出任何一个数,但有两点是肯定的:第一,这10个数里有一个数字7;第二,这里面的每个数都不能小于7。假设我们再给出一个A_5到A_19的最小值是8呢?此时,我们得知A_5到A_19里面有一个8,并且A_5到A_19的所有数都不小于8。注意!此时从A_5到A_10这一段中的数字下界升级了!由此得到启发,这些条件给出的信息说穿了就是每个位置可能出现的数的下界,它就是覆盖它的那些区间中的最大值。我们可以把区间端点排序,从左到右扫描一遍,用堆不断更新当前的最大值。在确定完每个位置可能的最小数后,我们开始寻找一个满足这些条件的解。由于数组中没有相同的数,因此对于所有回答“最小值为x”的区间,x必需出现在它们的交集中。如果这个交集为空,或者交集里面所有的位置(因下界过大)都不能取这个数,那么我们就可以肯定地说这一组条件是有矛盾的。如果我们顺利地给每个区间都安排好最小值所在位置,这立即说明了该组条件没有矛盾,因为我把其它那些没确定下来的位置取到无穷大,满足全部条件的数组就构造出来了。于是,我们有了一个O(nlogn)判断一组命题是否有矛盾的算法。
    这个题有趣就有趣在,上述算法虽然高效,但却只能判断命题组是否矛盾,不能检测矛盾首次出现的位置;而在线判断命题是否矛盾(一个命题一个命题地往里面加)反而要慢些。于是呢,二分答案就派上用场了:二分前面的无冲突命题的最大长度,然后用上面的O(nlogn)算法来判断看是不是有矛盾。

    然而,上面这些二分题都还太“正统”了一些。拼完了NOI之后,我便在网上自由潇洒地学习各种自己感兴趣的算法,看到了不少真正另类而绝妙的二分……   很多问题并不完全符合二分的模型。最常见的一个情况估计应该是无穷长的有序队列中的二分查找问题。例如,前文所提到的求解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塔与世界末日……无数火星的例子告诉我们,倍增的力量是异常强大的。如果你不信的话,下次来北京时请我喝酒,不妨这样来试试:先我喝一杯,你喝一口;然后我喝一杯,你喝两口;然后我喝一杯,你喝四口……看咱俩谁坚持的久。


    倍增法有许多出乎意料的神奇应用。有兴趣的读者不妨去看一看Chan凸包算法,这是我所见过的倍增法最诡异的应用。

    另一个不符合二分模型的相关问题是:如何寻找凸函数上的极大点?生活中的很多东西都是这样,大了也不好,小了也不好,不多不少的时候最好。我最喜欢举的例子是,粉笔短了不好写且用得快,粉笔长了又容易断;为了贯彻拿MM打比方的精神,这里可以再举一些例子来说明这一情况的普遍性:陪MM出去玩的次数多了很快会腻,陪MM次数少了又会疏远;把握火候贯彻“半糖主义”方针是非常重要的。事实上,从硬盘缓存的大小到初期农民的个数,从每学期的学分到论文的长度,生活中几乎所有东西都是这样,就连饭量和睡眠时间也是。这些例子说穿了就是一个单峰函数,我们需要用尽可能少的试验次数快速找到极大点。永远不要以为决策者们面对的都是高中数学考卷上的“每涨10块钱就会少100个消费者”一类的屁话,这些屁话都是用来编二次函数题目的。现实生活中企业做决策时,样点实验、不断取舍、逐步逼近最优点仍然是最实在最有效的手段。考虑到我们今天的话题,我们首先要做的是,想一个可以让规模层层递减的方法。这个办法貌似不错:从函数中选择两个点x、y(无妨假设x<y),如果f(x)<f(y),那么去掉所有比x还小的部分;反之若f(x)>f(y),那么y点的右边都可以抛弃掉。不管这两个点是在极大点的同侧还是异侧,抛弃较小值那一侧都是正确的。另外,如果不巧f(x)=f(y),这说明极大值一定在它们之间,去掉哪一边都是可以的。

   

    问题的关键就是,每一次我们的x和y取在哪里最合适?或许有人会说,既然有两个点,那就对称地取在三等分处吧。这样的话,每次问题的规模都降低到原来的2/3。当然,更好的办法是把两个点都取到中点附近,相互之间的距离充分小;这虽然不那么美观,但每次的规模都将变为原先的1/2+ε,效率上说显然更好一些。最聪明的做法是,把x和y两点取在一对既不远也不近的合适位置上,使得其中一个点(比如y)被去掉后,x点所在的位置正好可以作为下一个测试点(于是每一轮新的实验就只需要取一个点了)。这种“循环利用”旧测试点的做法将更加节省资源。为了保证这一过程可以永远进行下去,我们希望让x在y点去掉后的那一段上的位置与y点原先在整段上所处的位置比例相同,用算式写出来就是y/L=x/y,即(L-x)/L = x/(L-x),解出来x=(1±√5)L/2。神奇!黄金分割竟然出现在了这样一个意想不到的场合中!这就是所谓的“优选法”:不断在两个黄金分割点处进行测试,抛弃较次的测试点;由于黄金分割点的性质,另一个点仍然处于余下部分的黄金比例处,因此只需再对该小段的另一黄金分割点进行测试即可继续如此递归地操作下去。

 
 
    二分的应用范围还很广。这里我们再举四个简单的例子,它们是二分法的各种极其特殊的用法。首先,我们来看看交互式问答题中的一个奇妙的二分。黑箱子中有一个竞赛图(任两点之间都有一条单向边)。询问两个顶点,返回它们之间的边的方向。请用尽量少的询问次数找出一条经过全部n个顶点的路径。

   

    竞赛图有一个神奇的性质:经过所有顶点的路是一定存在的。换句话说,如果n个人之间两两进行比赛,假设比赛没有平局;那么我们一定能够给这n个人排出一个顺序,使得第一个人打败了第二个人,第二个人打败了第三个人,一直到倒数第二个人打败了最末一个人。这个证明是构造性的。从任意一条边出发,我们可以不断扩展出越来越长的链。假设现在我们已经有了一个长度为k的链,它们的节点分别是P_0, P_1, ..., P_k。现在我们想把节点P_t加进来,使得这条链的长度达到k+1。注意到,如果有边P_t → P_0或者P_k → P_t,那就直接完事儿了。否则的话,我们必须要找出链中的一个P_i,使得P_i → P_t并且P_t → P_(i+1),然后将P_t插到P_i和P_(i+1)中间。这样的P_i是一定存在的,因为此时P_0到P_t的边是正向的,但到了P_k时从链到P_t的边却变成逆向的了,这说明中间至少有从正向边变成逆向边这一步。换句话说,给你一个长度为k的01串,首位为0,末尾为1,那中间至少出现了一次子串“01”。
    按照这种方法做,最坏情况下我们需要询问O(n^2)条边。这相当于把整个图都给你了。有更好的办法吗?有!我们可以用二分法寻找P_i,从而只需要O(nlogn)次询问即可得到答案。具体地说,每次需要寻找那个P_i时,我们二分P_i的位置:如果有P_i→P_t,则P_t一定能插入到P_i后面的部分;反之如果P_t→P_i,则需要寻找的目标节点一定在它前面。用01串来解释似乎更清楚一些:由于每个首位为0末位为1的子串都包含至少一个“01”,于是我们看中间那一位数。如果它是“0”,即可抛弃前面那一段;如果它是“1”,则后面那一段就可以不管它了。

 
    接下来,让我们看一看二分法在两个有序数组中的应用。在多个有序数组中查找指定的数不好玩,真正好玩的是在多个有序数组中查找指定序数的数。给你两个有序数组,如何快速找出第k小的数?合并两个有序数组将导致线性的复杂度,这里还有更好的算法吗?不啰嗦了,我直接给出答案:分别取两个数组正中间的那个数a和b,不妨设a≥b。两个数组被划分为四段,分别记作A1、A2、B1和B2。如果A1的长度与B1的长度加起来还没有k大,那所求数绝对不可能在B1里面(任选B1中的一个数,比它小的只有A1和B1各自的一部分数,数目远不到k);反之如果len(A1)+len(B1)≥k,那么所求元素必然不在A2中(因为A1和B1里的所有数都比A2里的数小,而它们就已经有k个了)。对于两种情况,我们都可以将某个数组的长度减半,从而得到log级别的算法。

1 2 3 6 7 8 14 *16* 18 19 20 23 27 28 33
|<--- A1 --->|  a  |<------- A2 ------->|
 
4 5 9 10 11 *12* 14 17 22 24 29
|<-- B1 -->|  b |<---- B2 ---->|

 
    在竞赛题目中,大家见过最多的当属二分加贪心检测了。当然,偶尔也会遇到二分加最短路、二分加匹配、二分加网络流、二分加动态规划之类的题目。但是,大家有想过二分加二分的题目吗?前不久,我就见过这么一道“两次二分”的题目:给你n个数,这n个数两两的差值将产生n(n-1)/2个数。试求这n(n-1)/2个数的中位数。算法:二分答案加二分检测。先二分这个中位数,然后数一数比该数小的有多少,比该数大的又有多少,以确定这个“中位数”是取大了还是取小了。为了判断有多少个数比选定的中位数小,我们需要再一次使用二分:对于每一个固定的数A_i,二分A_j的位置,看A_i-A_j比中位数大还是小(j=1..i-1,假设数列已经有序)。假设n个数中的最大值为q,则算法的复杂度为O(n*log(n)*log(q))。

    有时候,二分的作用甚至根本就不是为了减小时间复杂度。某天在阅微堂看到一个非常有趣的题目。假设有两台计算机,每台计算机上都存了一个长度为n的字符串。两台计算机之间的数据传输是非常昂贵的。因此,我们希望用最少的数据传输量来确定,哪台计算机上存储的字符串的字典序更大。
    答案:一个基于概率的二分算法。两台机器各自算出前半段字符串的md5值,然后机器A把它的md5值传过来和机器B比。把比较结果传回去。若两个md5值不等,表明第一个不同之处一定在这里面发生,此时即可抛弃后面一半;若两个md5值相等则说明不同的地方一定在后面,前面这一半即可舍去。(补充一句,这里的“一定”并不是真的一定,md5判重是有概率因素的。)

    二分这个话题太有趣了,好玩的东西说也说不完。欢迎大家在下面留言,分享你所见过的最有趣、最奇妙、最不可思议的二分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值