您的位置:首页 > 其它

Joseph问题的递推公式解法

2008-04-28 09:46 225 查看
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语言实现。


unsigned int JosephCrack(int nPersonNum, int nCountNum)




...{


int nThisStartPos = 0;


int nThisPeopleNum = 0;


int nLastStartPos = 0;


int nLastPeopleNum = 0;


int nLastDeleted = 0;




int nAnswer = 0;




assert(nPersonNum > 0);


assert(nCountNum > 0);




for (


nThisStartPos = 0, nThisPeopleNum = 1;


nThisPeopleNum < nPersonNum;


nThisPeopleNum = nLastPeopleNum, nThisStartPos = nLastStartPos


)




...{


nLastPeopleNum = nThisPeopleNum + 1;




if (0 == nThisStartPos)




...{


nLastDeleted = nLastPeopleNum - 1;


}


else




...{


nLastDeleted = (nThisStartPos + 1) % nLastPeopleNum - 1;


nLastDeleted = nLastDeleted % nLastPeopleNum;


if (nLastDeleted < 0)


nLastDeleted += nLastPeopleNum;


}




nLastStartPos = (nLastDeleted - (nCountNum - 1)) % nLastPeopleNum;


if (nLastStartPos < 0)


nLastStartPos += nLastPeopleNum;


}




nAnswer = (-nThisStartPos) % nThisPeopleNum;


if (nAnswer < 0)


nAnswer += nThisPeopleNum;


return nAnswer;


}

我这个代码主要是为了容易理解递推算法而写的,所以几乎没有优化,而且有很多可以去掉的变量。一个更简洁的代码是我在http://zhidao.baidu.com/question/37329603.html?si=1找到的:


int i, s=0;
for (i=2; i<=n; i++) s=(s+m)%i;

这里n是初始人数,每次报到m的人出列。

当然,设计思路是完全相同的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: