zoukankan      html  css  js  c++  java
  • 约瑟夫问题(优化优化再优化)

     

    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;
    }

     
     
     



     
     
  • 相关阅读:
    CentOS下date命令
    spring-data-redis --简单的用spring-data-redis
    Unable to Rebuild JIRA Index
    JIRA Cannot Start Due to 'unable to clean the cache directory: /opt/jira/plugins/.osgi-plugins/felix'
    Java compiler level does not match the version of the installed Java project facet.
    maven scope含义的说明
    maven 下载 源码和javadoc命令
    Redis 入门第一发
    mysql 1194 – Table ‘tbl_video_info’ is marked as crashed and should be repaired 解决方法
    tomcat用redis做session共享
  • 原文地址:https://www.cnblogs.com/pangblog/p/3241076.html
Copyright © 2011-2022 走看看