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

来源:互联网 发布:方正字体侵权 淘宝 编辑:程序博客网 时间:2024/05/16 13:55

1 什么是约瑟夫问题

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

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

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

3 模拟方法

模拟方法就是所谓的一个个模拟,一个一个出列。这个方法比较多,可以直接用数组模拟,也可以直接建一个循环链表模拟,
总之这个很好实现,但是复杂度却是O(nm),如果n和m都是10000,要求1s计算出结果,估计就不行了。
这个算法实现,网上一大堆:随便给出两个:
struct ListNode  {      int num;        //编号      ListNode *next; //下一个      ListNode(int n = 0, ListNode *p = NULL)       { num = n; next = p;}  };    //自定义链表实现  int JosephusProblem_Solution1(int n, int m)  {      if(n < 1 || m < 1)          return -1;        ListNode *pHead = new ListNode(); //头结点      ListNode *pCurrentNode = pHead;   //当前结点      ListNode *pLastNode = NULL;       //前一个结点      unsigned i;        //构造环链表      for(i = 1; i < n; i++)      {          pCurrentNode->next = new ListNode(i);          pCurrentNode = pCurrentNode->next;      }      pCurrentNode->next = pHead;        //循环遍历      pLastNode = pCurrentNode;      pCurrentNode = pHead;        while(pCurrentNode->next != pCurrentNode)      {          //前进m - 1步          for(i = 0; i < m-1; i++)          {              pLastNode = pCurrentNode;              pCurrentNode = pCurrentNode->next;          }          //删除报到m - 1的数          pLastNode->next = pCurrentNode->next;          delete pCurrentNode;          pCurrentNode = pLastNode->next;      }      //释放空间      int result = pCurrentNode->num;      delete pCurrentNode;        return result;  } 


//使用标准库  int JosephusProblem_Solution2(int n, int m)  {      if(n < 1 || m < 1)          return -1;        list<int> listInt;      unsigned i;      //初始化链表      for(i = 0; i < n; i++)          listInt.push_back(i);        list<int>::iterator iterCurrent = listInt.begin();      while(listInt.size() > 1)      {          //前进m - 1步          for(i = 0; i < m-1; i++)          {              if(++iterCurrent == listInt.end())                  iterCurrent = listInt.begin();          }          //临时保存删除的结点          list<int>::iterator iterDel = iterCurrent;          if(++iterCurrent == listInt.end())              iterCurrent = listInt.begin();          //删除结点          listInt.erase(iterDel);      }        return *iterCurrent;  }  

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。

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

1k --> 0
2k+1 --> 1
3k+2 --> 2
4...
5...
6k-2 --> n-2
7k-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]。

递推公式:

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

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

int f(int n, int m) {    int f1 = 0,f2;    for(int i = 2; i <= n; i++)       {
 f2 = (f1 + m) % i; 
f1=f2;
}   return f2+ 1; }

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跳出循环即可。
具体代码及注释如下:
#include <iostream>using namespace std;//数据范围n<=10^18,m<=1000,时间几十ms__int64 N,M;int main(){while (cin >> N >> M){__int64 f1 = 0;__int64 f2;__int64 X;if (M == 1){cout << N <<endl;}else{for (__int64 i = 2; i <= N; ++ i){if (f1 + M < i)//表示很有可能跳过X个i{X = (i - f1) / M;//能跳过多少个if (i + X < N)//如果没有跳过n,就是i<=N{i = i + X;//i直接到i+Xf2 = (f1 + X*M);//由于f1+X*M肯定<=i,所以这里不用%if1 = f2;}else//如果跳过了n,那么就不能直接加X了,而是只需要加(N-i)个M即可{f2 = f1+(N-i)*M;f1 = f2;i = N;}}f2 = (f1 + M) % i;//如果f1+M>=i或者跳过上面的一些i之后还是要继续当前i对于的出列的人f1 = f2;}}cout << f2+1 <<endl;}return 0;}




原创粉丝点击