约瑟夫环问题求解
约瑟夫问题
约瑟夫问题是个著名的问题:N个人围成一圈,第一个人从1开始报数,报M的将被杀掉,下一个人接着从1开始报。如此反复,最后剩下一个,求最后的胜利者。
例如只有三个人,把他们叫做A、B、C,他们围成一圈,从A开始报数,假设报2的人被杀掉。
- 首先A开始报数,他报1。侥幸逃过一劫。
- 然后轮到B报数,他报2。非常惨,他被杀了
- C接着从1开始报数
- 接着轮到A报数,他报2。也被杀死了。
- 最终胜利者是C
求解
数组
我们可以直接用数组去模拟这个过程,遍历,代码如下,过程比较直观,注释辅助阅读
1 n, m = map(int, input().strip().split()) 2 3 a = [0] * (n + 1) 4 5 # cnt记录当前出局人数,i 用来记录数组中元素的位置, k用来记录本局游戏中报数 6 cnt, i, k = 0, 0, 0 7 8 while cnt != n: 9 i += 1 10 11 # 避免数据越界,重新开始 12 if i > n: 13 i = 1 14 15 if a[i] == 0: 16 k += 1 17 if k == m: 18 a[i] = 1 # 已经出局,标记为1 19 k = 0 # k重新开始计数 20 cnt += 1 # 出局人数加一 21 22 # 打印出局人位置 23 print(i) # 位置从1开始 24 # print(i - 1) # 位置从0开始
链表
除了数组,我们可以用链表的方法去模拟这个过程,N个人看作是N个链表节点,节点1指向节点2,节点2指向节点3,……,节点N-1指向节点N,节点N指向节点1,这样就形成了一个环。然后从节点1开始1、2、3……往下报数,每报到M,就把那个节点从环上删除。下一个节点接着从1开始报数。最终链表仅剩一个节点。它就是最终的胜利者。
1 class ListNode: 2 def __init__(self, val=0, next=None): 3 self.val = val 4 self.next = next 5 6 7 def list(): 8 head = ListNode(1) 9 p = head 10 i = 2 11 12 while i <= n: 13 node = ListNode(i) 14 p.next = node 15 p = node 16 i += 1 17 18 p.next = head 19 20 p = head # p指向头结点 21 r = ListNode() # 保存p的前一个节点 22 while p.next != p: 23 for i in range(1, m): 24 r = p 25 p = p.next 26 print(p.val) 27 r.next = p.next 28 p = p.next 29 30 print(p.val) # 输出最后一个节点 31 32 33 if __name__ == '__main__': 34 n, m = map(int, input().strip().split()) 35 list()
缺点:
要模拟整个游戏过程,时间复杂度高达O(nm),当n,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。
递归
首先从特例开始体会这个问题
特例:m = 2
当m = 2时候,是一个特例,能快速求解
即使是特例2 ,也有两种情况,
情况一:n 为2的倍数, n = 2^k
如果只有2个人,显然剩余的为1号
如果有4个人,第一轮除掉2,4,剩下1,3,3死,留下1
如果是8个人,先除去2,4,6,8,之后3,7,剩下1,5,除去5,又剩下1了
定义J(n)为n个人构成的约瑟夫环最后结果,
J(n) = 2^k - 2^k-1 = 2^k-1 n=2^k
J(n) = 2^k-1 - 2^k-2 = 2^k-2 n=2^k-1
………
J(2^2) = 2^2 - 2^1 = 2^1 n=2^2
递推得到如上结果,起始我们仔细分析也就是每次除去一半的元素,然后剩余的一半继续重复之前的策略,再除去一半。(可想到递归)结合:J(2) = 1 我知道两个数,从1开始,肯定是2先死,剩下1.
得到:j(2^k) = 1
情况二:n 不为2的倍数, n = 2^k+t
当n不为2的倍数,如 n = 2^k+t 时, 容易想到,当剔除了t个元素以后,就形成了上述的情况一,例如
假设n = 11,这时候n = 2^3 + 3,也就是说t = 3,所以开始剔除元素直到其成为2^k问题的约瑟夫问题。
So,我们在剔除了t(3)个元素之后(分别是2,4,6),此时我们定格在2t+1(7)处,并且将2t+1(7)作为新的一号,而且这时候的约瑟夫环只剩下2^3,也就是J(2^3 + 3) = 2*3 + 1 = 7,答案为7
总结一下这个规律:J(2^k + t) = 2t+1
一般解法
当q ≠ 2:
我们假定:
- n — n人构成的约瑟夫环
- m — 每次移除第m个人
-
约定:
- Jm(n)表示n人构成的约瑟夫环,每次移除第m个人的解
- n个人的编号从0开始至n-1
我们沿用之前特例的思想:能不能由Jm(n+1)的问题缩小成为J(n)的问题(这里的n是n+1规模的约瑟夫问题消除一个元素之后的答案),Jm(n)是在Jm(n+1)基础上移除一个人之后的解。也就是说,我们能由Jm(n)得到Jm(n+1)。
规律:Jm(n+1) = ( Jm(n) + m ) / (n+1)
推导过程:
初始情况: 0, 1, 2 ......n-2, n-1 (共n个人)
第一个人(编号一定是(m-1)%n,设之为(k-1) ,读者可以分m<n和m>=n的情况分别试下,就可以得出结论) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k==m%n的人开始):
k k+1 k+2 ... n-2, n-1, 0, 1, 2, ...,k-3, k-2
现在我们把他们的编号做一下转换:
x' -> x
k --> 0
k+1 --> 1
k+2 --> 2
...
...
k-2 --> n-2
k-1 --> n-1
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗!
x ->x'?(这正是从n-1时的结果反过来推n个人时的编号!)
0 -> k
1 -> k+1
2 -> k+2
...
...
n-2 -> k-2
变回去的公式 x'=(x+k)%n
那么,如何知道(n-1)个人报数的问题的解?只要知道(n-2)个人的解就行了。(n-2)个人的解呢?只要知道(n-3)的情况就可以了 ---- 这显然就是一个递归问题:
令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果就是f[n]
递推公式
f[1] = 0;
f[i] = ( f[i-1] + k ) % i = ( f[i-1] + m % i) % i = ( f[i-1] + m ) % i ; (i>1)
代码
1 # 递归 2 def josephus(n, m): 3 if n == 1: 4 return 0 5 return (josephus(n - 1, m) + m) % n 6 7 8 # 迭代 9 def josephus_2(n, m): 10 res = 0 11 for i in range(2, n + 1): 12 res = (res + m) % i 13 return res 14 15 16 if __name__ == '__main__': 17 print(josephus(10, 3) + 1) 18 print(josephus_2(11, 3) + 1)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?