搭到終點 題解|APCS 2024 年 10 月高級 第 4 題


前置知識

這題是「計數 DP + 轉移是一段連續和」的經典,下面的講解直接把這些當成已知,沒讀過的先讀;同一個主題兩邊都有的,對照著讀會更快上手:

簡潔題意

你在位置 \(0\),要搭公車到位置 \(m\)。第 \(i\) 班車從 \(l_i\) 開到 \(r_i\):目前位置 \(x\) 只要滿足 \(l_i \le x < r_i\) 就能上車,上了就一定坐到 \(r_i\) 才能下。只能靠搭車往前。兩條搭乘序列只要某一次搭的公車編號不同就算不同(路線一樣也算不同)。求抵達 \(m\) 的搭乘序列數除以 \(p\) 的餘數(\(1 \le n \le 2 \times 10^5\),\(1 \le m \le 10^9\),\(1 \le p \le 10^9 + 9\),\(0 \le l_i < r_i \le m\))。

依正確通過的測試資料筆數給分,其中:

  • 子題組 1(\(20\) 分):\(n, m \le 100\)。
  • 子題組 2(\(40\) 分):\(m \le 10^5\)。
  • 子題組 3(\(40\) 分):無額外限制。

先拿下 20 分:每個位置都是一個狀態

搭完一班車,人一定停在它的終點;而且位置只會變大。所以照位置由小到大算:

\(dp[x]\) = 抵達位置 \(x\) 的搭乘序列數,\(dp[0] = 1\)(什麼都還沒搭)。

站在位置 \(x\) 時,每一班「可以上」的車(\(l_i \le x < r_i\))都把 \(dp[x]\) 種走法送到 \(r_i\):\(dp[r_i] \mathrel{+}= dp[x]\)。因為 \(r_i > x\),處理到 \(x\) 時 \(dp[x]\) 已經不會再變了。答案是 \(dp[m]\),每一步都對 \(p\) 取餘數。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    long long p;
    cin >> n >> m >> p;
    vector<int> l(n), r(n);
    for (int &v : l) cin >> v;
    for (int &v : r) cin >> v;

    vector<long long> dp(m + 1, 0);      // dp[x]:抵達位置 x 的序列數
    dp[0] = 1 % p;                       // p 可能是 1,連初始值都要取餘數
    for (int x = 0; x <= m; x++)
        for (int i = 0; i < n; i++)
            if (l[i] <= x && x < r[i])   // 在 x 可以上第 i 班車
                dp[r[i]] = (dp[r[i]] + dp[x]) % p;
    cout << dp[m] << '\n';
    return 0;
}

每個位置掃全部公車,\(O(nm) \le 10^4\)。範例 1 走一遍:\(dp[4] = 1\)、\(dp[6] = 1 + 2 = 3\)、\(dp[7] = dp[3] + dp[4] + dp[5] + dp[6] = 4\)、\(dp[9] = dp[5] + \cdots + dp[8] = 7\)(圖 1)。\(m\) 一大這份程式就太慢也開不下,但子題組 1 的 \(20\) 分穩穩到手。

圖 1:範例 1 的五班公車畫成數線上的弧線,位置 0、4、6、7、9 的 dp 值依序是 1、1、3、4、7

從 20 分到 60 分:換成「每班車收一段連續和」

反過來看每一班車:從 \(l\) 到 \(r\) 這班車能接在任何抵達 \(l, l+1, \ldots, r-1\) 的序列後面,所以它貢獻給 \(dp[r]\) 的是

\[dp[l] + dp[l+1] + \cdots + dp[r-1]\]

——一段連續和,用前綴和 \(pre[t] = dp[0] + \cdots + dp[t-1]\) 就是 \(pre[r] - pre[l]\)。問題只剩「算到這班車時,\(dp[l..r-1]\) 都確定了嗎」:把公車依終點 \(r\) 由小到大排序再處理,輪到終點 \(r\) 的車時,所有終點小於 \(r\) 的車都處理完了,位置 \(< r\) 的 \(dp\) 都已經是最終值;前綴和跟著一路往右補到 \(r\) 就好。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    long long p;
    cin >> n >> m >> p;
    vector<int> l(n), r(n);
    for (int &v : l) cin >> v;
    for (int &v : r) cin >> v;

    vector<int> order(n);                   // 公車編號依終點由小到大排
    for (int i = 0; i < n; i++) order[i] = i;
    sort(order.begin(), order.end(), [&](int a, int b) { return r[a] < r[b]; });

    vector<long long> dp(m + 1, 0), pre(m + 2, 0);   // pre[t] = dp[0] + ... + dp[t-1]
    dp[0] = 1 % p;
    int done = 0;                           // pre 已經算到 done(dp[0..done-1] 都確定了)
    for (int i : order) {
        while (done < r[i]) {               // 把前綴和補到終點前一格
            pre[done + 1] = (pre[done] + dp[done]) % p;
            done++;
        }
        dp[r[i]] = (dp[r[i]] + pre[r[i]] - pre[l[i]] + p) % p;   // 抵達 [l, r) 的序列都能接這班車
    }
    cout << dp[m] << '\n';
    return 0;
}

