語法書 / AA 競程語法書 下冊 / 第十七單元 / 每輪減半的威力:約瑟夫問題 1

17.5 每輪減半的威力:約瑟夫問題 1

約瑟夫問題是有兩千年歷史的經典:n 位小朋友(編號 1n)圍成一圈,從編號 1 開始每隔一位移除一人(跳過一位、移除下一位),移除後從下一位繼續,直到所有人出局。本站題目〈約瑟夫問題 1〉n \le 2 \times 10^5)要你輸出完整的移除順序。

最直覺的模擬:把大家放進 vector,游標繞圈走,輪到誰就 erase 誰。正確——但 10.5 警告過:erase 一次要搬動後面整段元素,O(n);移除 n 個人就是 O(n^2)n = 2 \times 10^5 代進去約 10^{10} 量級——本站這題時限 0.5 秒,這個寫法實測要跑約 0.9 秒,穩穩 TLE。

換個角度看這個過程:一整輪繞下來,恰好每兩人移除一人。與其逐一 erase,不如整輪處理完再重建——掃過目前的隊伍,被跳過的收進新 vector、被移除的直接輸出,一輪只花「目前人數」的工:

  • 第一輪 n 人、第二輪約 n/2 人、第三輪約 n/4 人……
  • 總工作量 n + \dfrac{n}{2} + \dfrac{n}{4} + \cdots < 2n——想像一條長度 2 的線段:先走 1、再走 \dfrac{1}{2}、再走 \dfrac{1}{4}……永遠走不出去。每輪減半的總量,撐死是第一輪的兩倍

所以整體是 O(n)。唯一要小心的細節:一輪結束時「下一位要不要被移除」得帶到下一輪——用一個旗標記住它。

完整程式碼

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<int> alive(n);
    for (int i = 0; i < n; i++) alive[i] = i + 1;

    bool removeNext = false;               // 輪到的這位要不要被移除(跨輪保留)
    bool first = true;
    while (!alive.empty()) {
        vector<int> next;
        for (int i = 0; i < (int)alive.size(); i++) {
            if (removeNext) {
                if (!first) cout << ' ';
                cout << alive[i];          // 被移除:直接輸出
                first = false;
                removeNext = false;
            } else {
                next.push_back(alive[i]);  // 被跳過:留到下一輪
                removeNext = true;
            }
        }
        alive = next;
    }
    cout << '\n';
    return 0;
}

執行結果(輸入 7):

2 4 6 1 5 3 7

本站實測 n = 2 \times 10^5:逐一 erase 版約 0.9 秒(TLE),每輪重建版約 0.02 秒。同樣是「照規則模擬」,操作的成本選對了,形狀就換掉了

「每輪減半」這個節奏請先記在心裡——下一節它有個專屬的名字。