问题描述
在一间房间总共有n个人,给定一个数k,然后按照如下规则去杀人:
- 所有人围成一个圆圈,按顺时针依次给所有人编号:1, 2, 3…, n
- 由编号1开始报数,按顺时针方向,报到数字k的人将被杀掉
- 被杀掉的人从房间内被移走,从被杀的下一个人重新由1开始报数
- 报到数字k的人再次被杀掉,再移走,再次开始报数,一直杀到最后剩余一个人
最后剩余的人活命。
思路一
根据问题描述,可以使用循环单链表模拟杀人过程:
- 表头是1号,表尾是n号,循环单链表的表尾指向表头模拟圆圈
- 指针从表头1号开始走,当指到第k个节点时,即当报k的被杀时,就将该节点从链表中删除。
- 删除该节点后,从该节点的下一个节点开始,再从1走到k,
- 再次删除第k节点,一直到某节点的下一个节点指向自己,说明只有一个节点了,即最后活下的人
根据上面分析循环单链表的操作过程,代码实现如下:
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
26
27
28
29
30
31
32
33
34
35
36
37
|
class Node(object):
def __init__(self, value):
self.value = value
self.next = None
def create_linkList(people_num):
"""创新循环单链表"""
head = Node(1)
pre = head
for i in range(2, people_num+1):
newNode = Node(i)
pre.next = newNode
pre = newNode
pre.next = head
return head
people_num = 5 # 总人数
k = 2 #报k被杀
if k == 1:
print("最后存活编号:" + str(people_num))
else:
head = create_linkList(people_num)
pre = None
cur = head # 当前报数的人
while cur.next != cur: # #终止条件是节点的下一个节点指向本身,即只剩一个节点
for i in range(k-1):
# 走到第k节点
pre = cur
cur = cur.next
print("杀掉:" + str(cur.value)) # 被删除节点编号
# 删除节点
pre.next = cur.next
# 从被删除节点的下一个节点从新报数
cur.next = None
cur = pre.next
print("最后存活者编号是:" + str(cur.value))
|
这种方法的时间复杂度为:O(n*k),当人数量n很大,报的数k也很大时,并不适用。
思路二
递归思路,假设房间共有n = 10个人,初始编号为1,2,3,…10,设初始编号对应的编号位置为0, 1, 2, …9, 每次数到k = 3
的人杀死,求最后活下来的人的初始编号是几?
来看杀人过程:
(表中红色为报数k=3的被杀死的人的编号,绿色为最后活下来的人的编号)
仔细观察表中每一轮初始编号的移动规律:
第二轮到第一轮的编号移动规律: (第二轮的编号x的编号位置 + k) % 10 ==> 第一轮编号x的编号位置
比如第二轮编号5的编号位置是1, (1 + 3) % 10 ==> 4, 得到第一轮编号5的的编号位置是4
进而得到第三轮到第二轮的编号移动规律:(第三轮编号x的编号位置 + k) % 9 ==> 第二轮编号x的编号位置
比如第三轮编号5的编号位置是7, (7 + 3) % 9 –> 1, 得到第二轮编号5的的编号位置是1
进而得到第N轮与第N-1轮的编号移动规律:(第N轮的编号x的编号位置 + k) % 第N-1轮总人数 ==> 第N-1轮编号x的编号位置
最后一轮存活着的编号x对应的编号位置一定是0, 那么根据以上规律,可以得到倒数第二轮编号x对应的编号位置,根据规律进一步可以得到倒数第三轮编号x对应的编号位置, 一直可以推导出第一轮编号x的对应编号位置,由第一轮编号x的对应编号位置+1
得到的便是最后存活的人的初始编号。
由上总结,当房间共有n个人,报数k杀死时,令f(n, k)表示最后存活着的编号位置,则有递归公式:
- n = 1: f(1, k) = 0;
- n > 1: f(n, k) = (f(n-1, k) + k) % n;
有了递推公式以后,代码实现如下:
1
2
3
4
5
6
7
8
9
|
def josephus(n, k):
if n == 1:
return 0
else:
return (josephus(n - 1, k) + k) % n
n = 10
k = 3
print("最后存活者编号是:", josephus(n, k)+1) # 4
|
对思路二的优化
对递归思路的进一步优化,假设n非常大,而k又比较小,比如n=100, k=3, 被杀过程如下:
- 第一轮: 有100个人,每次报k=3的被杀,总共杀死了 math.floor(100/3) = 33个人,剩余67个人
- 第二轮: 有67个人,每次报k=3的被杀,总共杀死了 math.floor(67/3) = 22个人,剩余45个人
- 第三轮: 有45个人,每次报k=3的被杀,总共杀死了 math.floor(45/3) = 15个人,剩余30个人
- 第四轮: 有30个人,每次报k=3的被杀,总共杀死了 math.floor(30/3) = 10个人,剩余20个人
- 第五轮: 有20个人,每次报k=3的被杀,总共杀死了 math.floor(20/3) = 6个人,剩余14个人
- 第六轮: 有14个人,每次报k=3的被杀,总共杀死了 math.floor(14/3) = 4个人,剩余10个人
- 第七轮: 有10个人,每次报k=3的被杀,总共杀死了 math.floor(10/3) = 3个人,剩余7个人
- 第八轮: 有7个人,每次报k=3的被杀,总共杀死了 math.floor(7/3) = 2个人,剩余5个人
- 第九轮: 有5个人,每次报k=3的被杀,总共杀死了 math.floor(5/3) = 1个人,剩余4个人
- 第十轮: 此时,总人数n=4, 报的数k=3,再利用思路二中的递归方法求解最后剩余者编号
在上面杀人过程中,通过建立n/k的步长加快了杀人的速度,减少了算法时间。可以从下面这幅图中更加清晰的体会到:
本来需要10轮的,现在只需要7轮,如果n=100,k=3的话优化效果会更明显。
根据以上分析,优化方法如下:
- math.floor(n/k) == 1: 用思路二中方法求解
- math.floor(n/k) > 1: n = n - math.floor(n/k)
实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import math
def josephus(n, k):
if n == 1:
return 0
else:
return (josephus(n - 1, k) + k) % n
def kill_people(n, k):
while math.floor(n/k) > 1:
# 建立一个步长为n/k的递归过程;
n = n - math.floor(n/k)
kill_people(n, k)
live_index = josephus(n, k)
return live_index+1
n = 10
k = 3
print("最后存活者编号是", kill_people(n,k))
|
思路三
使用数组存储房间中的每个人: arr = [ i for i in range(1, 10+1) ]
arr数组代表房间里的10个人:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
每次被杀的人的编号: kill_num = (kill_num + k - 1) % len(arr)
。 其中的(k-1)对应数组的下标
有了被杀人的的编号后,将其pop出数组。
然后再次计算下一个被杀人的编号,直到数组中只剩一个人。
代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
|
def josephus(n, k):
arr = [ i for i in range(1, n+1) ]
kill_num = 0
while len(arr) != 1:
kill_num = (kill_num + k - 1) % len(arr)
print("杀死:" + str(arr.pop(kill_num)))
return arr[0]
n = 10
k = 3
print("最后存活者编号是:", josephus(n, k)) # 4
|
对思路三的优化
在思路三中需要构建一个数组,也可以不用数组来减少内存。使用动态规划来解:
1
2
3
4
5
6
7
8
9
|
def Josephus(n, k):
kill_num = 0
for i in range(1, n+1):
kill_num = (k + kill_num) % i
return kill_num + 1
n = 5
k = 2
print("最后存活者编号:", Josephus(n, k))
|
最后这个动态规划的方法来自:https://www.quora.com/What-is-the-best-solution-for-Josephus-problem-algorithm