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的人出列。
当然,设计思路是完全相同的。
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的人出列。
当然,设计思路是完全相同的。
相关文章推荐
- 关于Joseph problem(约瑟夫环)问题的解法汇总
- 蚂蚁问题解法----C版本
- 最大公约数问题的优化解法
- Path Sum问题及解法
- Predict the Winner一个动态规划的问题解法详解
- 同一问题的两种不同解法 : MFC8.0 与 C++ 标准库
- HDU ACM Step 2.2.2 Joseph(约瑟夫环问题)
- Pascal's Triangle问题及解法
- 第十二周项目五迷宫问题之图深度优先遍历解法
- 特殊的组合问题(另外一种解法)
- Path Sum III问题及解法
- 九宫问题(八数码问题)的解法
- 机器学习第二课:无约束优化问题(局部极小值的几种解法)(梯度下降法与拟牛顿法)
- 迷宫问题-深度遍历解法
- Map Sum Pairs问题及解法
- NYOJ 191 && POJ 1012 Joseph(约瑟夫环问题)
- 第十一周项目5—迷宫问题之图深度优先遍历解法
- 数组的众数问题的分治解法
- LeetCode Single Number I & II 都符合两个问题额外要求的 通用解法 与 思考过程
- Find Largest Value in Each Tree Row问题及解法