Joseph问题是算法领域的一个经典问题,虽然其抽象意义很简单,但曾被改变成各种千奇百怪的、貌似具有“故事性”的问题,被各高校(或许还包括一些特殊的中学)的数据结构课程或者算法课程的老师作为作业布置给学生们。
Joseph问题,音译为“约瑟夫问题”,大意是N个人排成一个环,按顺时针(或逆时针)编号0,1,2,...,N-1,从编号为0的人开始从1报数,报到M的人就出列,出列者的下一个人从1开始继续报数。问最后剩下的那个人的编号。
简单的方法当然是“模拟”,也就是用一个循环链表或者数组来实现(前者“出列”复杂度O(1),“报数”复杂度O(M)[因为链表不支持下标随机访问];后者“出列”复杂度O(N)[因为出列者后面的人要依次前移一格],“报数”复杂度O(1))。此类解法也是大多数老师布置给学生做的解法,而网上这类解法已经铺天盖地了。
对于链表的模拟运算,一个显而易见的优化当然是M%=k(k是当前剩余的人数),另一个显而易见的优化是使用双向链表(即支持反向报数),可以进一步减少到M%(k/2)。
对于数组的模拟运算,优化的方法是使用支持能在O(logN)的复杂度下随机访问和删除的数据结构,比如STL中的set或者Python中的list。当然,如果要自己写这种高效而复杂的数据结构而不出错,对大多数人而言是个艰巨的挑战。
显然,优化过的数组型(只是为了容易理解而沿用了此名词)模拟运算的时间复杂度已经很低了。但是模拟算法的一个致命弱点是空间复杂度太高,达到O(N)。当N超过1G的时候,模拟运算就是空谈了。
Joseph问题目前最高效的解决方法是递推(确切说是反向递推)公式解法。设计思路是:
最后还剩1个人时,把那个人记作X。此时如果开始数,则必然是从X开始了。而此时环中人数为1。
现在,对于任何一个状态:剩余人数n,当前从X之后(按照报数的顺序)的第p(<n)个人开始报数(上一个出列的人当然就在X之后的第p个位置上,而现在开始报数的那个人在上一个出列者还在时位于X之后第p+1个位置),总能用公式计算出q,从而得到上一个状态:剩余人数n+1,当前从X之后第q个人开始数。从n和p得出q的公式很简单,在下面的程序中能看到。
总之,用这个方法我们可以在N-1步后得到:如果最后剩下的是X,则在最开始(还有N个人)时第一个报数的人是X之后的第t个。这里t就是经过N-1次推导得到的最后的q。
而当前有N个人,按照题意,X之后的第t个人就是编号为0的那个人,我们自然可以得到X的编号了。下面是上述方法的纯C语言实现。
... {
int nThisStartPos = 0;