語法書 / AA 競程語法書 下冊 / 第十單元 / vector 的陣列與巢狀 vector

10.3 vector 的陣列與巢狀 vector

vector 本身也是一種型態,所以它能當陣列的元素,甚至能當另一個 vector 的元素。這一節就用這兩種「裝 vector 的櫃子」,回頭解決開場售票口的難題。

vector 的陣列

「vector 的陣列」是競程的常用場景:題目給很多條數列、長度有上限,但單條長度沒有上限。二維陣列得替每條數列都按上限開好開滿,記憶體直接爆炸;vector 的陣列讓每條數列各自長大,總共大約只花實際元素的空間:

const int MAX_N = 100005;
vector<int> line[MAX_N];   // 10^5 + 5 條各自獨立、長度可各異的數列,每條一開始都是空的

宣告方式跟普通陣列一樣(全域+const 常數大小,上冊 6.1 的慣例)——寫死的是條數的上限,每條的長度則交給 vector 自己伸縮。每條 vector 自帶少量管理資訊、也常多預留一點空間備用,但跟「按最壞上限開滿」相比是天壤之別——開 10^5 條空 vector 只花一點固定成本,記憶體主要隨實際加入的元素成長。

先用互動圖解建立畫面感再往下讀(本節兩種寫法都在裡面,讀到巢狀 vector 時記得回來切分頁玩):

圖 10-2:vector 的陣列與巢狀 vector——外層一格=一整條

開場的售票口場景,正是本站題目〈排隊模擬 1〉n 個售票口(n \le 10^5),依序發生 Q 筆事件——1 x d 是「國家編號 d 的民眾加入 x 號窗口的隊尾」、2 x y 是「x 號窗口隊尾的人移動到 y 號窗口的隊尾」;所有事件結束後,由隊首到隊尾輸出每個窗口的名單。10.2 的工具已經到齊,我們把它完整解掉:

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

const int MAX_N = 100005;
vector<int> line[MAX_N];               // line[i]:i 號售票口的隊伍,隊首在前、隊尾在後

int main() {
    int n, q;
    cin >> n >> q;
    for (int i = 0; i < q; i++) {
        int type;
        cin >> type;
        if (type == 1) {
            int x, d;
            cin >> x >> d;
            line[x].push_back(d);              // 國家 d 的民眾加入 x 號窗口的隊尾
        } else {
            int x, y;
            cin >> x >> y;
            line[y].push_back(line[x].back()); // x 號隊尾的人排到 y 號窗口的隊尾……
            line[x].pop_back();                // ……並離開原本的隊伍
        }
    }
    for (int i = 1; i <= n; i++) {
        int len = line[i].size();              // size() 先存進 int(10.4 會解釋原因)
        cout << len;
        for (int j = 0; j < len; j++) {
            cout << ' ' << line[i][j];
        }
        cout << '\n';
    }
    return 0;
}

執行結果(用題目的範例輸入):

2 5 7
2 24 233

兩種事件各只花一到三行:「加入隊尾」就是 push_back;「跳隊」拆成三步——back() 看看 x 號隊尾是誰、push_back 把他排進 y 號隊尾、pop_back 讓他離開原隊伍。題目保證事件 2 發生時 x 號窗口一定有人,所以這裡的 back()pop_back() 不會踩到「空 vector 是 UB」的地雷;沒有這種保證的題目,動手前記得先 empty() 檢查。

巢狀 vector

容器裡也能裝容器:vector<vector<int>>——連「有幾條數列」都可以動態增減(注意角括號是一層包一層)。剛剛的解法只要把 line 的宣告搬進 main、改成巢狀版,其餘一行都不用動:

int n, q;
cin >> n >> q;
vector<vector<int>> line(n + 1);   // n + 1 條空數列:用編號 1 到 n,0 號放著不用

「(長度,初始值)」的長度可以是變數(10.2),所以巢狀版連 MAX_N 都不需要——題目給幾個窗口就開幾條。巢狀 vector 也能用 push_back 一次加入一整條數列:

vector<vector<int>> g;                           // 一開始 0 條數列
g.push_back({1, 2, 3});                          // 加入一整條數列
g.push_back({4});
cout << g[0][2] << ' ' << g[1].size() << '\n';   // 輸出 3 1

g[0][2] 的讀法跟二維陣列(上冊 6.9)一樣:第 0 條數列的索引 2 元素。但心裡的圖像要換掉:這不是一個方方正正的表格,而是兩步取值——g[0] 先從外層挑出第 0 條(它本身就是一個 vector),後面的 [2] 再取那條自己的索引 2。也因為每條各自伸縮,g[0] 長度 5g[1] 長度 0 完全合法,每一條的索引都從自己的 0 開始算。(回到上面的圖 10-2 切到「巢狀 vector」分頁,點任一個格子——訊息會把這兩步逐字拆給你看。)

兩種寫法在 Level 0 幾乎等價,選哪個都行:條數上限已知、想省事就用全域的 vector 陣列;條數執行時才知道,或之後想整個複製(10.6 會看到)就用巢狀 vector。

動手試試看:先解掉好多數列(簡單版)(vector 的陣列),再把〈排隊模擬 1〉自己從零重打一遍提交——能不看課本寫出來才算真的會。行有餘力,挑戰進階版〈排隊模擬 2〉:一次移動 k 個人、順序還可能反轉,工具完全一樣,考驗你對 push_backbackpop_back 的熟練度。