NOIP算法:约瑟夫问题(C++)

NOIP算法:约瑟夫问题(C++)
NOIP算法:约瑟夫问题(C++)

约瑟夫问题

也称为约瑟夫斯置换,在计算机编程的算法中,类似问题又称为约瑟夫环,又称“丢手绢问题”。

一、一般形式

1.N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。

2.有n只猴子,按顺时针方向围成一圈选大王(编号从1到n),从第1号开始报数,一直数到m,数到m的猴子退出圈外,剩下的猴子再接着从1开始报数。就这样,直到圈内只剩下一只猴子时,这个猴子就是猴王,编程求输入n,m后,输出最后猴王的编号。

算法思路:模拟

(1)由于对于每个人只有在与不在环中两种状态

(2)开始时每个人都在环中

(3)模拟过程,直到环中只剩下一人。

方法一:设立标记,每个元素标记为出队或在队中

https://www.360docs.net/doc/c410024337.html,/ch0302/1748/

#include

#include

#include

#include

using namespace std;

const int maxn=310;

int a[maxn];

int main(){

int p,num,cnt,n,m,tmp;

while(scanf("%d%d",&n,&m)!=EOF&&(n!=0||m!=0)){

memset(a,0,sizeof(a));

p=1;

for(num=1;num<=n;num++){//一共要数n轮

for(cnt=1;cnt<=m;cnt++){//每一轮数到m

while(a[p]==1)p=p%n+1;//如果指针所指已有标记指针后移

tmp=p;//此时p指向环中的第cnt只猴子

p=p%n+1;//指针后移

}

a[tmp]=1; //将第m只猴子标记

}

printf("%d\n",tmp);//第n轮的第m只猴子就是解

}

return 0;

}

方法二:使用数组模拟链表

codevs1282 约瑟夫问题https://www.360docs.net/doc/c410024337.html,/problem/1282/

#include

#include

#include

#include

#include

using namespace std;

const int maxn=30050;

int a[maxn];

int main(){

int n,m,i,j,p;

scanf("%d%d",&n,&m);

for(i=1;i

for(i=1;i<=n;i++){

for(j=1;j

printf("%d ",a[p]);

a[p]=a[a[p]];

}

return 0;

}

二、深入探讨

对于约瑟夫问题,若需要依次记录退出编号的情况:优化方法是使用线段树维护区间和,并进行单点更新。

具体方法见线段树章节。

对于约瑟夫问题,若只需要求胜利者编号,而不需依次记录退出编号的情况:

优化1:

把问题稍微改变一下,并不影响原意:

问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。

则第一轮报数出队者的编号:(m%n-1+n)%n;

第二轮报数由剩下的n-1个人组成了一个新的约瑟夫环,从k=m%n开始报数;其编号由第一轮的

k,k+1...n,0,1,...k-2,变化为

0,1,... ... ...n-1

若第二轮出队编号为x,则其在第一轮中的编号应该为x’=(x+k)%n=(x+m)%n

推广到i-1个人报数若胜利者编号为x,则其在i个人的约瑟夫环中编号应该为x’=(x+m)%i;

故,令opt[i]表示i个人的约瑟夫环中报m个数的胜利者的编号,则有:

opt[1]=0;

opt[i]=(opt[i-1]+m)%i (2<=i<=n)

opt[n]即为问题的解!考虑编号问题,若n个人从1开始编号,f[n]+1即为解。

时间复杂度O(n);空间复杂度O(n);

#include

#include

#include

#include

using namespace std;

const int maxn=310;

int opt[maxn];

int main(){

int n,m,i;

while(scanf("%d%d",&n,&m)!=EOF&&(n!=0||m!=0)){

opt[1]=0;

for(i=2;i<=n;i++)opt[i]=(opt[i-1]+m)%i;

printf("%d\n",opt[n]+1);

}

return 0;

}

优化2:

使用迭代算法使空间复杂度降为o(1);

#include

#include

#include

#include

using namespace std;

int f;

int main(){

int n,m,i;

while(scanf("%d%d",&n,&m)!=EOF&&(n!=0||m!=0)){

f=0;

for(i=2;i<=n;i++)f=(f+m)%i;

printf("%d\n",f+1);

}

return 0;

}

优化3:

观察上述算法中的变量f,他的初始值为第一个出圈人的编号,但在循环的过程中,我们会发现它常常处在一种等差递增的状态,从f = (f + m ) % i,可以看出,当i比较大而f+m比较小的时候,f就处于一种等差递增的状态,这个等差递增的过程并不是必须的,可以跳过。

递推式:

f(i)=( f(i-1) + m ) % i;

1)对于m = 1的情况可以单独讨论:当f == 0时,最终结果就是n-1;

2)如果f+m>i-1则必须取模,使用原方法求结果;

3)如果f+m<=i-1则表示可以进行优化:

根据递推式:

第i轮的结果是由第i-1轮推得的;现要使得等式跳过x轮,在不取模的情况下仍成立,则有:

f(i-1)+ m+ (x-1)*m <= i - 1 + x - 1;//不等式左边为多跳过x-1轮,右边为增加x-1个人出圈

即:

