每日一題.每日一練 .15圓圈中最後剩下的數字(爲什麼沒有14,因爲幾何比約瑟夫環難多了,現在還沒明白)(“鍘刀思想”,或者叫咔擦思想)

這篇文章是我通過靈感所寫的是一種全新的理解約瑟夫環的方法,希望能幫到一些還沒有理解的朋友。所以我會盡力詳細的講述這篇文章。

面試題62. 圓圈中最後剩下的數字
0,1,n-1這n個數字排成一個圓圈,從數字0開始,每次從這個圓圈裏刪除第m個數字。求出這個圓圈裏剩下的最後一個數字。

例如,0、1、2、3、4這5個數字組成一個圓圈,從數字0開始每次刪除第3個數字,則刪除的前4個數字依次是2、0、4、1,因此最後剩下的數字是3。

示例 1:

輸入: n = 5, m = 3
輸出: 3

示例 2:

輸入: n = 10, m = 17
輸出: 2

在這前我們先來了解一下約瑟夫環問題:

17世紀的法國數學家加斯帕在《數目的遊戲問題》中講了這樣一個故事:15個教徒和15 個非教徒在深海上遇險,必須將一半的人投入海中,其餘的人才能倖免於難,於是想了一個辦法:30個人圍成一圓圈,從第一個人開始依次報數,每數到第九個人就將他扔入大海,如此循環進行直到僅餘15個人爲止。問怎樣排法,才能使每次投入大海的都是非教徒。(還有另一個版本但是他太長了==)

如果死去的人靈魂還能佔着空位,這題會變的非常簡單,如果我們每次扔進海里一個就重新從開頭數數,這題也會非常簡單,然而並不能,所以約瑟夫環的問題就有兩個:
1:我們沒有固定的環長,意味着我們數着的數肯定不成倍數增加(也就是沒有什麼明顯的規律):
2:沒有固定的開始地點,這個人被扔海里接着數後面的,所以我們之後的會受到環長的影響。

感謝在我嘗試理解約瑟夫環的過程中,一位大佬的文章裏提到了

每當一個人被推進海里時,所有的索引值會前進m,

也就是我們每次數到就要殺人的那個數,所有的索引值都要前進m,爲什麼,我們不是死了一個人嗎,爲什麼索引值前進了那麼多呢”但是,隨着思考我突然冒出了一種新的思想,這種思想我稱爲“鍘刀思想”(淦))

這種思想的根本在於,我們通常會把數數的過程看做指針的移動,當指針移動到符合條件時,我們刪掉(殺掉)這個人,接着讓指針往下移動,這是一般的想法,而"鍘刀思想"並非如此,在“鍘刀思想”裏移動的不是“”鍘刀”(指針),而是人(數組)

這種思想是這樣,由於我們最後肯定會剩下一個數據,這個數據的索引值必定是0,所以我們在數組的開始設置一個鍘刀,你可以認爲是這樣(這裏我們用實例1的數據)

“”“”“”“”“”“”“”“”鍘刀 0 , 1, 2 , 3 , 4(引號是爲了對齊和下面的情況對齊,抱歉我是個菜鳥)

然後我們對着人(數組)喊,過來吧,由於n=3,所以每當過來到第三個人的時候,我們就咔嚓一刀

0 ,1 ,2(鍘刀到3,咔擦!),3,4

2陣亡了,由於這是一個環,我們跟 0 和 1 說,你回去接着排隊吧,0,1:誒好嘞

“”“”“”“”“”“”“”“”鍘刀 3 ,4,0,1

然後我們對這個過程進行週而復始的循環,直到最後只剩一個人站在鍘刀前,也就是索引值爲0的位置

“”“”“”“”“”“”“”“”“鍘刀 3

恭喜3獲得了大逃殺的勝利(霧)

也就是說,“鍘刀思想”的根本在於刪除點一直在數組的最前端,而我們是通過讓人走過鍘刀,回到隊尾來實行計數,每有走過鍘刀的過程我們就記+1,如果加完變成n我們就咔擦一刀,然後歸零。

那麼現在,我們將所有過程反推,最後一個元素(在這個事例中是3)是怎麼來到鍘刀面前呢?(3的索引值變成0),而“鍘刀思想”的妙處就在於,他給出了每一次在咔擦後索引值的變化,”由於每次數組都是整體移動,我們可以認爲每個元素在兩次咔嚓間都進行了m單位的左位移”(重點)

我們回到上一個過程,另一個元素在和3鬥智鬥勇的時候,他們兩個經歷了輪流走到後面的過程,我們就叫他甲好了,那麼喊號過程是這樣的

鍘刀計數爲零

鍘刀, 甲, 3

鍘刀計數爲1,甲通過

鍘刀 ,3 ,甲

鍘刀計數爲2,3通過

鍘刀 ,甲 ,3

鍘刀計數在甲走的時候變成了3 咔嚓一刀甲沒了

鍘刀 3 (你贏了)

我們看一下這個移動過程就會發現,儘管3進行了左m單位的位移,但是這裏面出現了跑圈現象,3並不是沒有經過鍘刀,而是經過鍘刀成功跑了一圈,於是我們將這個過程反推,把左位移用右位移倒放回去,注意此時3的索引值是零,我們假設一秒動一次

“”“”“”“”“”0, 1(索引值)
現在:3 ,甲(如果沒死的話)
前1秒:甲,3
前2秒:3,甲
前3秒:甲,3

我們可以看到,3在每往右移兩個單位就要回到原處,也就是說,3實際上發生的位移是3%2=1,跑一圈甚至幾圈不會對3造成實際的位移。於是我們的上一時刻的索引值便有如下推導式

上一時刻索引值=(當前索引值+右移位移距離)%上一時刻圈長

我們代入數據驗證一下

(0+3)%2=1

由此我們的兩個問題就都解決了:
一方面,我們固定在開始刪除人。
另一方面,我們把圈長的變化精確到了每個時刻。

於是我們可以利用遞推不斷推勝利者在上個時刻的位置,最後便可得到得到在最初時刻的時候,也就是圈長爲n的時候,勝利者的索引值

好,說了這麼多,我們來總結一下過程

整個過程在執行這三部分
隊列整體移動
咔嚓(在開始刪人)
圈長減小

移動的值是個定值這點是思想的核心。因此刪掉的人是否佔位只會影響圈長的大小,而不影響整體移動的量

於是代碼如下:

class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        f = 0 #最後勝利者索引值爲0
        for i in range(2, n + 1):     #對於每個上一時刻時刻的圈長的逆推,變化從2到n
            f = (m + f) % i     #m+f是將這時刻的索引值推回上一時刻,%i是不算跑圈後得到實際更新的索引值
        return f        #在最後的索引值,即全場爲n,剛開始是勝利者的索引

而這題我們把n想成一個數組,會發現n的索引值和留下來的數據是相等的,
所以輸出索引值等於輸出數值,其他情況做些加減便好

咔嚓!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章