在上一篇文章中,我们聊到了约瑟夫问题的定义,以及其用环的数学模型来建模,最后用循环单链表的数据结构解决该问题等内容。这些只是基本内容,当我们有了数学模型把这个问题变成数学问题以后,就可以在数学结构内去研究更多的东西,而不局限于仅仅求出最后那个被杀的人,比如:
在约瑟夫问题中,我们还可以关心几个内容:
1. 被移除的节点的顺序如何?
2. 最后一个以及倒数若干个留下的节点编号是多少?
本质上,我们希望对约瑟夫问题更细致地每一步到底发生了什么进行更细致的观察。具体就是,到底一个原始环上的元素,是以怎样的排列顺序给移出去的,有怎样的规律,这个搞清楚了,其他的问题都是这个问题的子问题,包含于此。因此,除了把以上过程用状态机模型和数据结构模拟出来以外,我们可以从数学角度继续探讨下,其中有着怎样的规律可循,利用这些规律又可以怎样地化简以上的过程。
约瑟夫问题通项公式猜想
设n为初始的人数,编号0:(n - 1),每k人(对应的每次跳过的是k - 1个人)剔除1人,默认第一次剔除的是前k人中的最后一个,如果要先剔除或者第一次仅空少于(k - 1)人就进行剔除,那么直接等效为执行完这部分偏移的相位后,再等效考虑接下来的步骤。假设最后被剔除的人满足函数关系f(n, k),无论f多么复杂,但结果确实仅受n和k两个参数的影响,最后都会执行到唯一的结果,这恰好是函数的定义。
但这个函数的解析式并不好写,我们得想点办法。研究问题的一个基本思路是,先从小规模,特例情况来找规律,再慢慢地扩展类比到复杂情况。首先我们来看一下k = 2的情况。
我们可以列举一下不同n时候的结果:
n = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
f(n, 2) = 0 0 2 0 2 4 6 0 2 4 6 8 10 12 14
你看出其中的规律了吗?
没错,当n按照自然数列增长的时候,f(n, 2)会是以2为公差,0为起点的等差数列的拼接,长度分别取1,2,4,8的2的指数序列。那发现了规律,自然想写一下通项公式,这是小学学找规律,填数字的功夫呀!
第一次写出来,发现还真有点别扭:
令n = 2 ^ m l,其中l in [0, 2 ^ m - 1],则f(n, 2) = 2l.
这显然是一个漂亮而简洁的结论。但是这个写法有点怪怪的,非得把自变量n拆成两个部分,有些不完美,似乎离本质和优雅还有一点距离。其实可以看到,假设中对n的分解,其实相当于取其二进制表达的最高位,其位值记为(m 1),剩下的l其实是去掉其最高位1以后的大小。而结论2l实际上相当于把原来n的二进制表达结果直接左移1位!因此,该结论还可以写成下面的形式:
令n的二进制表达为1b1b2......bm,位数为(m 1),有f(n, 2) = n << 1 = b1b2......bm0。
所以,求解半天的约瑟夫过程的结果,竟然最后是个移位操作!
注意这里特意强调了该二进制所占位数就是其最高位的位数,也就是最高位1(只能为1)的位置,前面也不再补任何固定总位数或者固定数量的0了,这样才能在左移操作中正确地溢出。实际上,有溢出,就不是理想的四则运算了,真实意思是模 2 ^ bit_num加法群上的四则运算了。一般时候,没有溢出,我们想的是去用这个阉割的模加法群上的运算来模拟那个无穷大的自然数集合上的运算;但是一旦溢出就不符合了。换句话讲,你永远要知道,计算机只能近似模拟数学里的极限啊,无穷啊这些概念,从来都不能真的表达这些概念,比如pi,根号2,等等。
到目前为止,都是基于观察得到的归纳结果,从中可以得到一些直觉上的理解。接下来,我们再仔细分析一下约瑟夫问题的过程,看看这个结论为什么会成立。
约瑟夫问题通项公式理解
原始的约瑟夫问题的每一步仅往后处理一个人,这也是前面代码模拟的思路。但是显然,因为这里明显的同余,周期的结构,我们有更深的规律可以挖掘,一方面利用数学规律可以加速算法,另一方面也可以对背后的数学逻辑有着更深的理解。
环的结构本身是没有起点和终点之分的,但是我们选择约瑟夫执行过程的起点的时候,或者命名某个节点值为0的时候,就无形给了一个起点。于是,我们就有所谓一个执行周期的概念,即从0开始,再次执行到0之前的n个步骤(如果是扑克牌叠,那就是从第一张执行到最后一张)。显然,每个周期内,如果都是从0开始的自然序列编号,那么杀掉的人的编号p满足(p 1) == 0(mod k),也即其除以k的余数恰好仅比k小1的时候,如果从1开始编号,那就是整除的时候。剔除个数为[n / k],编号分别为k - 1,2k - 1,……,[n / k]k - 1。
而这里,k = 2,情况是相对简单的,即每次都剔除编号为奇数的人,留下偶数的人,即二进制末位为1的剔除,为0的留下(如果从1开始编号则反过来)。显然,第一次剔除后,我们想要考察的f(n, 2) = b1b2......bm0留下来了。接下来,我们看第二个周期,容易看到,下一个周期所剔除的结果,有两种情况:
当n为偶数时,bm = 0,上个周期剔除的恰好是原序列的最后一项,新的周期刚好从原来的第0项继续开始,没有相位错位。不过,现在留下的二进制尾数都为0,也就都是2的倍数,公差为2,如果集体右移1位,恰好又恢复成一个0开始的自然数列,和前面满足完全一样的剔除规律,只留下尾部为0的数。实际上,两轮以后,就是那些倒数第1,2位都为0的数被留了下来,其余01,10,11的尾数排列组合都被剔除了。注意,此时bm = 0,所以我们考察的那个值的倒数第2位因为移位运算的关系,也恰好是bm = 0,由此在人数为偶数时候的下一个周期成功躲过一劫。其剩余数的总数也相当于n / 2,也是对应二进制值右移1位的效果。
如果是这样,事情不就简单了么。但是能否让下一轮剔除和前面满足一样的规律,完全取决于当时还剩下多少元素。偶数就如上所述,但是奇数的时候,怎么来理解呢?很简单,n为奇数的时候,bm = 1,因为第一轮最后一个杀掉的不是最后一个,而是倒数第2个,所以,当开启下一轮时,原来编号为0的那个元素第一个被剔除,接下来所有末位为0的都被剔除。而我们考察的数的这一位是bm = 1,还是成功躲过了下一劫,完美。这时候,一轮剔除剩余的等效序列依然是0开始的连续自然数列,二轮剔除完以后成了1,3,5,7......我们对这些数仍然采用右移1位的做法,因为都是奇数,所以相当于减1除以2后,还是恢复成我们想要的0开始的自然数列了。而总数变成(n 1) / 2,右移1位还要加1。
注意下我们这里的等效思路,其实就是规模的归约,看看能不能把原问题,甚至其超问题,转化为规模更小的问题,以实现递归求解。
于是,在模拟执行的过程中,相当于每次给定n的值,以及开始执行的相位,我们去考察其执行结果,即n的更新以及我们要的数是否没有被命中。至于序列,都默认通过移位操作化归成0开始的自然数列,直到(n - 1)。
完整来看,其实还有当某一轮剔除就是从0开始时候,n分别为奇数和偶数的两种情况。为奇数,其变为(n - 1) / 2,直接右移,偶数则n / 2,也是直接右移。
到此,k = 2时候,约瑟夫问题剔除元素的全过程模拟的递归规律就已经十分清楚了,大家可以对照一下结论,来理解一下结论成立的深层机理,会发现,k = 2的时候能这么写,完全就是上帝构造的巧合。当k > 2的时候,这个优美的结论就不复存在了,哪怕你用对应的进制数来表达也没用。
如果你还没想清楚,也没有关系,下一期,我们会把今天分析的内容,用严谨的数学语言全部表达出来给你看,你也可以先想好,然后和我想的对照一下看看,有否互相借鉴的地方。