题解:约瑟夫环问题
Guderian出品
题目背景
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲在一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。问题是,给定了总人数n和报数值m,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
题目描述
n个人(n<=100)围成一圈,从第一个人开始报数,数到m的人出列,再由下一个人重新从1开始报数,数到m的人再出圈,……依次类推,直到所有的人都出圈,请输出依次出圈人的编号。
输入格式
n m
输出格式
出圈的编号
输入输出样例
输入 #1
1 | 10 3 |
输出 #1
1 | 3 6 9 2 7 1 8 5 10 4 |
说明/提示
m, n ≤ 100
解题方法
解法一:模拟 + 指针模拟链表
时间复杂度:$O(m * n)$
1 | //Run on C++ |
解法二:模拟 + 数组模拟链表
时间复杂度:$O(m * n)$
1 | //Run on C++ |
解法三:模拟 + 数组
时间复杂度:$O(m * n)$
1 | //Run on C++ |
解法四:模拟 + 队列
时间复杂度:$O(m * n)$
1 | //Run on C++ |
注:
- 你也可以不用
stl
提供的队列,自己编写一个队列 - 你也可以用
stl
提供的双端队列deque
,不过据笔者观察除了装逼效果之外并没有其他用处
解法五:递归(运用递推公式)
时间复杂度:$O(n^2)$
1 | //Run on C++ |
解法六:迭代(运用递推公式)
时间复杂度:$O(n^2)$
1 | //Run on C++ |
推导递推公式*
以解法五为例,在递归函数部分代码中
1 | int Josephus(int x, int y, int z) |
x
代表总人数,y
代表每次报y
的人出列,z
是次数,该函数可以求第z
次出圈的人的编号。
举个例子:总人数x
为6人,从1开始,每报到3就出圈(y = 3
)
初始情况:1 2 3 4 5 6
通过递推的方式使第一个幸运观众出圈之后:1 2 4 5 6
此时,这些编号已经不能组成一个环,但是2
和4
之间还是连着的,且下一次报数将从4
开始。然而,之后的报数将总要考虑3
处的空位问题。
如何避免空位对报数所造成的影响?
可以将剩下的4个数重新组合成一个新的环,这样报数的时候就不必在意3
的空缺了。但是新产生的环的数字并非连续的,报数时不能直接用+ y
再% x
的简单递推方式,这下真令人头大。
如何使新环上的编号依然能够递推?
可以建立一种有确定规则的映射,要求映射之后的数字可以递推,且可以将在新环中继续按原规则报数得到的结果逆推出在旧环中的对应数字。
阻止我们使用老办法递推的因素就是3
号出圈之后产生的新环编号不连续,那么,我们可以通过建立某种映射,使得新环的编号依然连续,方便我们继续使用递推的方法。
原始:1 2 3 4 5 6
旧环:1 2 _ 4 5 6
新环:4 5 _ 1 2 3
正如你所见,相比于旧环中2
和4
之间被割裂开,新环的5
和1
之间在对5
取余的基础上是完美连续的,这就意味着我们可以继续从新环的1
(即旧环的4
)开始实施我们的递推方法。且只要推导出新环与旧环的映射关系,就能从在新环中报数的结果得知在旧环中的报数结果。
对于第二个幸运观众:
旧环:1 2 _ 4 5 *6
新环:4 5 _ 1 2 *3
如何从新环的3
得到旧环的6
呢?其实可以简单地逆推回去 : 新环是由(旧环中编号 - 报数值) % 旧环总人数
得到的,所以逆推时可以由(新环编号 + 报数值) % 旧环总人数
得到。
如:(3 + 3) % 6 = 0
咦?奇怪怎么是0
?不是应该是6
吗?这就涉及到一个进位的问题。由于旧环的编号为1~6
,而我们在新环依然保留了1~5
的编号并确保5
和1
之间在对5
取余的基础上连续,这就导致了5 % 5 = 0
,也就是说5
与0
是无法区分的。因此,虽然实际上旧环编号为6
的幸运观众出圈,但是我们的计算结果依然不可避免地把它对6
取余,得到一个看似奇怪却又顺理成章的0
。
如何避免错误地把最大的数算成0的情况?
把所有数减1
即可,最后输出结果的时候再加上1
。让我们再来一遍:
对于第二个幸运观众:
旧环:0 1 _ 3 4 *5
新环:3 4 _ 0 1 *2
从新环推出旧环的幸运观众:(2 + 3) % 6 = 5
由上得,原序列x
中第二次出圈的编号可以由新序列x - 1
第一次出圈的编号通过特定的逆推运算得出。即在以y
为出环报数值的约瑟夫环中,x
人环的第z
次出环编号可以由x - 1
人环中的第z - 1
次出环编号通过也定的运算推出。
幸运的是,第一次出环的编号是可以直接求出的,也就是说,对于任意次出环的编号,我们可以将之一直转化为第一次出环的编号问题,再一一迭代求出;也可以把第一次出环的编号作为递归基础,通过递归求出。
因此我们可以写出Josephus
函数的递推公式如下:
结束语
以上就是关于约瑟夫环问题的六种解法,并送上调试过的C++
代码,作者水平不高,将就看看吧 ╮(╯_╰)╭
*. 参考该网站,感谢作者的写作 ↩