排序 \(O(n \log n)\),前綴和與每班車各 \(O(1)\),總共 \(O(n \log n + m)\)。相減後可能是負的,所以 + p 再取餘數;\(dp\) 和 \(pre\) 都小於 \(p\),兩個相加不會超過 long long。\(m\) 到 \(10^9\) 時陣列開不出來——這是子題組 3 要解決的事。

從 60 分到 100 分:只有公車端點的位置才重要

\(m\) 到 \(10^9\) 但公車只有 \(2 \times 10^5\) 班。\(dp[x]\) 只有在 \(x\) 是某班車的終點(或 \(0\))時才可能不是 \(0\),前綴和也只在 \(l_i\)、\(r_i\) 這些位置被查到——所以把 \(0\)、\(m\)、所有 \(l_i\)、\(r_i\) 收集起來排序去重,用「它是第幾個座標」當下標,上面的程式一行都不用改思路,只是查表前先用 lower_bound 把位置換成編號。

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    long long p;
    cin >> n >> m >> p;
    vector<int> l(n), r(n);
    for (int &v : l) cin >> v;
    for (int &v : r) cin >> v;

    vector<int> xs = {0, m};                // 所有會用到的位置
    for (int i = 0; i < n; i++) {
        xs.push_back(l[i]);
        xs.push_back(r[i]);
    }
    sort(xs.begin(), xs.end());
    xs.erase(unique(xs.begin(), xs.end()), xs.end());   // 去重:xs[t] 是第 t 個位置
    int c = xs.size();
    auto id = [&](int v) { return int(lower_bound(xs.begin(), xs.end(), v) - xs.begin()); };

    vector<int> order(n);
    for (int i = 0; i < n; i++) order[i] = i;
    sort(order.begin(), order.end(), [&](int a, int b) { return r[a] < r[b]; });

    vector<long long> dp(c, 0), pre(c + 1, 0);   // 下標改成「第幾個位置」
    dp[0] = 1 % p;                               // xs[0] 一定是 0
    int done = 0;
    for (int i : order) {
        int a = id(l[i]), b = id(r[i]);
        while (done < b) {
            pre[done + 1] = (pre[done] + dp[done]) % p;
            done++;
        }
        dp[b] = (dp[b] + pre[b] - pre[a] + p) % p;
    }
    cout << dp[c - 1] << '\n';                   // xs[c-1] 一定是 m
    return 0;
}

座標最多 \(2n + 2\) 個,排序、去重、每班車一次二分搜,總共 \(O(n \log n)\);\(n = 2 \times 10^5\) 本站實測不到 \(0.2\) 秒。

測過再交:範例裡沒有「剛好在端點」的局面

兩個範例的公車都是從 \(0\) 或中途上車、終點各不相同。沒蓋到的:剛好在某班車的起點上車、到了終點不能再上同一班、同路線的兩班車、\(p = 1\)、到不了、\(m\) 很大。

輸入 正確輸出 這一筆在測什麼
1 1 5 / 0 / 1 1 最小的輸入
2 5 100 / 0 3 / 3 5 1 第一班的終點 \(3\) 剛好是第二班的起點:\(l \le x\) 的等號要放行
2 3 100 / 0 1 / 3 3 1 到了位置 \(3\) 不能再上終點是 \(3\) 的第二班:\(x < r\) 不含等號
2 5 100 / 0 0 / 5 5 2 兩班車路線完全相同,編號不同就是兩種序列
1 5 1 / 0 / 5 0 \(p = 1\):任何數除以 \(1\) 餘 \(0\),連 \(dp[0]\) 都要取餘數
2 6 100 / 0 4 / 3 6 0 第一班到 \(3\)、第二班 \(4\) 才能上:到不了
1 1000000000 7 / 0 / 1000000000 1 \(m = 10^9\):陣列開不下,一定要離散化

七筆都親眼看過正確,這題就穩了。

常犯錯誤
  • 到了終點 \(r\) 還讓人上同一班車(寫成 \(x \le r\)):自測表第 3 列印 2;本站 \(22\) 筆測資掛 \(10\) 筆。
  • 剛好在起點 \(l\) 不讓上車(寫成 \(l < x\)):從 \(0\) 出發的第一班車就上不了,範例 1、2 都印 0;本站掛 \(12\) 筆。
  • 公車沒依終點排序就一班一班累加前綴和:兩個範例都剛好對,本站卻掛 \(7\) 筆——後面才讀到的車如果終點比較小,它貢獻的 \(dp\) 值在前綴和裡已經算不到了。
  • 最後才取餘數:序列數會像 \(2^{70}\) 那樣爆掉 long long;本站掛 \(3\) 筆。
  • 直接開 \(m\) 大小的陣列:\(m = 10^9\) 開不出來,子題組 3 全掛;\(O(nm)\) 的做法在 \(m = 10^5\) 也超過時限,本站掛 \(14\) 筆。