约瑟夫环问题求解

约瑟夫问题

约瑟夫问题是个著名的问题: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)

 

posted @ 2022-01-03 21:17  r1-12king  阅读(132)  评论(0编辑  收藏  举报