约瑟夫环问题是数学和计算机科学中的一个经典问题,详见约瑟夫问题百度词条。之前在计算机二级考试中遇到过,当时用C语言写的,感觉有点复杂。现在学习python,发现可以用列表的pop方法完美模拟把人移除的操作,因此重新用python写了一下:
问题的具体描述:有34个人围成一圈,顺序排号。从第一个人开始报数(从1到3报数),凡报到3的人退出圈子,问最后留下的是原来第几号的那位。
python代码:
ring_list = [ i+1 for i in range(34)]
bias = 0
print(ring_list)
while(len(ring_list)>1):
pop_time = (len(ring_list)+bias)//3
residue = (len(ring_list)+bias)%3
pop_index = [ i*3-bias+2 for i in range(pop_time)]
pop_index.sort(reverse =True)
for index in pop_index:
ring_list.pop(index)
print(ring_list)
bias = residue
输出结果:
[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]
[1, 2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17, 19, 20, 22, 23, 25, 26, 28, 29, 31, 32, 34]
[1, 4, 5, 8, 10, 13, 14, 17, 19, 22, 23, 26, 28, 31, 32]
[1, 4, 8, 10, 14, 17, 22, 23, 28, 31]
[1, 4, 10, 14, 22, 23, 31]
[1, 10, 14, 23, 31]
[10, 14, 31]
[10, 31]
[10]
[Finished in 0.7s]
我们可以看到,最终留下的人为10号。
我们也可以把这段代码封装成函数,把34和3作为参数传入,也可得到同样的结果:
def joseph(total_num,pop_num):
ring_list = [ i+1 for i in range(total_num)]
bias = 0
print(ring_list)
while(len(ring_list)>1):
pop_time = (len(ring_list)+bias)//pop_num
residue = (len(ring_list)+bias)%pop_num
pop_index = [ i*3-bias+2 for i in range(pop_time)]
pop_index.sort(reverse =True)
for index in pop_index:
ring_list.pop(index)
print(ring_list)
bias = residue
joseph(34,3)
我们也可以使用del命令来实现约瑟夫环的功能,其中n,k,s分别表示总人数、kill的编号和最后幸存者的数量。
def joseph_survivor(n,k,s):
person_list = []
for i in range(1,n):
person_list.append(i)
index,count = 0,1
list_len = len(person_list)
while (list_len>s):
while(index<list_len):
if count==k:
del person_list[index]
count = 0
index -= 1
list_len = len(person_list)
if list_len==s:
break
index += 1
count += 1
index = 0
return person_list
print(joseph_survivor(42,3,1))
运行结果如下:
[31]
[Finished in 0.3s]
2020年5月19日更新:
以上的方法使用的是while循环,今天给同学们上课,突然想到了一种使用for循环的方法,答题的思路就是以每次处决一个人(我把他定义为pop_guy),循环的次数就可以通过总人数减去幸存者人数来计算出来,每次循环,就执行一次处决操作,并且更新下一个处决人的index编号。具体的代码如下所示:
num = 41 #总人数为41人
suv = 2 #幸存者为2人
step = 3 #数到3处决1人
index = 0 #初始的index编号从0开始
lst = [i+1 for i in range(num)] #列表生成式生成一个41位长度的数字列表代表打开的环
def update_index(index,lst_length): #根据index和列表长度的关系更新index值
if (index + step - 1) >= lst_length-1: #如果列表太短,那么需要考虑循环计数的情况
index = (index + step -1 -lst_length)%lst_length
else:
index = index + step - 1 #如果列表足够长,那么只需要增加步长-1即可
return index #最后返回更新后的index值,也就是下一个需要处决的人的编号
def kill_popguy(index,lst):
lst_length = len(lst)
index = update_index(index,lst_length) #更新index的值
popguy = lst.pop(index) #kill处决对象
return popguy,index,lst #返回处决对象及其编号和更新后的id列表
for i in range(num-suv): #逐个kill处决对象,直到剩下2个幸存者
(popguy,index,lst)=kill_popguy(index,lst)
# print(popguy,lst)
print(lst) #打印幸存者id名单
运行结果:
2020年5月21日更新
今天突然发现,以上的代码可以进一步化简,对于index的更新,可以用一行代码来进行概括,就是
index = (index + step - 1 - len(lst)) % len(lst)
完美的统一了在队中和队尾的两种情况,这样,代码就可以缩减到6行如下:
num,suv,step,index = 41,2,3,0
lst = [i+1 for i in range(num)]
for i in range(num-suv):
index = (index + step - 1 - len(lst)) % len(lst)
lst.pop(index)
print(lst)