INFOR38th 學術 葉倚誠
在羅馬-猶太戰爭中,約瑟夫與他的 40 個戰友被羅馬軍隊包圍在一個洞穴中,他們寧死不屈,決定透過自殺來結束生命。
他們所有人圍成一個圓圈,由某個人開始依順時針報數,每數到第 3 個人,那個人就必須自殺,然後由下一個人重新報數,直到所有人死亡。
約瑟夫並不想死,他迅速計算出了兩個安全位置,並與一名好友站在那裡,並成為最後活下來的兩個人,向羅馬軍隊投降。
設有 n 個人圍成一圈,編號為 1 到 n。從編號 1 的人開始順時針報數,每數到第 m 個人,該人就會被淘汰出局,接著由下一個人重新從 1 開始報數。
如此循環,直到最後只剩下一人,請問最後活下來的人原本的編號是多少?
開一個動態陣列儲存所有人
每次計算出局者的索引值並將其移除
每次刪除的時間複雜度為 O(n)
總時間複雜度為 O(n)
#include <bits/stdc++.h>
using namespace std;
int josephusVector(int n, int m) {
vector<int> people;
for (int i = 1; i <= n; ++i) {
people.push_back(i);
}
int idx = 0;
while (people.size() > 1) {
idx = (idx + m - 1) % people.size();
people.erase(people.begin() + idx);
}
return people[0];
}
int main() {
int n = 5, m = 2;
int ans = josephusVector(n, m);
cout << ans << endl;
return 0;
}利用先進先出(FIFO)的特性,將圓圈拉成一條直線:
沒數到 m 的人:從隊頭取出,直接移到隊尾
數到 m 的人:從隊頭取出,直接淘汰(不放回隊尾)
#include <bits/stdc++.h>
using namespace std;
int josephusQueue(int n, int m) {
queue<int> q;
for (int i = 1; i <= n; ++i) {
q.push(i);
}
while (q.size() > 1) {
for (int i = 0; i < m - 1; ++i) {
q.push(q.front());
q.pop();
}
q.pop();
}
return q.front();
}
int main() {
int n = 5, m = 2;
int ans = josephusQueue(n, m);
cout << ans << endl;
return 0;
}我們用 n = 5, m = 3(5 個人,每數到 3 出局)來舉例:
步驟 1:原問題(n = 5)
此時有 5 個人,初始編號為 0, 1, 2, 3, 4
從 0 開始順時針數 3 下,2 號出局。
步驟 2:子問題(n = 4)
現在剩下 4 個人了,遊戲規定由出局者的下一個人重新開始報數,所以 3 號成為了新一輪的起點
這時候,我們可以看作是這 4 個人舉辦了一場全新的「4人約瑟夫遊戲」
為了算這場新遊戲,我們給這 4 個人一組全新編號
從新起點 3 號開始編為 0, 1, 2, 3
| 新編號 x' (n - 1 人遊戲) | 舊編號 x (n 人遊戲) |
| 0 | 3 |
| 1 | 4 |
| 2 | 0 |
| 3 | 1 |
| X | 2 |
已知 1 個人時: J(1, 3) = 0
推導 2 個人時: J(2, 3) = (J(1, 3) + 3) (mod 2) = (0 + 3) (mod 2) = 1
推導 3 個人時: J(3, 3) = (J(2, 3) + 3) (mod 3) = (1 + 3) (mod 3) = 1
推導 4 個人時: J(4, 3) = (J(3, 3) + 3) (mod 4) = (1 + 3) (mod 4) = 0
推導 5 個人時: J(5, 3) = (J(4, 3) + 3) (mod 5) = (0 + 3) (mod 5) = 3
#include <bits/stdc++.h>
using namespace std;
int josephusRecursive(int n, int m) {
if (n == 1) {
return 0;
}
return (josephusRecursive(n - 1, m) + m) % n;
}
int main() {
int n = 5, m = 2;
int ans = josephusRecursive(n, m) + 1;
cout << ans << endl;
return 0;
}
當總人數為 i 個人、每數到 m 淘汰一人時,最後倖存者在 0 索引值得編號
根據前面在遞迴中所推導出規律,人數為 i 人的狀態,可以由人數為 i-1 人的狀態推導出來:
$$dp[i] = (dp[i-1] + m) \pmod i$$
仔細觀察轉移方程式
$$dp[i] = (dp[i-1] + m) \pmod i$$
你會發現,我們在計算 dp[i] 的時候,只需要用到前一個狀態 dp[i-1] 的值,至於更早之前的 dp[i-2]、dp[i-3] 早就沒用了
我們只需要一個滾動變數來不斷更新答案即可
這樣可以把空間複雜度壓到極致的 O(1)
#include <bits/stdc++.h>
using namespace std;
int josephusDP(int n, int m) {
int ans = 0;
for (int i = 2; i <= n; ++i) {
ans = (ans + m) % i;
}
return ans + 1;
}
int main() {
int n = 5, m = 2;
int ans = josephusDP(n, m);
cout << ans << endl;
return 0;
}我們可以把人數 n 拆解成n = 2ᵏ + L
其中 2ᵏ 是不大於 n 的最大 2 的次方數,而 L 是剩下的餘數
此時,最後倖存者的 1 基準編號公式會精簡成:
$$J(n, 2) = 2L + 1$$
假設 n = 5,最大 2 的次方是 4(即 2²),所以 5 = 2² + 1, L = 1
答案 2(1) + 1 = 3 號
假設 n = 41,最大 2 的次方是 32(即 2⁵),所以 41 = 32 + 9,L = 9
答案 2(9) + 1 = 19 號
這個規律在二進位下代表:
把 n 的二進位最高位(最左邊的 1)移到最右邊,就是答案
以 n = 41 為例:
41 的二進位是:101001
把最左邊的 1 移到最右邊:010011
010011 的十進位就是 19
#include <bits/stdc++.h>
using namespace std;
int josephusBitwiseM2(int n) {
int highest_bit = 1 << (31 - __builtin_clz(n));
int L = n - highest_bit;
return (L << 1) | 1;
}
int main() {
int n = 5;
int ans = josephusBitwiseM2(n);
cout << ans << endl;
return 0;
}