参考博客:
1、约瑟夫环问题详解
2、约瑟夫环
约瑟夫环问题
什么是约瑟夫环问题
约瑟夫是犹太军队的一个将军,在反抗罗马的起义中,他所率领的军队被击溃,只剩下残余的部队40余人,他们都是宁死不屈的人,所以不愿投降做叛徒。一群人表决说要死,所以用一种策略来先后杀死所有人。于是约瑟夫建议:每次由其他两人一起杀死一个人,而被杀的人的先后顺序是由抽签决定的,约瑟夫有预谋地抽到了最后一签,在杀了除了他和剩余那个人之外的最后一人,他劝服了另外一个没死的人投降了罗马
我们这个规则是这么定的:
在一间房间总共有n个人(下标0~n-1),只能有最后一个人活命
按照如下规则去杀人:
- 所有人围成一圈
- 顺时针报数,每次报到q的人将被杀掉
- 被杀掉的人将从房间内被移走
- 然后从被杀掉的下一个人重新报数,继续报q,再清除,直到剩余一人
推导过程:
特例1:q=2,n=2^k
#n = 20 1 ==> 0#n = 40 1 2 4 ==> 0 2 ==> 0#n = 80 1 2 3 4 5 6 7 ==> 0 2 4 6 ==> 0 4 ==> 0
定义 Jq=(n=) 为n个人,每次杀死第q个人构成的约瑟夫环最后结果,则有jq=2(n=2^k) = 0
q=2,n=任意数
当n可以为任意数字的时候,就不会有上面这么简单的站位了,你的走位需要飘逸一点才能活到最后
举个栗子:n=9
注:示例途中的下表从1开始,我们也可以看成是从0开始,就不去改图了
能看出来,我们干掉途中的第一个人也就是2,之后就只剩下8个人了,这时候n=8=2^3,这样一来又回到Jq=2(2^k)上了,
这时候我们需要的是找到当前的1号元素
这时候,我们从3号开始,就成了另外一个规模小1的约瑟夫问题(恰好为2^k的特例)。
此时,我们可以把3号看成新的约瑟夫问题中的1号位置:
Jq=2(n=8) = Jq=2(n=2^3) = 1,也就是说这里的1代表的就是上一个问题中的3号
So:Jq=2(n=9) = 3
答案为3号
总结下规律:
在q=2的前提下,给出n,我们首先找出,离n最近的一个2^k数,如n=9,那么这个2的幂次方数就是8,同理可得。找到之后,我们需要转换成对应的这个2^k数的约瑟夫环问题,因为其第一个元素即是我们想要的答案
Jq=2(n) = Jq=2(2^k + t) = 2t+1
q=任意数,n=任意数
说完了特例,应该对约瑟夫环的问题了解了,现在说说q≠2的情况下,应该是什么样的规律
我们假定:
- n — n人构成的约瑟夫环 - q — 每次移除第q个人
- Jq(n)表示n人构成的约瑟夫环,每次移除第q个人的解 - n个人的编号从0开始至n-1
0 1 2 ................................ n-1 总共n人| | | |q q+1 q+2 ...... n-2 n-1 0 1 2 ...... q-2 (这里是除去q-1这位兄台的剩余n-1人)设第q个人也就是下标为q-1的那位,杀死,剩下n-1个人,如上这时,又来重复我们的老套路:将新的被杀的后一个人作为新的0号,于是新的如下:0 1 2 ...... .......... ........ n-2
现在大概知道我们的新的约瑟夫环的下标都是这样来的:在旧的下标基础上,减去一个q,再用计算出的结果对长度取余
new = (old-q) % n
反推一下:
old = (new+q) % n
1 #include<iostream> 2 #include<stdio.h> 3 using namespace std; 4 5 int yuesefu(int n,int m){ 6 if(n == 1){ 7 return 0; //这里返回下标,从0开始,只有一个元素就是剩余的元素0 8 } 9 else{ 10 return (yuesefu(n-1,m) + m) % n; //我们传入的n是总共多少个数 11 } 12 } 13 int main(void){ 14 int a,b; 15 cin>>a>>b; 16 cout<<yuesefu(a,b)<<endl; 17 18 //或者,直接循环迭代,求出来的result如上 19 int result = 0; 20 for(int i = 2;i <= a;i++){ 21 result = (result+b) %i; 22 } 23 cout<<"result = "<<result<<endl; 24 return 0; 25 }
【题目链接】https://vjudge.net/problem/51Nod-1073
首先,将n个人编号为: 0,1,2,3........k-1,k,k+1........n-1,
第一轮结束之后第k个人,也就是k-1将出列,剩下的人: 0,1,2,3.........k-2, k, k+1.......n-1
下一轮从k开始我们进行重新编号,将上面的编号为: n-k,n-k+1,n-k+2,n-k+3.............., 0,.1,.........n-k-1,
我们如果知道了这一轮存活下来的人对应的编号是y,那么他本来的编号就是 x=(y+k)%n
由此引出递推式 f(n)=(f(n-1)+k)%n | f(1)=0
【题目链接】https://vjudge.net/problem/UVA-1394
给出n,k,m表示,第一次先出队第m个人,然后从后面的人开始数到k的出列接着从1开始数,
和原来的有了一点变化就是不是从第一个人开始数了,而是先出队第m然后从m+1开始,不过只有第一轮的和后面的公式不一样我们只要算出f(n-1)再推f(n)就好了。
我们将第m人出列后从新对余下的(n-1)人编号,f(n-1)=(f(n-2)+k)%(n-1); 设x=f(n-1),则最后的答案 ans=(x+m)%n+1;
1 #include<bits/stdc++.h> 2 using namespace std; 3 int main() 4 { 5 int n,m,k; 6 while(~scanf("%d%d%d",&n,&k,&m),(n||m||k)){ 7 int f = 0 ; 8 for(int i=2;i<n ;i++) 9 f = ( f+k ) % i ; 10 f = (f+m+n) % n + 1 ; 11 printf("%d\n",f); 12 } 13 return 0; 14 }
【题目链接】http://acm.hdu.edu.cn/showproblem.php?pid=2211
【参考博客】https://blog.csdn.net/u012717411/article/details/43925433
这个就是每次出队是k倍数的人,找到队尾之后将再次从队首开始数1,与约瑟夫有些不同,但不变的是公式推倒的过程。
想要知道f(n),我们不妨先假设得到了x=f(n-1),这里的n表示是第几轮从头开始,我们如果将这个x转为原本的编号呢,如果我们知道他前面死了几个人的话,
直接让x加上这个人数就好了,因为数到k就死一个,也就是说x前面有几个(k-1)就表示上一轮x前面淘汰的人,加上就好,最后边界f(k,k)=k
1 #include<bits/stdc++.h> 2 using namespace std; 3 int dfs(int n,int k){ 4 if( n == k ){ 5 return k ; 6 } 7 int x = dfs(n-n/k,k); 8 return x + (x-1)/(k-1); 9 } 10 int main(){ 11 int T,n,k; 12 scanf("%d",&T); 13 while(T--){ 14 scanf("%d%d",&n,&k); 15 int ans = dfs(n,k) ; 16 printf("%d\n",ans); 17 } 18 return 0; 19 }