约瑟夫问题(优化优化再优化)

1 什么是约瑟夫问题

约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。
从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;
依此规律重复下去,直到圆桌周围的人全部出列。

2 如何求最后一个出列的人

1、模拟方法
2、数学方法

3 模拟方法

模拟方法就是所谓的一个个模拟,一个一个出列。这个方法比较多,可以直接用数组模拟,也可以直接建一个循环链表模拟,
总之这个很好实现,但是复杂度却是O(nm),如果n和m都是10000,要求1s计算出结果,估计就不行了。
这个算法实现,网上一大堆:随便给出两个:
[cpp]  view plain  copy
  1. struct ListNode    
  2. {    
  3.     int num;        //编号    
  4.     ListNode *next; //下一个    
  5.     ListNode(int n = 0, ListNode *p = NULL)     
  6.     { num = n; next = p;}    
  7. };    
  8.     
  9. //自定义链表实现    
  10. int JosephusProblem_Solution1(int n, int m)    
  11. {    
  12.     if(n < 1 || m < 1)    
  13.         return -1;    
  14.     
  15.     ListNode *pHead = new ListNode(); //头结点    
  16.     ListNode *pCurrentNode = pHead;   //当前结点    
  17.     ListNode *pLastNode = NULL;       //前一个结点    
  18.     unsigned i;    
  19.     
  20.     //构造环链表    
  21.     for(i = 1; i < n; i++)    
  22.     {    
  23.         pCurrentNode->next = new ListNode(i);    
  24.         pCurrentNode = pCurrentNode->next;    
  25.     }    
  26.     pCurrentNode->next = pHead;    
  27.     
  28.     //循环遍历    
  29.     pLastNode = pCurrentNode;    
  30.     pCurrentNode = pHead;    
  31.     
  32.     while(pCurrentNode->next != pCurrentNode)    
  33.     {    
  34.         //前进m - 1步    
  35.         for(i = 0; i < m-1; i++)    
  36.         {    
  37.             pLastNode = pCurrentNode;    
  38.             pCurrentNode = pCurrentNode->next;    
  39.         }    
  40.         //删除报到m - 1的数    
  41.         pLastNode->next = pCurrentNode->next;    
  42.         delete pCurrentNode;    
  43.         pCurrentNode = pLastNode->next;    
  44.     }    
  45.     //释放空间    
  46.     int result = pCurrentNode->num;    
  47.     delete pCurrentNode;    
  48.     
  49.     return result;    
  50. }   


[cpp]  view plain  copy
  1. //使用标准库    
  2. int JosephusProblem_Solution2(int n, int m)    
  3. {    
  4.     if(n < 1 || m < 1)    
  5.         return -1;    
  6.     
  7.     list<int> listInt;    
  8.     unsigned i;    
  9.     //初始化链表    
  10.     for(i = 0; i < n; i++)    
  11.         listInt.push_back(i);    
  12.     
  13.     list<int>::iterator iterCurrent = listInt.begin();    
  14.     while(listInt.size() > 1)    
  15.     {    
  16.         //前进m - 1步    
  17.         for(i = 0; i < m-1; i++)    
  18.         {    
  19.             if(++iterCurrent == listInt.end())    
  20.                 iterCurrent = listInt.begin();    
  21.         }    
  22.         //临时保存删除的结点    
  23.         list<int>::iterator iterDel = iterCurrent;    
  24.         if(++iterCurrent == listInt.end())    
  25.             iterCurrent = listInt.begin();    
  26.         //删除结点    
  27.         listInt.erase(iterDel);    
  28.     }    
  29.     
  30.     return *iterCurrent;    
  31. }    

4 数学方法-优化

由于上面O(nm)的方法很容易超时,所以这里的数学方法可以做到O(n).

问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。

我们知道第一个人(编号一定是m%n-1) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%n的人开始): k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2,并且从k开始报0。

现在我们把他们的编号做一下转换:

1 k --> 0
2 k+1 --> 1
3 k+2 --> 2
4 ...
5 ...
6 k-2 --> n-2
7 k-1 --> n-1

变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:x'=(x+k)%n。

如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:

令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]。

递推公式:

1 f[1]=0;
2 f[i]=(f[i-1]+m)%i; (i>1)

有了这个公式,我们要做的就是从1-n顺序算出f[i]的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1。

[cpp]  view plain  copy
  1. int f(int n, int m)   
  2. {   
  3.    int f1 = 0,f2;   
  4.    for(int i = 2; i <= n; i++)   
  5.       {  
[cpp]  view plain  copy
  1. f2 = (f1 + m) % i;   
[cpp]  view plain  copy
  1. f1=f2;  
[cpp]  view plain  copy
  1. }  
  2.    return f2+ 1;   
  3. }  

5 在优化还能优化吗?-再优化

今天碰到一个题目,n <= 10^18,m<=1000,时间1s,这想想O(n)肯定超时,没得说。
但是我么可以看看上面的规律,
f[i] = (f[i-1]+m)%i,通过这个式子,我们发现,到一定程度,m会远远小于i的,所以每次不是仅仅加一个m,我可以一下子加X*m,从而跳过X个i,事实证明,这样做的效率非常高。
当然只有当m远远小于n的时候,效率会比较高。如果m>n那么效率也就接近O(n)了。

对于当前的i,如果f1+m <i,那么表示,很有可能可以跳过下一个i,这里我们假设f1+X*m=i,那么至少可以跳过X=(i-f1)/m,然后i+=X即可,这样就不用求i到i+X之间的数据了。
什么时候结束呢?
如果i+X>=n,那么就证明这次已经超过了n,这里只需要令f2=f1+(n-i)*m,并且i=n跳出循环即可。
具体代码及注释如下:
[cpp]  view plain  copy
  1. #include <iostream>  
  2. using namespace std;  
  3. //数据范围n<=10^18,m<=1000,时间几十ms  
  4. __int64 N,M;  
  5. int main()  
  6. {  
  7.     while (cin >> N >> M)  
  8.     {  
  9.         __int64 f1 = 0;  
  10.         __int64 f2;  
  11.         __int64 X;  
  12.         if (M == 1)  
  13.         {  
  14.             cout << N <<endl;  
  15.         }  
  16.         else  
  17.         {  
  18.             for (__int64 i = 2; i <= N; ++ i)  
  19.             {  
  20.                 if (f1 + M < i)//表示很有可能跳过X个i  
  21.                 {  
  22.                     X = (i - f1) / M;//能跳过多少个  
  23.                     if (i + X < N)//如果没有跳过n,就是i<=N  
  24.                     {  
  25.                         i = i + X;//i直接到i+X  
  26.                         f2 = (f1 + X*M);//由于f1+X*M肯定<=i,所以这里不用%i  
  27.                         f1 = f2;  
  28.                     }  
  29.                     else//如果跳过了n,那么就不能直接加X了,而是只需要加(N-i)个M即可  
  30.                     {  
  31.                         f2 = f1+(N-i)*M;  
  32.                         f1 = f2;  
  33.                         i = N;  
  34.                     }  
  35.                 }  
  36.                 f2 = (f1 + M) % i;//如果f1+M>=i或者跳过上面的一些i之后还是要继续当前i对于的出列的人  
  37.                 f1 = f2;  
  38.             }  
  39.         }  
  40.         cout << f2+1 <<endl;  
  41.     }  
  42.     return 0;  
  43. }  



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值