约瑟夫环问题详解

本文介绍约瑟夫环的问题详细推导和理解

前言

  挺早之前在csdn上写过一篇关于约瑟夫环的博文,那是当时正处在找工作期间刷题,然后做的笔记。最近看见博文有人回复,并且有提醒我写错了的地方,于是又看了看这篇文章,发现确实有些地方写错了,且这些概念换做现在的自己来说,基本上都生疏了,特意重新整理了下,用作巩固

约瑟夫环问题

什么是约瑟夫环问题

  约瑟夫是犹太军队的一个将军,在反抗罗马的起义中,他所率领的军队被击溃,只剩下残余的部队40余人,他们都是宁死不屈的人,所以不愿投降做叛徒。一群人表决说要死,所以用一种策略来先后杀死所有人。于是约瑟夫建议:每次由其他两人一起杀死一个人,而被杀的人的先后顺序是由抽签决定的,约瑟夫有预谋地抽到了最后一签,在杀了除了他和剩余那个人之外的最后一人,他劝服了另外一个没死的人投降了罗马
  我们这个规则是这么定的:
  在一间房间总共有n个人(下标0~n-1),只能有最后一个人活命
按照如下规则去杀人:

1
2
3
4
所有人围成一圈
顺时针报数,每次报到q的人将被杀掉
被杀掉的人将从房间内被移走
然后从被杀掉的下一个人重新报数,继续报q,再清除,直到剩余一人

你要做的是:当你在这一群人之间时,你必须选择一个位置以使得你变成那剩余的最后一人,也就是活下来

约瑟夫环特例

  为了更好地分析和理解约瑟夫环的问题,这里先聊一下这个问题的特例
  以上概念部分提到两个变量:nq,其中n是总人数,q是每次要杀的人是第几人
  下面我们假设这两个值分别是不同取值的计算方法:

q=2,n=2^k
1
2
3
4
5
6
7
8
9
10
#n = 2
0 1 ==> 0

#n = 4
0 1 2 4 ==> 0 2 ==> 0

#n = 8
0 1 2 3 4 5 6 7 ==> 0 2 4 6 ==> 0 4 ==> 0

......

  由上面的规律是不是能发现一些问题,当q=2,n=2^k时,活下来的人总是这个0,PS(大家知道该站在哪个位置了吧~)
  定义 Jq=(n=) 为n个人,每次杀死第q个人构成的约瑟夫环最后结果,则有jq=2(n=2^k) = 0

q=2,n=任意数

  当n可以为任意数字的时候,就不会有上面这么简单的站位了,你的走位需要飘逸一点才能活到最后
  举个栗子:n=9
  注:示例途中的下表从1开始,我们也可以看成是从0开始,就不去改图了
示例图1
  能看出来,我们干掉途中的第一个人也就是2,之后就只剩下8个人了,这时候n=8=2^3,这样一来又回到Jq=2(2^k)上了,这时候我们需要的是找到当前的1号元素
示例图2
  这时候,我们从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的情况下,应该是什么样的规律
  我们假定:

1
2
- n — n人构成的约瑟夫环 
- q — 每次移除第q个人

  约定:

1
2
- Jq(n)表示n人构成的约瑟夫环,每次移除第q个人的解 
- n个人的编号从0开始至n-1

  我们沿用之前特例的思想:能不能由Jq(n+1)的问题缩小成为J(n)的问题(这里的n是n+1规模的约瑟夫问题消除一个元素之后的答案),Jq(n)是在Jq(n+1)基础上移除一个人之后的解。也就是说,我们要想的是:由Jq(n)怎么得到Jq(n+1)?
  更详细推导过程见这篇博文
  大致过程我也来推导一遍:

1
2
3
4
5
6
7
8
9
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(第q+1个人)开始,到之前最大数n-1,每个数都减去q,减到0之后,接着新的值每次往后+1,直到加到q-2

1
2
3
4
5
6
7
J4(9) :
0 1 2 3 4 5 6 7 8 消去3--> 0 1 2 4 5 6 7 8 ( 0 1 2)
对应的新值: 0 1 2 3 4 5 6 7

其中:q=4,从3之后第一个数4开始:

(4-1)%n=0,(5-q)%n=1,(6-q)%n=2,(7-q)%n=3,(8-q)%n=4,因为是个环,不能绕成负数吧,(0-q)%n=5,(1-q)%n=6 , (2-q)%n=7

  觉得抽象的可以参考下图:
  示例图3

  现在大概知道我们的新的约瑟夫环的下标都是这样来的:在旧的下标基础上,减去一个q,再用计算出的结果对长度取余
  new = (old-q) % n

  反推一下:
  old = (new+q) % n

  好了,到了这里知道了怎么推出之前的下标,那么也就可以一步步递推回去得到开始的队列或者从小推到大得到最后剩余的结果

实战一下

  J2(1) = 0
  J2(2) = (J2(1) + 2) % 2 = 0
  J2(3) = (J2(2) + 2) % 3 = 2
  J2(4) = (J2(3) + 2) % 4 = 0
  J2(5) = (J2(4) + 2) % 5 = 2
……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
#include<stdio.h>
using namespace std;

int yuesefu(int n,int m){
if(n == 1){
return 0; //这里返回下标,从0开始,只有一个元素就是剩余的元素0
}
else{
return (yuesefu(n-1,m) + m) % n; //我们传入的n是总共多少个数
}
}
int main(void){
int a,b;
cin>>a>>b;
cout<<yuesefu(a,b)<<endl;

//或者,直接循环迭代,求出来的result如上
int result = 0;
for(int i = 2;i <= a;i++){
result = (result+b) %i;
}
cout<<"result = "<<result<<endl;
return 0;
}

End

好了终于写完了,假期开始咯~~