约瑟夫环问题详解
问题描述
约瑟夫环(Josephus Problem)是一个著名的数学问题: n个人围成一圈,从第一个人开始报数,报到m的人出列,然后从出列的下一个人重新开始报数,如此反复,直到只剩最后一个人的编号。约瑟夫环问题是一个很经典的算法问题,对于编号存在两种情形:(0,1...,n-1)和(1,2...,n),对于两种情况求解思路一致,但具体实现上略有区别。
本文介绍两种主要解题思路:一种是使用双端队列直观模拟整个过程的模拟法(时间复杂度O(nk),空间复杂度O(n)),另一种是通过观察发现问题可以分解为子问题并推导出递推公式f(n,k) = (f(n-1,k) + k) % n的递推法,其中递推法可以用递归(时间O(n),空间O(n))或迭代(时间O(n),空间O(1))方式实现。文章还特别讨论了从0开始编号和从1开始编号两种情况的区别,并提供了相应的代码实现。
问题分析
基本思路(模拟法):
模拟整个删除过程是最直觉的想法,一共进行需要n-1轮报数,剩下最后一个人便是答案。通过双端队列很容易实现这个过程,从队头开始报数,前k-1个人依次出队拼接到队尾,而后将第k个人弹出队列,重复这个过程直至只剩下一人。
from collections import deque
def findlast(n: int, k: int) -> int:
# 使用双端队列
queue = deque(range(1, n + 1))
while len(queue) > 1:
# 将前k-1个数移到队尾
for _ in range(k - 1):
queue.append(queue.popleft())
# 删除第k个数
queue.popleft()
return queue[0]
模拟一共需要进行n-1轮,每轮进行对队列进行k次操作,总体时间复杂度为O(nk);空间上仅利用一个长度为n的队列,空间复杂度为O(n)。
对于输入n和k都很大时,时间复杂度接近O(n^2),情况会存在超时。进一步观察模拟过程可以发现这个问题是可以进行子问题分解,通过递归或者迭代的方式来解决,关键是要找出递推关系。
进阶思路:
考虑10个人报数3的情况,最初的序列为
通过第一次报数后,2被删除,结果变为
由于是围成一个环,序列本质是首尾相接的,可以写成
现在我们接着观察9个人的情况,最初的序列为
发现如果在这个序列的基础上+3后对10取余,结果与10个人第一次报完数的序列一致
那么接着报数,最后两种情况剩余的编号肯定是一致的。由此可知9个人报数的结果通过处理,可以得到10个人报数的结果。
这时候我们可以写出递推式,先定义n个人报数k最后的结果编号即为f(n,k)。
f(10,3) = (f(9,3) + 3) % 10
f(9,3) = (f(8,3) + 3) % 9
........
f(2,3) = (f(1,3) + 3) %2
f(1,3) = 0
递推关系: f(n,k) = (f(n-1,k) + k) % n
初始条件: f(1,k) = 0
通过上述递推关系通过递归或者迭代解决问题
- 每次出列一个人后,问题规模减小1
- 出列后的编号需要映射到原始编号
- 从简单情况(n=1)开始,逐步递推到n个人的情况
上述为对于从0开始编号的情况,而对于从1开始编号的情况,由于取余%运算结果区间为[0,n-1],需要进行将0映射为n的处理
将0映射为10,才符合原序列要求
代码实现
递归解法
# 从0开始编号
def findlast(n: int, k: int) -> int:
# 递归结束条件
if n == 1:
return 0
res = findlast(n-1,k)%n
return res
# 从1开始编号
# 将0映射为n
def findlast(n: int, k: int) -> int:
# 递归结束条件
if n == 1:
return 1
res = findlast(n-1,k)%n
if res == 0:
res = n
return res
# 将区间[1,n]变换为[0,n-1]
def findlast(n: int, k: int) -> int:
# 递归结束条件
if n == 1:
return 1
res = (findlast(n-1,k)-1)% n
return res + 1
迭代解法
def findlast(n: int, k: int) -> int:
# 编号从1开始
x = 0
for i in range(2,n+1):
x = (x + k) % i
return x + 1 # return x 编号从0开始
复杂度分析
-
时间复杂度:递归解法O(n),迭代解法O(n)
-
空间复杂度: 递归解法O(n),迭代解法O(1)
总结
约瑟夫环问题是一个经典的数学算法问题,在不同的刷题平台上有不同的变体,但是不管如何变化算法的思想是不变的。对于算法题的学习,一题多解的思考是很重要的。现在出现了许多AI编程产品,也为学习算法提供了有效的工具。