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

 

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。

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

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。

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+X
						f2 = (f1 + X*M);//由于f1+X*M肯定<=i,所以这里不用%i
						f1 = 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;
}

 
 
 



 
 
原文地址:https://www.cnblogs.com/pangblog/p/3241076.html