圓圈中最後剩下的數字
題目描述
0,1,n-1這n個數字排成一個圓圈,從數字0開始,每次從這個圓圈裏刪除第m個數字。求出這個圓圈裏剩下的最後一個數字。
例如,0、1、2、3、4這5個數字組成一個圓圈,從數字0開始每次刪除第3個數字,則刪除的前4個數字依次是2、0、4、1,因此最後剩下的數字是3。
- 題目限制
1 <= n <= 10^5
1 <= m <= 10^6
示例
- 示例1
輸入: n = 5, m = 3
輸出: 3
- 示例2
輸入: n = 10, m = 17
輸出: 2
解題思路
隊列模擬
看到這個題,我第一眼想到的是使用隊列去模擬題目所說的情況,先將0-n-1個數字加入到隊列中,開始數數,從0開始到m-1,取出隊列重新加入到隊列尾部,刪除第m個,不斷重複這個過程,直到隊列中只剩1個元素,直接返回這個剩餘的元素即可。
很不幸,這種方法超時了。。。。
var lastRemaining = function(n, m) {
let queue = new Queue()
let index = 0
for (let i=0; i<n; i++) {
queue.enqueue(i)
}
while (queue.size() > 1) {
/**
* 開始數數字
* 不是m的時候,重新加入到隊列的末尾
* 是m的時候,將其從隊列刪除
*/
for (let i=0; i<m-1; i++) {
queue.enqueue(queue.dequeue())
}
// 是m的時候直接刪除
queue.dequeue()
}
let end = queue.front()
return end
};
這種方法不成功是顯而易見的,首先,事件複雜度O(nm),再看一下數據範圍,不超時纔怪!!!!
鏈表模擬
與隊列模擬相同,只不過是換一下數據結構而已!!
使用數組求解
首先,對於本題,假設刪除的位置是index位置的元素,那麼下一個要刪除的是index + m位置的元素,但是由於當前位置的數字刪除了,後面的數字會前移一位,所以實際的下一次刪除的位置是index + m - 1,由於是一個環,所以可以取模:
(index + m - 1) mod n
來從頭開始
// 通過
var lastRemaining = function(n, m) {
let arr = []
for (let i=0; i<n; i++) {
arr.push(i)
}
let id = 0
while(n > 1){
id = (id + m - 1) % n
arr.splice(id,1)
n--
}
return arr[0]
};
這種方法過是僥倖過了,但耗時有點大
數學+遞歸 – 官方答案
-
思路
題目中的要求可以表述爲:給定一個長度爲
n
的序列,每次向後數m
個元素並刪除,那麼最終留下的是第幾個元素?這個問題很難快速給出答案。但是同時也要看到,這個問題似乎有拆分爲較小子問題的潛質:如果我們知道對於一個長度 n - 1 的序列,留下的是第幾個元素,那麼我們就可以由此計算出長度爲 n 的序列的答案。
-
算法
我們將上述問題建模爲函數
f(n, m)
,該函數的返回值爲最終留下的元素的序號。首先,長度爲 n 的序列會先刪除第 m % n 個元素,然後剩下一個長度爲 n - 1 的序列。那麼,我們可以遞歸地求解 f(n - 1, m),就可以知道對於剩下的 n - 1 個元素,最終會留下第幾個元素,我們設答案爲 x = f(n - 1, m)。
由於我們刪除了第 m % n 個元素,將序列的長度變爲 n - 1。當我們知道了 f(n - 1, m) 對應的答案 x 之後,我們也就可以知道,長度爲 n 的序列最後一個刪除的元素,應當是從 m % n 開始數的第 x 個元素。因此有 f(n, m) = (m % n + x) % n = (m + x) % n
我們遞歸計算 f(n, m), f(n - 1, m), f(n - 2, m), … 直到遞歸的終點 f(1, m)。當序列長度爲 1 時,一定會留下唯一的那個元素,它的編號爲 0。
/** class Solution { int f(int n, int m) { if (n == 1) return 0; int x = f(n - 1, m); return (m + x) % n; } public: int lastRemaining(int n, int m) { return f(n, m); } //時間複雜度:O(n)O(n),需要求解的函數值有 nn 個。 //空間複雜度:O(n)O(n),函數的遞歸深度爲 nn,需要使用 O(n)O(n) 的棧空間 */ //官方C++代碼毫無問題,使用JS後 RangeError: Maximum call stack size exceeded報錯!!! function f (n, m) { if (n === 1) { return 0 } let x = f(n-1, m) return (m+x) % n } function lastRemaining(n, m) { return f(n,m) }
意思是遞歸次數太多導致內存消耗過大!!!!好尷尬!!!!
數學+迭代 – 官方答案
-
避免遞歸使用棧空間。
// 完美通過 function lastRemaining(n, m) { let f = 0; for (let i = 2; i != n + 1; ++i) f = (m + f) % i; return f; } //時間複雜度:O(n)O(n),需要求解的函數值有 nn 個。 //空間複雜度:O(1)O(1),只使用常數個變量。反推法
反推法
-
思路
n個人編號0,1,2,…,n-1,每數m次刪掉一個人
假設有函數f(n)表示n個人最終剩下人的編號
n個人刪掉1個人後可以看做n-1的狀態,不過有自己的編號。
n個人刪掉的第一個人的編號是(m-1)%n,那麼n個人時刪掉第一個人的後面那 個人(m-1+1)%n一定是n-1個人時候編號爲0的那個人,即n個人時的編號m%n(這個編號是對於n個人來考慮的),n-1個人時編號爲i的人就是n個人時(m+i)%n
所以f(n)=(m+f(n-1))%n
f(1)=0,因爲1個人時只有一個編號0因此可以將人數從2反推到n:
var lastRemaining = function (n, m) { let ans = 0; for (let i = 2; i <= n; i++) { ans = (ans + m) % i; } return ans; }