约瑟夫(Joseph)问题的解决方法
问题描述
最初Joseph问题是这样的:有n个人(分别编号1,2……n)围成一圈,这些人轮流数数(编号为1的人开始,从1开始数),当数到m的人就会被处决。接着,后面的人继续从1开始数数,如此下去直到最后剩下来的那个人才能活命。当然Joseph先生非常聪明,他总是选择能最后剩下的那个位置站位。
用图示来阐述Joseph问题更加直观:如下图,假如有A,B……E共五个人(为了方便描述,这里用字母来对每个人编号),m= 3(即数到3的人出去);则第一个被处决的人是C,接着D报1,E报2,然后A报3,于是A被处决,如此进行下去,E,B又先后被处决,最后活下来的是D,因此最后输出答案就是D(或者输出其下标:3)。
解法一:模拟法
实现:
求解约瑟夫问题最直观(但非最高效)的方法就是直接模拟了。借助循环链表(或者数组模拟的循环链表)来表示约瑟夫环,然后就模拟数数过程,当数到m时就将该元素从循环链表中删除,直到循环链表中只剩一个元素为止!在这里我用数组来模拟循环链表,从而实现约瑟夫环问题的求解,代码如下:
- #include"iostream"
- #include"assert.h"
- using namespacestd;
- int Joseph_1(intN,int m)
- {
- assert(N>0&&m>0); //check theparameter,you can alse use if statement to instead of assert
- int ret = 0;
- int* Loop = new int[N];
- memset(Loop,0,sizeof(int)*N);//Loop[i] =0means the people numbered i are remained
- int leave = 0; //leave record the number of executedpeople
- int index = 0;
- while(leave != N-1)//while N - 1 people areexecuted,then quit loop
- {
- int count = 0;
- while(true)
- {
- if(Loop[index] == 0)
- {
- count ++;//increase thecounter,it means Loop[index] is counting!
- if(count == m)
- {
- Loop[index] =1;//Loop[index]is executed!
- break;
- }
- }
- index = (index +1)%N;//this is thekey point of using array to simulate Loop_List
- }
- leave ++; //increase the number of executed people
- }
- //use a loop to find which people isremained,return its subscript not index!
- for(int i =0;i<N;i++)
- {
- if(Loop[i] ==0)
- {
- ret =i;
- }
- }
- return ret;
- }
- int main()
- {
- cout<<Joseph_1(6,5)<<endl;
- system("pause");
- return 0;
- }
#include"iostream"
#include"assert.h"
using namespacestd;
int Joseph_1(intN,int m)
{
assert(N>0&&m>0); //check theparameter,you can alse use if statement to instead of assert
int ret = 0;
int* Loop = new int[N];
memset(Loop,0,sizeof(int)*N);//Loop[i] =0means the people numbered i are remained
int leave = 0; //leave record the number of executedpeople
int index = 0;
while(leave != N-1)//while N - 1 people areexecuted,then quit loop
{
int count = 0;
while(true)
{
if(Loop[index] == 0)
{
count ++;//increase thecounter,it means Loop[index] is counting!
if(count == m)
{
Loop[index] =1;//Loop[index]is executed!
break;
}
}
index = (index +1)%N;//this is thekey point of using array to simulate Loop_List
}
leave ++; //increase the number of executed people
}
//use a loop to find which people isremained,return its subscript not index!
for(int i =0;i<N;i++)
{
if(Loop[i] ==0)
{
ret =i;
}
}
return ret;
}
int main()
{
cout<<Joseph_1(6,5)<<endl;
system("pause");
return 0;
}
分析:
从上面的实现可以看出来,该方法的空间复杂度是O(N),而时间复杂度至少是O(m*N):因为有两重循环,外层循环N-1次是很明显的,内层循环至少m次!因此最小时间复杂度应该是O(N*m)!
如果不用数组来做,而是用循环链表呢?其复杂度理论上要好点,因为每execute一个人之后链表的长度就会减1,所以,内层循环的次数会逐渐减少,即使这样,总体复杂度仍然逃不脱O(N*N)。
解法二:数学公式法
实现:
上面介绍的是基于直接模拟的方法的时空复杂度都不够理想,这在m或者n非常大时效率尤其低。为此这里介绍一个数学公式法,假设N个人的约瑟夫环的问题的解是f(N),这里特别要理解f(N)的含义:它只N个人的约瑟夫环中最终存活的人的下标,同理可得,f(N-1)是N-1个人的约瑟夫环中的最终存活的人的下标,理解f的含义对理解后面的推导有重要作用,我最初将f(N)理解成N个人的约瑟夫环中第一个被execute的人的下标了,导致我怎么也无法理解后面的推导。现在我们就来推导f(N)的函数式:
首先我们来看看从原始的约瑟夫环到第一个被execute的过程:这时,环中有N个人,从下标为0的人开始从0数数,数到m-1的人就被execute,此人的下标是(m % N)-1(不是m-1因为m很可能比N大)。
当有一个人被execute之后,剩下来的N-1个人继续进行数数(从刚被execute那个人之后的人开始重新从0开始数数),这又是一个规模较小的约瑟夫环问题。但是,要注意,这N-1个人的下标不连续(m % N已经被execute了)并且是从m % n开始数数的。为此我们对剩下的N-1个人的下标做如下映射使得(m% N)-1的后面一个人的下标(假设为K = m % N )映射到0:
K——>0
K+1——>1
……
N-1——>(N-1)-K=(N-k)-1
0——>(N-1)-K+1 =N-k
……
K-2——>N-2
做了如上的映射之后,剩下的N-1的下标即连续又是从下标为0的人开始数数了,根据定义其解是:x’ = f(N-1)。特别要注意的是x’是经过上面映射之后的下标,需要反变换回去才是该人的最初下标。那么如何反变换回去呢?假设x’对应的最初下标是x,则容易得到:x=( x’+k)%N;由此可得:f(N) = (f(N-1)+m%N)%N =(f(N-1)+m)%N。由此要求f(N)只需求f(N-1),求f(N-1)只需求f(N-2),如此下去,直到求得f(1),然而我们直到f(1)恒等于0。因此得到该问题的一般公式:
f(i) = 0; (i=1)
f(i) = (f(i-1)+m)%i(i>1)
根据这个公式可以写出如下两个函数:其中一个是递归,另外一个是迭代:
- #include"iostream"
- #include"assert.h"
- using namespace std;
- int Joseph_2_1(int N,int m)
- {
- assert(N>0&&m>0);
- int ret = 0;
- for(int i = 2;i<=N;i++)
- {
- ret = (ret+m)%i;
- }
- return ret;
- }
- int Recursion(int N,int m)
- {
- if(N==1)
- {
- return 0;
- }
- else
- {
- return (Recursion(N-1,m) + m)%N;
- }
- }
- int Joseph_2_2(int N,int m)
- {
- assert(N>0&&m>0);
- returnRecursion(N,m);
- }
- int main()
- {
- cout<<Joseph_2_1(6,5)<<endl;
- cout<<Joseph_2_2(6,5)<<endl;
- system("pause");
- return 0;
- }
#include"iostream"
#include"assert.h"
using namespace std;
int Joseph_2_1(int N,int m)
{
assert(N>0&&m>0);
int ret = 0;
for(int i = 2;i<=N;i++)
{
ret = (ret+m)%i;
}
return ret;
}
int Recursion(int N,int m)
{
if(N==1)
{
return 0;
}
else
{
return (Recursion(N-1,m) + m)%N;
}
}
int Joseph_2_2(int N,int m)
{
assert(N>0&&m>0);
returnRecursion(N,m);
}
int main()
{
cout<<Joseph_2_1(6,5)<<endl;
cout<<Joseph_2_2(6,5)<<endl;
system("pause");
return 0;
}
分析:
解法2与解法1比较起来最显著的特点就是代码少了很多,当然这个差别还不是最根本的!现在从时空性能来比较一下:
空间上,解法2不需要数组或者链表来保存约瑟夫环,只需要一个临时变量“ret”来保存结果就成,因此空间复杂度是O(1);
再来看时间复杂度,由于只需要一重循环(从2循环到N),因此时间复杂度为O(N)!而解法1的最小时间复杂度都是O(N*m),当m非常大时(可能比N大许多),解法2比解法1效率高很多!
约瑟夫的变种问题:
1.N个人的约瑟夫环,从1开始数数,数到m就被execute。请求出第i(0<i<N)次被execute的人的下标。
2.(POJ1012):k个好人和k个坏人站成一个圈(k个好人连续站在前k个位置),请找到最小的m使得在第一个好人被execute之前所有的坏人均已经被execute了。为了简化,假设0<k<14。例如:当k=3时,最小的m是5;当k=4时,最小的m是30。
转自:http://blog.csdn.net/w57w57w57/article/details/6639194