约瑟夫问题

备注:多重转载、整理,如有疑问,请留言。

一. 问题描述:

n个人围成一圈,按顺时针方向依次编号,1, 2, ..., n,然后按顺时针方向从编号为1的人开始报数,报到m的人退出此圈,下一位继续从1开始报数,直到只剩下一人,求这个人的编号。

二. 问题解决:

方法一:朴素解法。利用循环链表,时间复杂度O(m*n),空间复杂度O(n)。C语言实现如下:

typedef struct node{
    long n;
    struct node *next;
}NODE;

long josephus_plain(long n=500,long m=2){
	//n: total person number
	//m: death number
	if(n<1||m<0)return -1;
	if(m==1)return n;	
	NODE *clink = (NODE *)malloc(sizeof(NODE));
	NODE *p = NULL, *pre = NULL;
	clink->n = 1;
	clink->next = clink;
	long i;
	for(i=n;i>1;i--){
		p = (NODE *)malloc(sizeof(NODE));
		p->n = i;
		p->next = clink->next;
		clink->next = p;
	}
	while(clink!=clink->next){
		for(i=1;i<m;++i){
			pre=clink;
			clink=clink->next;
		}
		p=clink;
		clink=clink->next;
		pre->next=clink;
		free(p);
		p=NULL;
	}
	i = clink->n;
	free(clink);
	clink=NULL;
	return i;
}

方法二:数学解法。寻找递推公式,时间复杂度为O(n),空间复杂度为O(1)(备注:多重转载,作者不详,但思路清晰,比较容易理解)。

为方便计算,问题重新描述:n个人围成一圈,按顺时针方向依次编号,0, 1, 2, ..., n-1,然后按顺时针方向从编号为0的人开始报数,报到m-1的人退出此圈,下一位继续从0开始报数,直到只剩下一人,求这个人的编号。

第一个人出圈后(其编号一定(m-1)%n),剩下的n-1个人仍构成约瑟夫问题(以编号为k=m%n人开始):k, k+1, ..., n-1, 0, 1, ..., k-2,对剩下的n-1人重新编号:

k, k+1, ..., n-1, 0, 1, ..., k-2 (k-1)

=>

0, 1, ..., n-1-k, n-k, n-k+1, ..., n-2 (n-1)

若n-1人的约瑟夫问题最后结果为x,则由上述重新编号过程可以推出n人的约瑟夫问题的最后结果为x'=(x+m)%n。所以对步长为m(m>0且m为整数)的n人约瑟夫问题有如下递推公式(编号为:0, 1, ..., n-1):

J(1)=0;

J(n)=(J(n-1)+m)%n, n>0.

算法实现如下:

long josephus_optimize1(long n=500,long m=2){
	//n: total person number
	//m: death number
	long i,s=0;
	for(i=2;i<=n;++i){
		s=(s+m)%i;
	}
	return s;
}

方法三:数学解法优化。对方法二中递推过程进行优化,当m<n时,时间复杂度可以降到对数数量级O(ln(n)/ln(m/(m-1)))。

方法二中,变量s,他的初始值为剩余的那个人的编号,但在循环的过程中,我们会发现它常常处在一种等差递增的状态。对递推式:s=(s+m)%i,可以看出,当i比较大而s+m比较小的时候,s就处于一种等差递增的状态,这个等差递增的过程并不是必须的,可以跳过。设可以跳过的次数为x,则有:

s+m*(x+1)<=i+x

=>

x<=(i-s-m)/(m-1)

令x=floor((i-s-m)/(m-1)), s=s+m*x, i=i+x,然后直接进行操作s=(s+m)%i,这样就跳过了x次不必要的操作,从而节省了等差递增的时间开销。当然,若求出来的x+i超过n,则表明可以直接结束算法了,另做处理。算法实现如下:

long josephus_optimize2(long n=500,long m=2){
	//n: total person number
	//m: death number
	long i,x=-1,s=0;
	for(i=2;i<=n;++i){
		if(s+m<i){
			x=(i-s-m)/(m-1);
			if(i+x<n){
				s=(s+m*x);
				i+=x;
			}else{
				s+=m*(n-i);
				i=n;
			}
		}
		s=(s+m)%i;
	}
	return s;
}
对上述不等式:s+m*(x+1)<=i+x,求得的最大整数x,大多数情况下不等式等号都不成立,因此在大多数情况下可以直接跳过x+1次,不必要的操作。设可以跳过的最大次数,计算如下不等式:

s+m*x+1<=i+x-1

=>

x<=(i-s-2)/(m-1)

