傳送點 題解|APCS 2019 年 10 月中高級 第 3 題


前置知識

這題的核心(怎麼把「按一次鍵」變成圖上的一條邊)下面會從頭講;要先會的是:

簡潔題意

位置 \(0\) 到 \(n - 1\) 排成一列,從 \(0\) 出發、目標是 \(P\)。一次操作:從目前位置 \(u\) 先走到 \(v = u - L\) 或 \(v = u + R\),\(v\) 必須在 \(0\) 到 \(n - 1\) 之間;踩到 \(v\) 會立刻被傳送到 \(S_v\),\(S_v\) 不在範圍內的話這個操作不能用。一次操作只傳送一次,不會連鎖。求最少幾次操作能到 \(P\),到不了輸出 \(-1\),\(P = 0\) 輸出 \(0\)(\(2 \le n \le 10^6\),\(1 \le L, R \le \lfloor n / 2 \rfloor\),\(|S_i| \le 10^8\),\(S_0 = 0\)、\(S_P = P\))。(APCS 逐筆給分。)

把每一次操作畫成一條邊

每個位置當成圖上的一個點。從 \(u\) 按一次鍵,能到的地方只有兩種:往左踩 \(u - L\) 然後落在 \(S_{u - L}\)、往右踩 \(u + R\) 然後落在 \(S_{u + R}\)——所以每個點最多兩條有向邊,終點是 \(S_v\) 而不是 \(v\);踩的位置或傳送的終點只要有一個出界,那條邊就不存在。範例的五個點與邊見圖 1。

圖 1:範例的圖——0 往右踩 2 傳到 4,4 往左踩 3 停在 3,所以 2 步;1、2 沒有任何邊指向它們

每條邊都代表「一次操作」,所以「最少幾次操作」就是「從 \(0\) 到 \(P\) 最少走幾條邊」。邊權都是 \(1\) 的最短路用 BFS:從 \(0\) 開始,先找出一步能到的所有點、再找兩步能到的……用佇列維持「先發現的先處理」,dist[u] 記第一次到達 \(u\) 時走了幾步。某個點第一次被發現時,它的步數一定是最少的——比它更少步能到的點,全部都在它前面被處理完了。

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

const int MAX_N = 1000000;
int S[MAX_N];                    // S[v] = 走到 v 會被傳送到哪裡
int dist[MAX_N];                 // dist[u] = 從 0 到 u 最少幾次操作;-1 = 還沒到過

int main() {
    int n, P, L, R;
    cin >> n >> P >> L >> R;
    for (int i = 0; i < n; i++) cin >> S[i];
    for (int i = 0; i < n; i++) dist[i] = -1;
    queue<int> q;
    dist[0] = 0;
    q.push(0);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int d = 0; d < 2; d++) {            // 兩種操作:往左 L 格、往右 R 格
            int v;
            if (d == 0) v = u - L;
            else v = u + R;
            if (v < 0 || v >= n) continue;       // 走出去了,這個操作不能用
            int to = S[v];                       // 踩到 v 立刻被傳送到 S[v]
            if (to < 0 || to >= n) continue;     // 傳送到界外,這個操作也不能用
            if (dist[to] != -1) continue;        // 已經到過:之前那次一定不會更多步
            dist[to] = dist[u] + 1;
            q.push(to);
        }
    }
    cout << dist[P] << endl;                     // 到不了就還是 -1,剛好就是題目要的
    return 0;
}

幾個細節:

  • 兩個範圍檢查缺一不可:先檢查踩的位置 \(v\),再檢查傳送的終點 \(S_v\)。\(S_v\) 可以是負的或超過 \(n - 1\),直接拿去當索引會讀到陣列外面。
  • 到不了的點 dist 停在 \(-1\),正好是題目要的輸出;\(P = 0\) 時 dist[0] = 0 也自動對。
  • 每個點最多進佇列一次、檢查兩條邊,\(O(n)\);\(n = 10^6\) 的輸入約 \(10\) MB,本站實測 \(0.42\) 秒。

測過再交:範例沒有「到不了」

範例兩步就到、每個操作都合法;沒蓋到的是:\(P = 0\)、\(S_v\) 出界的操作、完全到不了、「傳送只做一次」、第一次碰到不一定最短的情況,以及 \(n = 10^6\) 的長鏈。

輸入 正確輸出 這一筆在測什麼
2 0 1 1/0 1 0 起點就是目標
4 3 1 1/0 1 2 3 3 沒有傳送:一格一格往右走
4 3 2 2/0 1 2 3 -1 到不了:\(0 \to 2\) 之後往右出界、往左回到 \(0\)
4 3 1 1/0 -5 2 3 -1 \(S_1 = -5\) 出界:踩 \(1\) 這個操作不能用,不能拿 \(-5\) 當索引
4 3 1 1/0 2 1 3 2 傳送只做一次:\(0\) 踩 \(1\) 傳到 \(2\),就停在 \(2\)(\(S_2 = 1\) 不會再傳)
6 4 1 2/0 1 2 3 4 5 2 \(0 \to 2 \to 4\) 兩步;先往左探的走法會繞到 \(5\) 步
用程式造:\(n = 10^6\)、\(P = n - 1\)、\(L = R = 1\)、每個 \(S_i = i\) 999999 最長的一條鏈:驗速度和佇列

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

常犯錯誤
  • 傳送後繼續沿著 \(S\) 連鎖傳送:範例裡 \(S_2 = 4\)、\(S_4 = 1\)、\(S_1 = 2\) 繞成一圈,程式直接卡死;本站計分測資另掛 \(4\) 筆。
  • 忘了檢查 \(S_v\) 有沒有出界:範例過,本站計分測資掛 \(6\) 筆(拿負數當索引,程式當掉)。
  • 把 \(v\) 當成到達的點、忽略傳送:範例自己就會抓到——印 3,正確是 2。
  • dist 沒有先設成 \(-1\)(全域陣列預設是 \(0\)):分不出「到過」和「沒到過」——範例過,本站計分測資掛 \(4\) 筆;自測表第 \(1\) 列印 2。
  • 用遞迴(DFS)走、第一次碰到就記步數:先探的那條路不一定最短——自測表第 \(6\) 列印 5,正確是 2;\(n = 10^6\) 時遞迴也會疊到一百萬層。