#! /usr/bin/env python3
#-*- coding:utf-8 -*-
”’约瑟夫环的问题,递推方法的理解”’
n个人循环报数,杀第k个人
def fn(n,k):
L = list(range(1,n+1))
if n == 1:
return
else:
index_ = 0
for i in range(n-1):
index_ = (index_ + k) % len(L) - 1
print('出局的人是:',L[index_])
del L[index_]
if index_ < 0:
index_ = 0
print('最后存活的人:',L[0])
fn(10,6)
”’假设有10个人,编号1-10号,按顺序报数到第6个人时出局,然后循环报数,每到第6个人就出局,求此过程中的最后存活者.
先来做个模拟流程:
第一次报数顺序:L = [1,2,3,4,5,6,7,8,9,10],把此顺序当成原队列顺序
第一次出局6,原队列顺序变为:[1,2,3,4,5,7,8,9,10],然后从7开始继续数6位,数到10的时候发现人数不够,就从1开始借人继续数2位
第二次出局2,原队列顺序变为:[1,3,4,5,7,8,9,10],然后从3开始继续数6位
第三次出局9,原队列顺序变为:[1,3,4,5,7,8,10],然后从10开始继续数6位,因为人数不够,从1开始借人继续数
第四次出局7,原队列顺序变为:[1,3,4,5,8,10],然后从8开始继续数6位,因为人数不够,从1开始借人继续数
第五次出局5,原队列顺序变为:[1,3,4,8,10],然后从8开始继续数6位,因为人数不够,从1开始借人继续数
第六次出局8,原队列顺序变为:[1,3,4,10],然后从10开始继续数6位,因为人数不够,从1开始借人继续数
第七次出局1,原队列顺序变为:[3,4,10],然后从3开始继续数6位,因为人数不够,从3开始借人继续数
第八次出局10,原队列顺序变为:[3,4],然后从3开始继续数6位,因为人数不够,从3开始借人继续数
第九次出局4,原队列顺序变为:[3].最后[3]存活!
我们现在唯一可以知道的是,最后存活的人在原队列中一定新排在第一位,用索引表示为L[0]
我们再从头推导一次:
第一次报数列表:L = [1,2,3,4,5,6,7,8,9,10]
从1开始数6位,第一次出局的人为6,索引为L[5]
出局6后,L变为:L = [1,2,3,4,5,7,8,9,10].
新的报数顺序为:7,8,9,10,1,2,3,4,5
从7开始数6位,第二次出局的人为2,索引为L[1]
怎么在两个出局者的索引中找到规律呢?从新报数的第一个人:7 开始突破
我们发现7在原列表(第一次报数列表)中的索引为L[6],6出局后,因为原报数列表长度的改变,索引变为L[6-1]
第二次出局的人为2,在原列表(第一次报数列表)中的索引本应该为L[5+6],意思是在6的索引L[5]后,往后数6个人,即为在L[11].但此时从7数到10,也不够数到6个人,只能让先7-10先报完数,再从开头借几个人.怎么算借几个人呢?用求余数的方法.
求余:
求余可以理解为:有10个人,但只有0-5的号码牌(6张牌),即:
10个人按顺序站一起时,手上拿的号码牌显示为:0,1,2,3,4,5,0,1,2,3
那么:第7个人拿的号码牌是 7 % 6(张牌) == 1 这个1代表的是6张牌的第一张牌,牌值为1-1=0
第8个人拿的号码牌是 8 % 6(张牌) == 2 这个2代表的是6张牌的第二张牌,牌值为2-1=1
第9个人拿的号码牌是 9 % 6(张牌) == 3 这个3代表的是6张牌的第三张牌,牌值为3-1=2
第10个人拿的号码牌是 10 % 6 == 4 这个4代表的是6张牌的第四张牌,牌值为4-1=3
…
回头再看1-9号人:
第1个人拿的号码牌是 1 % 6 == 1 这个1代表的是6张牌的第一张牌,牌值为1-1=0
第2个人拿的号码牌是 2 % 6 == 2 这个2代表的是6张牌的第二张牌,牌值为2-1=1
第3个人拿的号码牌是 3 % 6 == 3 这个3代表的是6张牌的第三张牌,牌值为3-1=2
…
可以看出规律:第n个人拿的号码牌的牌值是: (n % 有几张牌) – 1
几张牌就是代表剩余的人数
号码牌的牌值就是代表索引位置
知道索引中的规律后,我们再来推导一遍:
第一次出局的人在L = [1,2,3,4,5,6,7,8,9,10]中索引为L[5],即数字6.把6踢出局后,L变为9个人:L = [1,2,3,4,5,7,8,9,10],下一次从数字7开始数6个人,但7-10只有4人,所以需要从数字1以后接着数2人.那么第二次出局者的索引即为L[(5+6) % 9 – 1],即L[1],即2.2也出局后,此时的L = [1,3,4,5,7,8,9,10].那么第三次出局者的索引即为L[(1+6) % 8 – 1],即L[6],即9.
第四次出局者的索引为:L[(第三次出局者的索引+6) % 7 – 1]
…
第九次出局者的索引为:L[(第八次出局者的索引+6) % 2 – 1]
而同时我们发现,第一次出局的人的索引为:L[(0+6) % 10 – 1],即L[5]
可以得出公式:在每次把出局者单纯踢出原队列,同时原队列删除出局者,大体保持不变的前提下,每一次出局者的索引为:
L[(上一次出局者的索引+6) % 上一次出局后的剩余人数 – 1]
而第0次出局者的索引为0
共进行的轮数为总人数-1
这种后一个结果是由前一个结果传参解答的问题,就是递归了.那么我们是否可以直接做个for或者while循环,让计算机帮我们计算出每一轮被踢出的人呢?
L = list(range(1,10+1))
index_ = 0 #因为每次出局人的索引是根据上一次出局的人的索引而变,所以单独设了一个变量
for i in range(10-1): #i的每一次遍历,相当于每一轮,从0-8,即为9次
index_ = (index_+6) % len(L) – 1
print(‘出局的人是:’,L[index_])
del L[index_]
if index_ == -1:#或者写成index_ < 0也可 #要保证index_永远是正数.因为当index_为负数时,比如index_ = -1时,进行+6操作本应该从列表开头开始数6位,得到索引L[(0+6) % 剩余人数 -1].但如果是负数时,就不是单纯的像后数6位,而是得到L[(-1+6) % 剩余人数 – 1],出局者的计算会失误.
index_ = 0
print(‘最后的存活者为:’,L)
”’