約瑟夫環
1. 報數,刪除報到k的人,直到只剩下一個人
題目:已知n個人(以編號1,2,3...n分別表示)圍坐在一張圓桌周圍。從編號為1的人開始報數,數到m的那個人出列;他的下一個人又從1開始報數,數到m的那個人又出列;依此規律重復下去,直到圓桌周圍的人全部出列。通常,我們會要求輸出最后一位出列的人的序號。那么這里主要研究的是最后一個出列的人的序號要怎么確定。
首先,數組寫成以0開頭更便於計算,所以后面的分析都是以0開頭的。
分析:
舉個栗子:總人數sum為10人,從0開始,每報到4就把一人扔下去(value=4)。
初始情況為: 0 1 2 3 4 5 6 7 8 9
扔下去一個之后: 0 1 2 4 5 6 7 8 9
此時,這些編號已經不能組成一個環,但是可以看出4至2之間還是連着的(4 5 6 7 8 9 0 1 2),且下一次報數將從4開始。但是,之后的報數將總要考慮原編號3處的空位問題。
如何才能避免已經產生的空位對報數所造成的影響呢?
可以將剩下的9個連續的數組成一個新的環(將2、4連接),這樣報數的時候就不用在意3的空位了。但是新產生的環的數字並非連續的,報數時不像之前那樣好處理了(之前沒人被扔海里時下一個報數的人的編號可以遞推,即(當前編號+1)%sum ),無法不借助存儲結構得知下一個應該報數的現存人員編號。
如何使新環上的編號能夠遞推來簡化我們之后的處理呢?
可以建立一種有確定規則的映射,要求映射之后的數字可以遞推,且可以將在新環中繼續按原規則報數得到的結果逆推出在舊環中的對應數字。
方法:將它與 sum-1 個人組成的(0 ~ sum-1)環一 一映射。
比如之前的栗子,將剩余的 9 人與 9 人環(0~8)一 一映射
既然 3 被扔到海里之后,報數要從4開始 (4 其實在數值上等於最大報數值),那么就將4映射到0~8的新環中0的位置,也就是說在新環中從0開始報數即可,且新環中沒有與3對應的數字,因此不必擔心有空位的問題。從舊環的 4 開始報數等效於從新環中的 0 開始報數。
原始 0 1 2 3 4 5 6 7 8 9
舊環 0 1 2 4 5 6 7 8 9
新環 6 7 8 0 1 2 3 4 5
新環有這么一個優勢: 相比於舊環中2與4之間在數學運算上的不連續性,新環8和0之間在對9取余的運算中是連續的,也就是說根本不需要單獨費心設計用以記錄並避開已產生的空位(如 編號3)的機制 ,新環的運算不受之前遺留結果的掣肘。同時只要能將新環與舊環的映射關系逆推出來,就能利用在新環中報數的結果退出之前舊環中的報數結果。
以下是新環與舊環中下一個要人扔海里的人位置:
舊環 0 1 2 4 5 6 7 8 9
新環 6 7 8 0 1 2 3 4 5
如何由新環中的 3 得到舊環中的 7 呢。其實可以簡單地逆推回去 : 新環是由 (舊環中編號-最大報數值)%舊總人數 得到的,所以逆推時可以由 ( 新環中的數字 + 最大報數值 )% 舊總人數 取得。即 old_number = ( new_number + value ) % old_sum.
如 : ( 3 + 4 ) % 10 =7 .
也就是說在,原序列( sum ) 中第二次被扔入海中編號可以由新序列( sum - 1) 第一次扔海里的編號通過特定的逆推運算得出。
而新序列 (sum -1)也是(從0開始)連續的,它的第二次被扔入海中的編號由可以由(sum - 2)的第一次扔入海里的編號通過特定的逆推運算得出,並且它的第二次被扔入海中的編號又與原序列中的第三次被扔入海里的編號是有對應關系的。
也求是說有以下推出關系: (sum-2)環的第1次出環編號 >>>(sum-1)環的第2次出環編號 >>>(sum)環的第3次出環編號
即 在以 k 為出環報數值的約瑟夫環中, m人環中的第n次出環編號可以由 (m-1) 人環中的第 (n-1) 次出環編號通過特定運算推出。
幸運的是,第一次出環的編號是可以直接求出的,也就是說,對於任意次出環的編號,我們可以將之一直降到第一次出環的編號問題,再一 一 遞推而出。
注意 以下圖示中的環數字排列都是順序的,且從編號0開始。

由圖知,10人環中最后入海的是4號,現由其在1人環中的對應編號0來求解。

通過以上運算,其實我們已經求出分別位於9個環中九個特定次數的結果,只不過我們需要的是10人環的結果罷了。
這種方法既可以寫成遞歸也可以寫成循環,它對於求特定次數的出環編號效率較高。遞歸就比較好寫了,出口即是當次數為1時。實際編號是從1開始,而不是0,輸出時要注意轉換
代碼1:
def lastRemaining(n,m):
if n < 1:
return -1
con = list(range(n))
final = -1
start = 0
while con:
k = (start+m-1)%n
final = con.pop(k)
n -= 1
start = k
return final
res = lastRemaining(10,4)
print(res)
代碼2: 一共有三十個人,從1-30依次編號。每次隔9個人就踢出去一個人。求踢出的前十五個人的號碼:
a = [ x for x in range(1,31) ] #生成編號 del_number = 8 #該刪除的編號 for i in range(15): print a[del_number] del a[del_number] del_number = (del_number + 8) % len(a)
2. 按遞增間隔刪數,直到最后只剩下一個數
題目:有數組A,每個元素存放相應的下標,即A[i]=i,要求按照1,2,3,...的規律刪除數組中的元素,第一次間隔1個數,第二次間隔2個數,依次類推,到末尾時循環至開頭繼續進行,求最后一個被刪掉的數的原始下標位置

思路:
count用來記錄要刪除的數在數組中的下標,num是要刪除的間隔
step1:刪除第一個元素,num=1,count=1,刪除1


step2:刪除第二個元素,num=2,count=1+2=3,刪除4


step3:刪除第三個元素,num=3,count=3+3=6,刪除8


step4:刪除第四個元素,num=4,count=6+4=10>7,count=10%7=3,刪除5



step5:刪除第五個元素,num=5,count=3+5=8>7,count=8%6=2,刪除3



step6:刪除第六個元素,num=6,count=2+6=8>5,count=8%5=3,刪除7



step7:刪除第七個元素,num=7,count=3+7=10>4,count=10%4=2,刪除6



step8:刪除第八個元素,num=8,count=2+8=10>3,count=10%3=1,刪除2



step9:刪除第九個元素,num=9,count=1+9=10>2,count=10%2=0,刪除0



代碼如下:
def remove_element(input_list):
count = 0
num = 0
while len(input_list) != 1:
num += 1#間隔1,2,3,...直到只剩下一個數
count = count + num#1 3 6 10 15% 21 28 36
#按間隔排的數1,3,6,10,8,8,10,10,10---1,3,6,3,2,3,2,1,0
# print('count=%s'%count)
if count >= len(input_list):
count = count % len(input_list)
print('count=%s,刪除%s'%(count,input_list[count]))
input_list.pop(count)
print(input_list[0])
remove_element([0,1,2,3,4,5,6,7,8,9])
最后剩下的數為9
參考文獻:
【4】約瑟夫環問題
