尝试一篇仿m67风格的博客。
你受邀参加一场女装聚会,除了你和聚会组织者之外还有n个人参加,这些人你都不认识。
组织者告诉你:这场聚会上所有人都会穿女装,但其中有一部分是真妹子,另外一些则是女装大佬。
作为直男,你的目的显然是去欣赏妹子而不是女装大佬。然而到现场之后你慌了——单身多年的你竟然分辨不出哪些人是妹子。泡到一个女装大佬显然是一个让人想想就菊花一紧的事,你又不想白来一趟,于是赶紧向组织者求助。
你以为你会很快得到想要的答案?哈哈你想得太简单了,这种魔术揭秘一般的事怎么可能随便告诉你,不然聚会的乐趣全无。不过,看着你饥渴可怜的眼神,组织者还是决定帮你一把:
不妨把所有人按照1~n编号。每次你可以向组织者给出两个编号集合,每个集合里元素不能重复(但两个集合可以有相同元素)。组织者会告诉你:其中某个集合里的妹子数量,一定不少于另一个集合。
举个例子:你向组织者询问集合{1,2,3,6,8}和{2,3,4,5},组织者回答第二个集合,就代表集合{2,3,4,5}中的妹子人数一定不少于集合{1,2,3,6,8}中的妹子人数。
需要注意的是,如果两个集合妹子人数一样,则组织者无论回答哪个集合都是合理的,此时他会任选一个集合回答你。
组织者信心满满地认为,即使他给出了这些信息,你还是无法确定出所有人的性别。他是对的,举个最简单的例子,当n=1时,你显然无法得到任何有用的信息。于是,他又向你透露了两个关键信息:
1、这场聚会中至少有一个妹子,也就是说绝对不会所有人都是女装大佬。不过不排除所有人都是妹子而没有女装大佬的情况。
2、他告诉你了场上妹子人数的奇偶性。
组织者认为即使这样你还是没辙,谁知你却已经有了一个方案,能问出所有人的性别!不仅如此,你还打算用最省事的方式问出答案。这里的“最省事”不是询问次数最少,而是询问的总人次数(也可以看做所有询问的集合大小总和)最小。
试问:你有什么好的方案?注意你给出的方案应该能在任何情况下(比如,你可以假定在所有两集合相等时都回答对你最不利的答案)都能给出正确答案。
这题第一眼看上去比较简单,不过在多次尝试之后,想必你已经发现了:“两集合相同时返回任意一个”这个设定远比你想象中的坑得多。比如,你永远无法直接询问两个集合来得到它们之间准确的大小关系——即使你询问了许多次得到的结果都相同,你又怎么能确定,确实是这个集合更大,而不是两个集合相等而你运气背到了一定程度呢?(别忘了,你需要考虑的是对你最不利的情况。)
在接下来的叙述中,我们不妨把问题抽象成如下模型:我们要确定n个01变量的值,其中某个变量为0代表女装大佬,1代表妹子,并定义集合之间的“大于等于”运算指的是集合中1的个数之间的比较,定义询问总代价为所有询问的集合大小总和。
首先一个自然的想法是,如果把所有人两两比较一次会怎样?每个人都会进行n-1次比较,并且可以发现一个关键的性质:对于任意一个值为0的变量x和任意一个值为1的变量y来说,x大于等于其他元素的次数一定比y大于等于其他元素的次数多。
这是显然的,不妨设所有变量中有a个的值为0,则任意一个0最多只能大于等于a-1的元素,而任意一个1至少能大于等于a个元素。
因此,如果把所有变量按照大于等于其他变量的次数进行从大到小排序,则一定存在一个分界点,它的左侧全都是1,右侧全都是0。
问题是,怎么确定这个分界点在哪里呢?别忘了,保证至少存在一个变量为1,所以排在最靠前的元素一定是1。接下来,我们就可以枚举每一对相邻的元素,把它们放在一个集合(记做S),把这个确定的1放在另一个集合(记做T)。
接下来,有3种可能的情况发生:
1、这两个相邻的元素都是1,则一定会回答T。
2、这两个相邻的元素都是0,则一定会回答S。
3、这两个相邻的元素是一个1一个0,则返回S或T都有可能。
换句话说,对于两种可能的回答,我们能得到的信息如下:
1、如果回答的是T,说明两个元素是2个1,或者一个1一个0。
2、如果回答的是S,说明两个元素是2个0,或者一个1一个0。
由于我们已经知道了当前的序列一定是前面一段1后面一段0,所以询问结果也一定是前面一段T后面一段S。问题是,中间分界处有一处0和1相邻的情况,回答序列也有一处S和T相邻的情况,如何知道此处具体哪个回答对应的是一个0一个1的情况?
奇偶性!别忘了我们还有这么一个条件。因为中间的不确定性因素最多只有1,再结合1的个数的奇偶性我们就能得到一个准确的答案。
此时我们终于得到了一个确定性的解答,它的询问总代价是O(n^2)级别的。还能做得更好吗?
细心的读者可能已经发现,最后一步枚举所有的相邻对其实是没必要的。因为我们已经知道询问的结果必然是一段T一段S,我们就可以通过二分的方法确定分界点的位置在哪里。不过,这里的优化只是把一个O(n)变成了O(logn),我们还是需要O(n^2)级别的总代价。
但是……再仔细想想,我们为什么会得到一个“询问的结果必然是一段T一段S”的结果?本质原因是我们把所有元素排成了前面一段1后面一段0……排序!我们第1步花O(n^2)的代价,本质上只是将这n个元素排了个序而已。不过我们既然可以直接对两个元素进行比较,为什么不直接进行排序呢?
众所周知,不少基于比较的排序算法的比较次数都是O(nlogn)级别,利用我们的“大于等于”运算,当然可以直接实现一个基于比较的排序。这样,我们就把总代价降低到了O(nlogn)级别。
还能更优一点吗?注意到基于比较的排序复杂度下界也是O(nlogn)级别,因此这种方法也就走到头了。
当时我做这个题的时候,认为O(nlogn)级别的代价就是最优方案了。事后才得知,居然有更优秀的方案,可以把总代价降低到O(n)级别!究其原因,还是因为我们的权值只有01两种,正常排序的话使用计数排序显然是O(n)的,我们却需要基于比较的O(nlogn)排序,这显然是一种浪费。
首先考虑我们能否找到一个值为1的元素出来。这本质上就是在找最大值,利用大于等于运算,我们能轻松地在O(n)的代价之内找出来。设这个元素为q。
然后呢?我们就可以利用这一个确定为1的元素,逐步确定其余所有的元素。
首先,如果当前仅剩1个未确定的元素,我们容易根据奇偶性确定出它的值。
否则,任取两个未确定的元素x,y,首先对它们进行一次比较,不妨设x>=y。
然后,再令S={x,y},T={q},对S和T进行一次比较。
如果S>=T,意味着x和y中至少有1个为1。
如果T>=S,意味着x和y中至少有1个为0。
看起来我们还是不能确定每个元素的值,然而这个算法的最精妙之处就在这里:
以S>=T为例,此时x和y中至少有1个为1。然而我们又有x>=y,这意味着,如果x为0,那么y也必须为0,这就会产生矛盾。于是,此时x必须为1——尽管我们仍然不知道y的值,但是x的值我们可以确定了!
同样,如果S<=T,也可以推出y必须为0。
这个思路不可谓不令人惊讶——通过如上的操作,无论是x还是y我们都不能保证一定能求出它的准确值,但我们一定能求出其中某一个的值!
同时注意到进行一次上述操作的代价为5,因此求出所有元素的值,总代价是O(n)的。
这么长的文章看到这里也不容易,相信你如果还没被绕晕的话也一定会赞叹这算法的精妙。此时我说,这种算法还不是最优,你也多半不会相信吧?毕竟我们已经把总代价降到了O(n),而显然这也是下界了。
但是,真的有比这还优秀的方案!之前我们的算法虽然是O(n),但它还有常数项优化的空间。准确地说,由于第一步找最大值的代价约为2n,第二步每确定一个元素的代价为5,因此总代价约为7n。接下来,我们将进一步突破这个下限。不过由于接下来的算法将会较为复杂,建议先完全理解上述算法之后再继续看下去。
我们尝试把第一步那个找最大值的过程去掉,随便取一个元素作为基准值。
那如果这个元素为0怎么办?不慌,我们冷静分析一下。
取元素a,b,c,假设b>=c,构造集合S={b,c},T={a}。
如果S<=T,则不管a的值如何,b与c中均不可能都是1。此时,我们依然能确定出c=0。下一次操作时,保持当前的a不变,另取一个新的元素,与原来的b作为新的b和c即可。
如果S>=T呢?如果a是1,我们就已经能知道b一定是1了,然而现在a的值不确定,因此我们还不能给出“b一定是1”的判断。
然而,“如果a是1,我们就已经能知道b一定是1了”这话意味着什么?意味着b>=a!也就是说,我们没有直接比较a与b,就凭空得出了这一组大小关系。我们把这组关系记录下来,紧接着用b去替换a,再取一个新的元素,与原来的c作为新的b和c。
反复操作直到所有元素都使用了,此时我们会得到什么?若干个已经确定为0的元素,一连串元素x1x2...xk满足x1<=x2<=...<=xk,还有一个额外的元素是最后一次操作剩下的,记做p。
容易发现,在xk和p之间一定存在一个元素的值为1,因此我们可以用一次比较确定一个1。
那么其余的未确定元素呢?我们发现它们已经天然地排成了有序的,除了最多一个可能的元素游离于这条链之外(即元素p,如果p<=xk的话)。我们就可以利用之前提到的二分算法在链上找到一个分界点,对于分界点处的元素和p,我们也可以用至多1次额外比较和奇偶性确定出来。
因此我们终于得到了这题的最优解:总代价5n+O(logn)的优秀算法。(你还有其它更优的解吗?欢迎指出。)
唉,如果现实中真有这种聚会该多好……