约瑟夫环问题是数学和计算机科学中的一个经典问题,详见约瑟夫问题百度词条。之前在计算机二级考试中遇到过,当时用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名单

运行结果:

python实现约瑟夫环 python中约瑟夫问题_python实现约瑟夫环


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)