joseph环的数学解法

 

约瑟夫(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时就将该元素从循环链表中删除,直到循环链表中只剩一个元素为止!在这里我用数组来模拟循环链表,从而实现约瑟夫环问题的求解,代码如下:

  1. #include"iostream" 
  2. #include"assert.h" 
  3. using namespacestd; 
  4. int Joseph_1(intN,int m) 
  5.     assert(N>0&&m>0); //check theparameter,you can alse use if statement to instead of assert 
  6.     int ret = 0; 
  7.     int* Loop = new int[N]; 
  8.     memset(Loop,0,sizeof(int)*N);//Loop[i] =0means the people numbered i are remained 
  9.     int leave = 0;        //leave record the number of executedpeople 
  10.     int index = 0;         
  11.     while(leave != N-1)//while N - 1 people areexecuted,then quit loop 
  12.     { 
  13.         int count = 0;     
  14.          while(true
  15.         { 
  16.             if(Loop[index] == 0) 
  17.             { 
  18.                 count ++;//increase thecounter,it means Loop[index] is counting! 
  19.                 if(count == m) 
  20.                 { 
  21.                     Loop[index] =1;//Loop[index]is executed! 
  22.                     break
  23.                 }     
  24.             } 
  25.             index = (index +1)%N;//this is thekey point of using array to simulate Loop_List 
  26.         }      
  27.         leave ++;  //increase the number of executed people               
  28.     }    
  29.     //use a loop to find which people isremained,return its subscript not index! 
  30.     for(int i =0;i<N;i++) 
  31.     { 
  32.         if(Loop[i] ==0) 
  33.         { 
  34.             ret =i; 
  35.         } 
  36.     } 
  37.     return ret; 
  38. }    
  39. int main() 
  40.     cout<<Joseph_1(6,5)<<endl; 
  41.     system("pause"); 
  42.     return 0; 
  43. }   
#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)

根据这个公式可以写出如下两个函数:其中一个是递归,另外一个是迭代:


 

  1. #include"iostream" 
  2. #include"assert.h" 
  3. using namespace std; 
  4. int Joseph_2_1(int N,int m) 
  5. assert(N>0&&m>0); 
  6. int ret = 0; 
  7. for(int i = 2;i<=N;i++) 
  8. ret = (ret+m)%i; 
  9. return ret; 
  10.   
  11. int Recursion(int N,int m) 
  12. if(N==1) 
  13. return 0; 
  14. else 
  15. return (Recursion(N-1,m) + m)%N; 
  16. int Joseph_2_2(int N,int m) 
  17. assert(N>0&&m>0); 
  18. returnRecursion(N,m); 
  19. int main() 
  20. cout<<Joseph_2_1(6,5)<<endl; 
  21. cout<<Joseph_2_2(6,5)<<endl; 
  22. system("pause"); 
  23. 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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值