令x=floor((i-s-2)/(m-1)), 则此x,即为此次可以跳过的最大次数。令s=s+m*x, i=i+x,然后直接进行操作s=(s+m)%i。

long josephus_optimize3(long n=500,long m=2){
	//n: total person number
	//m: death number
	long i,x=-1,s=0;
	for(i=2;i<=n;++i){
		if(s+m<i){
			x=(i-s-2)/(m-1);
			if(i+x<n){
				s=(s+m*x);
				i+=x;
			}else{
				s+=m*(n-i);
				i=n;
			}
		}
		s=(s+m)%i;
	}
	return s;
}
以上实现中,josephus_optimize1(), josephus_optimize2(), josephus_optimize3()给出的结果均为在编号为0~n时的结果,当编号为1~n时,只需将上述结果加1以修正。另外,当报数的起始位置不是从编号为1人开始时,只需做如下修正,假设编号为1~n,起始报数人编号为k(1<=k<=n),并从1开始报数,则结果为:(J(n)+k-1)%n+1。完整实现如下:

long josephus(long n=500,long m=2,long k=1){
	//n: total person number(1, 2, ..., n), default vaule: 500
	//m: death number(the one who get this number will be killed), default value: 2
	//k: start position(the position to start the game), defualt value: 1
	long s;
	n=(n<1)?500:n;
	m=(m<1)?2:m;
	k=(k<1)?1:(k+n-1)%n+1;
	if(m==1){
		s=n-1;
	}else{
		//optimize :
		//s=josephus_optimize1(n,m);
		//s=josephus_optimize2(n,m);
		s=josephus_optimize3(n,m);
	}
	return (s+k-1)%n+1;
}

测试结果:

n=500, m=2, k=1(i:x:s): 489

josephus_optimize2:

2:-1:0,4:1:0,8:3:0,16:7:0,32:15:0,64:31:0,128:63:0,256:127:0,500:-1:488,times::9

josephus_optimize3:

2:-1:0,4:1:0,8:3:0,16:7:0,32:15:0,64:31:0,128:63:0,256:127:0,500:-1:488,times::9


n=1000, m=7, k=3(i:x:s): 406

josephus_optimize2:

2:-1:1,3:-1:2,4:-1:1,5:-1:3,6:-1:4,7:-1:4,8:-1:3,9:-1:1,10:0:8,11:0:4,12:0:11,13
:0:5,14:0:12,15:0:4,16:0:11,17:0:1,19:1:15,20:1:2,23:2:0,26:2:21,27:2:1,31:3:29,
32:3:4,36:3:32,37:3:2,42:4:37,43:4:1,50:6:0,58:7:56,59:7:4,68:8:67,69:8:5,79:9:7
5,80:9:2,93:12:0,108:14:105,109:14:3,126:16:122,127:16:2,147:19:142,148:19:1,172
:23:169,173:23:3,201:27:199,202:27:4,235:32:0,274:38:273,275:38:5,320:44:0,373:5
2:371,374:52:4,435:60:431,436:60:2,508:71:506,509:71:4,593:83:592,594:83:5,692:9
7:691,693:97:5,807:113:803,808:113:2,942:133:940,943:133:4,1000:-1:403,times::64

josephus_optimize3:

2:-1:1,3:-1:2,4:-1:1,5:-1:3,6:-1:4,7:-1:4,8:-1:3,9:-1:1,11:1:4,13:1:5,15:1:4,17:
1:1,20:2:2,23:2:0,27:3:1,32:4:4,37:4:2,43:5:1,50:6:0,59:8:4,69:9:5,80:10:2,93:12
:0,109:15:3,127:17:2,148:20:1,173:24:3,202:28:4,235:32:0,275:39:5,320:44:0,374:5
3:4,436:61:2,509:72:4,594:84:5,693:98:5,808:114:2,943:134:4,1000:-1:403,times::3
9

方法四:数学解法优化。时间复杂度降到O(logn)

详见 Donald E. Knuth的《具体数学》中相关部分的讨论,相当精彩,这里下载

当m=2时,有更简单的方法:

Josephus(1)=1

Josephus(2n)=2Josephus(n)-1, n>0

Josephus(2n+1)=2Josephus(n)+1, n>0

令n=2^k+l, 0<=l<2^k,则有,Josephus(n)=2*l+1

令n=(bm,bm-1,...,b1,b0)2,则有,Josephus(n)=(bm-1,...,b1,b0,bm)2,其中bm为1。

三. 参考资料:

1. 雨中飞燕

2. 【整理】约瑟夫问题的数学方法

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值