POJ约瑟夫环题目解析

本文参考了http://blog.csdn.net/wuzhekai1985,http://blog.csdn.net/kangroger/article/details/39254619等文章,在此一并致谢。

约瑟夫环是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后 结果+1即为原问题的解。

我们最容易想到的解法就是使用链表、数组等数据结构来模拟整个过程。下面分别给出数组、自定义链表和使用STL中的list的解法:

int JosephusProblem_Solution1(int n,int m)  
{  
	bool *p=new bool[n];
	for(int i=0;i<n;i++) *(p+i)=true;  
    int res,count=0;
    for(int i=0,j=0;;i++) 
    {  
		if(*(p+i))
		{  
			j++;  
            if(j==m)  
			{  
				*(p+i)=false;  
                j=0;  
                count++;
            }  
            if(count==n) 
			{
				res=i;
				break;
			}
        }  
        if(i==n-1) i=-1;
    }  
    delete []p;  
    return res;
} 

struct ListNode  
{  
    int num;        
    ListNode *next; 
    ListNode(int n=0,ListNode *p=NULL)   
    {num=n;next=p;}  
};  
  
int JosephusProblem_Solution2(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)  
    {   
		for(i=0;i<m-1;i++)  
        {  
			pLastNode=pCurrentNode;  
            pCurrentNode=pCurrentNode->next;  
        }   
        pLastNode->next=pCurrentNode->next;  
        delete pCurrentNode;  
        pCurrentNode=pLastNode->next;  
    }  
    int result=pCurrentNode->num;  
    delete pCurrentNode;  
    return result;  
}  
int JosephusProblem_Solution3(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)  
    {
        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;  
}  

上述方法的效率很低,其时间复杂度为O(mn)。当n和m很大时,很难在短时间内得出结果。不过好处就是可以给出n个人出圈的次序。只要在删除前保存一下即可。在程序设计竞赛中我们往往只需要求出最后留下的编号,可以利用递推关系寻求更简便的解法。

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

k             -->0
k+1        --> 1
k+2        --> 2
...
...
k-2         --> n-2
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去刚好就是n个人情况的解。变回去的公式很简单:x'=(x+k)%n

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

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

递推公式:
f[1]=0;
f[i]=(f[i-1]+m)%i; (i>1)

下面我们来看一下POJ中与约瑟夫环有关的问题:

POJ3517:第一次报的是m,以后每次报的都是k,简单逆推一下即可。

//POJ3517
#include<stdio.h>  
int m,n,k;  
int ans(int num)  
{  
    if(num==1) return 0;  
    return (k+ans(num-1))%num;  
}
  
int main()  
{  
    scanf("%d %d %d",&n,&k,&m);  
    while(n)  
    {  
        printf("%d\n",(ans(n-1)+m)%n+1);  
        scanf("%d %d %d",&n,&k,&m);  
    }  
    return 0;  
}  
其中(ans(n-1)+m)%n模拟的是第一次报数的情况,ans函数模拟的是以后每次报数的情况,加1是因为原题中是从1开始的。

POJ2359:对于输入的字符串,从开始依次进行报数,当报到N=1999时,删除对应的字符,字符串可以看作是首尾相连的环,直到剩余最后一个字符。直接套用公式即可。

//POJ2359
#include<iostream>
using namespace std;
const int m=1999;  
char str[30005];  
  
int main()  
{  
	char ch;  
	int k=0,s=0;  
	while((ch=getchar())!=EOF) 
	{
		if(ch!='\n')  
			str[k++]=ch;
	}
    for(int i=2;i<=k;i++) s=(s+m)%i;  
    if(str[s]=='?') printf("Yes\n");  
    else if(str[s]==' ') printf("No\n");  
    else printf("No comments\n");  
    return 0;  
}  
POJ1781:m值固定为2,也就是每隔一个人死一个。很显然,当n值为2的k次幂时,剩下的总是第一个人。

我们试着用推导最开始的递推公式的办法进一步寻找一下规律。

假设有8个人,分别是1 2 3 4 5 6 7 8,第一轮后剩下1 3 5 7 ,给它们重新编号为1 2 3 4 ,得出递推关系式:

f(2n)=2f(n)-1

假设有9个人,分别是1 2 3 4 5 6 7 8 9,第一轮后剩下1 3 5 7 9,由于1接下来也会被杀死,不妨计为3 5 7 9,给它们重新编号为1 2 3 4,得出递推关系式:

f(2n+1)=2f(n)+1

我们依次计算有n个人时最后剩下的人的编号,寻找规律:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

1 1 3 1 3 5 7 1 3 5    7   9   11 13 15 1     3    5   7   9  11

通项公式为f(2n+k)=2k+1。详细证明可以参考《具体数学》。

//POJ1781
#include<iostream>
using namespace std;

int main()
{
	char str[10];
	long long a,b,c,d;
	while(scanf("%s",str)!=EOF)
	{
 		if(str[0]=='0'&&str[1]=='0'&&str[3]=='0')	break;
		a=(str[0]-'0')*10+str[1]-'0';
		for(b=1;b<=str[3]-'0';b++) a=a*10;
  		c=1;
		while(1)
		{
			if(c>a)break;
   			c=2*c;
		}
		if(c!=1) c=c/2;
		a=a-c;
		printf("%lld\n",2*a+1); 
	}
	return 0;
}
POJ2244:从第一个人报数并被杀死,最后剩下第二个人,求最小的m值。从2开始枚举m即可。

//POJ2244
#include<iostream>
using namespace std;

int n;

int last(int n,int m)
{
	int ret=0;
 	for(int i=2;i<=n;i++) ret=(ret+m)%i;
    return ret;
}

int main()
{
    while(scanf("%d",&n),n)
	{
        int i=2;
        while(last(n-1,i)!=0) i++;
        printf("%d\n",i);
    }
    return 0;
}
为什么last(n-1,i)要等于0呢,因为第一个最开始就被杀死,所以我们把第二个人当成第一个人再开始递推的。

POJ1012:一共有2k个人,求m使得前k个人存活,后k个人死亡。只要前k个人没有死那么每次重新编号时他们的原有编号就不会改变。在这里打表保存每个k值对应的m值以免超时。很显然,m的最小取值是k+1。不同之处在于这道题需要正向模拟这个过程,而前面的递推公式是逆向模拟的。

//POJ1012
#include<iostream>
using namespace std;  
  
int main()  
{  
	int k;
    int Joseph[14]={0};  
    while(cin>>k)  
    {  
        if(!k) break;  
        if(Joseph[k])  
        {  
            cout<<Joseph[k]<<endl;  
            continue;  
        }  
        int m=k+1;
        int n=2*k;
        int ans[30]={0}; 
        for(int i=1;i<=k;i++) 
        {  
            ans[i]=(ans[i-1]+m-1)%(n-i+1);   
            if(ans[i]<k)  
            {  
                i=0;  
                m++;  
            }  
        }  
        Joseph[k]=m;  
        cout<<m<<endl;  
    }  
    return 0;  
}  



你可能感兴趣的:(POJ约瑟夫环题目解析)