第i-1轮的约瑟夫号码 + x*m <= i-1+x轮的总人数 - 1(号码从0开始)

f + m * x < = i -2 + x

x最大能取到(i - 2 - f)/(m - 1);

f(i)=f(i-1)+m,为1轮,则x轮需再增加x-1轮,

得到f(i-1+x)=f(i-1)+ x * m ,这样就跳过了中间共x重的循环,从而节省了等差递增的时间开销。

可是其中求出来的x -1 + i可能会超过n,则:

当x-1+i>n时

x=n-i+1

即x最大可取n-i+1

f = f + m * (n - i +1) ;

该算法的算法复杂度在m=n时,用方程求出的值不能减少循环重数,算法复杂度仍为O(n)。

#include

#include

#include

#include

using namespace std;

int f;

int main(){

int n,m,i,x;

while(scanf("%d%d",&n,&m)!=EOF&&(n!=0||m!=0)){

if(m==1)f=n-1;

else{

i=2;f=0;

while(i<=n){

if(f+m<=i-1){//如果可以优化

x=(i-2-f)/(m-1);//计算x的值

if(i+x-1>=n)x=n-i+1;//如果跳过x轮后超出n,更新x

f=f+x*m;//得到当前f

i=i-1+x;//得到当前i

i++;//计算下一轮

}

else {

f=(f+m)%i;

i++;

}

}

}

printf("%d\n",f+1);

}

return 0;

}

笔算,n=300,m=3的约瑟夫问题(编号从1开始),最后胜出者的编号是?

根据优化3的推导,依次求解:

基本式:f(i)=(f(i-1)+m)%i

若f(i-1)+m>i-1只可求f(i)

若f(i-1)+m<=i-1则可直接求f(i+x-1),即f(i+x-1)=f(i-1)+x*m 并且x=(i-2-f)/(m-1)

手算时,由于f(i+x-1)=f(i-1)+x*m即f(i-1)+x*m< i+x-1,若x扩大一个恰好f(i-1)+x*m= i+x-1只需手工置0。

故可将f(i-1)+ m * x<=i-2+x 改为将f + m * x < = i -1 + x ,即x=(i-1-f)/(m-1)手工取模即可

f(1)=0;f(2)=(0+3)%2=1;f(3)=(1+3)%3=1;f(4)= (1+3)%4=0;f(5)=(0+3)%5=3;f(6)=(3+3)%6=0;

此时i=7,x=(6-0)/2=3 f(9)=0+3*3=9%9=0;

此时i=10,x=(9-0)/2=4 f(13)=0+3*4=12 f(14)=1

此时i=15,x=(14-1)/2=6 f(20)=1+3*6=19 f(21)=(19+3)%21=1

x=(21-1)/2=10 f(31)=1+3*10=31%31=0

x=(31-0)/2=15 f(46)=3*15=45 f(47)=(45+3)%47=1

x=(47-1)/2=23 f(70)=1+3*23=70%70=0

x=(70-0)/2=35 f(105)=0+3*35=105%105=0

x=(105-0)/2=52 f(157)=0+52*3=156 f(158)=159%158=1

x=(158-1)/2=78 f(236)=1+3*78=235 f(237)=1

f(300)=1+3*63=190

则最后胜利者的编号为f(300)+1=191。

《具体数学》中对于约瑟夫问题在m=2时的解法:

猜想1:f(n)都是奇数;

事实上,第一轮就删除了所有偶数,因此,猜想成立;

猜想2:

若n是偶数,第一轮过后,人数少一半,剩下的情形类似与原先;

假设原先有2n个人,第一轮过后,剩下的人的编号变为1,3,5,7...2n-1

这与最初有n个人的情形是相同的,区别在于编号有变化。

n个人,第一轮删除编号与2n个人第二轮删除编号对比:

1, 2, 3, 4...

1, 3, 5, 7...

相同点人数相同,不同点编号变为2倍减1

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

若n是奇数,

假设原先有2n+1个人,第一轮过后,删除2,4,6,8...2n(注意,此时指针在2n+1上,接下来还要删除1,才能使剩下的人数为n)则剩下的人的编号变为3,5,7...2n-1,2n+1

这与最初有n个人的情形是相同的,区别在于编号有变化。

n个人,第一轮删除编号与2n个人第二轮删除编号对比:

1, 2, 3, 4...

3, 5, 7, 9...

相同点人数相同,不同点编号变为2倍加1

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

综上:

f(1)=1;

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

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

猜想3:

n与f(n)的对应关系

n 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

f(n) 1 1 3 1 3 5 7 1 3 5 7 9 11 13 15 1

从中可以看出,f(n)是一个递增的奇数数列,每当n是2的幂时,便重新从f(n)=1开始。因此,如果我们选择m和l,使得n=2^m+l且0

答案的最漂亮的形式,与n的二进制表示有关:把n的第一位移动到最后,便得到f(n)。这可以通过把n表示为2^m+l来证明。

作业:

1.codevs2286反约瑟夫问题https://www.360docs.net/doc/c410024337.html,/problem/2286/

2.codevs4992 慈善的约瑟夫https://www.360docs.net/doc/c410024337.html,/problem/4992/

相关主题
相关文档
最新文档