# APCS實作題2019年10月:傳送點 > 第1版:2024年8月20日 > 第2版:2025年1月7日,加上用陣列儲存待走訪佇列的另一種寫法 > 作者:王一哲 > [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=f166) ## 題目 ### 問題描述 有 $n$ 個格子由左至右編號為 $0$ 到 $n−1$,皮卡丘的起始位置於編號 $0$ 的格子,想要走到編號 $P$ 的格子,每次他都可以往左走 $L$ 格或往右走 $R$ 格。 對於每個格子,都有一個對應的傳送點 $S$。在完成向左或向右的操作後,如果走到的格子為 $i$,皮卡丘將會瞬間被傳送到 $S(i)$,$S(i)$ 也有可能恰等於 $i$,即不傳送,此時 $i$ 被稱為停留點。其中起點和目標位置必為停留點,即 $S(0) = 0$、$S(P) = P$。 當傳送發生後,不能發生連續傳送的情況,也就是被傳送到 $S(i)$ 後,不會再次被傳送到 $S(S(i))$, 皮卡丘都必須在目前位置,再次選擇往左或往右走。需要注意的是,當走出表格外時,會被認定為出界,即不論是往左或往右或瞬間移動 $S(i)$,都不能夠走出表格外。 請問至少需要幾次操作,皮卡丘才能夠移動到 $P$;如果無解,則輸出 $-1$。 <br /> ### 輸入說明 第一行包含四個整數 $n, P, L, R$,代表格子有 $n$ 格,目標位置 $P$ 和每次可以往左走 $L$ 格或往右走 $R$ 格,其中 $2 \leq n \leq 10^6,~0 \leq P \leq n-1,~1 \leq L, R \leq n/2$。 第二行有 $n$ 個數字,由左至右,表示每個格子的傳送點 $S(i)$,即 $S(0), S(1), \dots, S(n−1),~ |S(i)| ≤ 10^8$。 <br /> ### 輸出格式 輸出跳躍到格子 $P$ 所需的最少操作數,若無法達成則輸出 $-1$。 <br /> ### 範例輸入 ``` 5 3 1 2 0 2 4 3 1 ``` ### 範例輸出 ``` 2 ``` <br /><br /> ## Python 程式碼 ### 寫法1,用串列儲存待走訪位置 費時最久約為 1 s,使用記憶體最多約為 113.2 MB,通過測試。 ```python= n, P, L, R = map(int, input().split()) # 讀取格數 n,終點 P,向左走格數 L,向右走格數 R S = list(map(int, input().split())) # 讀取傳送點 steps = [-1]*n # 走到此格的步數,預設為 -1,代表還沒有走過 steps[0] = 0 # 原點步數為 0 st = [0] # 待走訪的串列,資料為位置 head = 0 # 由 st 讀取資料的索引值 while head < len(st) and steps[P] == -1: # 當 head 還沒有出界而且還沒有走到終點時繼續執行 x = st[head]; head += 1 # 由 st 讀取資料,將 head 加 1 cur = steps[x] # 現在這格的步數 for d in (-L, R): # 向左走 L 格,向右走 R 格 xn = x + d # 試著走訪的位置 # 如果 xn 及 S[xn] 沒有出界,而且 S[xn] 沒有走訪過 if xn >= 0 and xn < n and S[xn] >= 0 and S[xn] < n and steps[S[xn]] == -1: steps[S[xn]] = cur + 1 # 最後停留的位置步數為 cur + 1 st.append(S[xn]) # 將 S[xn] 加入 st print(steps[P]) # 印出終點對應的步數 ``` <br /> 也可以寫成這樣。 ```python= n, P, L, R = map(int, input().split()) S = list(map(int, input().split())) que = [0] head = 0 step = [-1]*n step[0] = 0 while head < len(que) and que[head] != P: u = que[head]; head += 1 for d in (-L, R): v = u + d if v < 0 or v >= n: continue x = S[v] if x < 0 or x >= n or step[x] >= 0: continue step[x] = step[u] + 1 que.append(x) print(step[P]) ``` <br /><br /> ### 寫法2,用 collections 函式庫中的雙向佇列儲存待走訪位置 費時最久約為 0.9 s,使用記憶體最多約為 113 MB,通過測試。 ```python= from collections import deque n, P, L, R = map(int, input().split()) # 讀取格數 n,終點 P,向左走格數 L,向右走格數 R S = list(map(int, input().split())) # 讀取傳送點 steps = [-1]*n # 走到此格的步數,預設為 -1,代表還沒有走過 steps[0] = 0 # 原點步數為 0 st = deque([0]) # 待走訪的雙向佇列,資料為位置 while st and steps[P] == -1: # 當 st 還有資料而且還沒有走到終點時繼續執行 x = st.popleft() # 移除 st 最左側的資料並指定給 x cur = steps[x] # 現在這格的步數 for d in (-L, R): # 向左走 L 格,向右走 R 格 xn = x + d # 試著走訪的位置 # 如果 xn 及 S[xn] 沒有出界,而且 S[xn] 沒有走訪過 if xn >= 0 and xn < n and S[xn] >= 0 and S[xn] < n and steps[S[xn]] == -1: steps[S[xn]] = cur + 1 # 最後停留的位置步數為 cur + 1 st.append(S[xn]) # 將 S[xn] 加入 st print(steps[P]) # 印出終點對應的步數 ``` <br /><br /> ## C++ 程式碼 ### 寫法1,用陣列儲存待走訪位置 費時最久約為 0.1 s,使用記憶體最多約為 8.6 MB,通過測試。 ```cpp= #include <iostream> #include <vector> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); // 加速 int n, P, L, R; cin >> n >> P >> L >> R; // 讀取格數 n,終點 P,向左走格數 L,向右走格數 R vector<int> S (n); // 儲存傳送點的陣列 for(int i=0; i<n; i++) { // 執行 n 次 cin >> S[i]; // 讀取傳送點 if (S[i] < 0 || S[i] >= n) S[i] = n; // 傳送後會出界的位置,重設為 n,比較方便檢查是否出界 } vector<int> steps (n, -1); // 走到此格的步數,預設為 -1,代表還沒有走過 steps[0] = 0; // 原點步數為 0 vector<int> st = {0}; // 待走訪的串列,資料為位置 int head = 0; // 由 st 讀取資料的索引值 int d[2] = {-L, R}; // 向左、向右走的格數 while(head < (int)st.size() && steps[P] == -1) { // 當 head 還沒有出界而且還沒有走到終點時繼續執行 int x = st[head]; head++; // 由 st 讀取資料,將 head 加 1 int cur = steps[x]; // 現在這格的步數 for(int i=0; i<2; i++) { // 向左走 L 格,向右走 R 格 int xn = x + d[i]; // 試著走訪的位置 if (xn >= 0 && xn < n && S[xn] != n && steps[S[xn]] == -1) { // 如果 xn 及 S[xn] 沒有出界,而且 S[xn] 沒有走訪過 steps[S[xn]] = cur + 1; // 最後停留的位置步數為 cur + 1 st.push_back(S[xn]); // 將 S[xn] 加入 st } } } cout << steps[P] << "\n"; // 印出終點對應的步數 return 0; } ``` <br /> 也可以寫成這樣。 ```cpp= #include <iostream> #include <vector> using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); int n, P, L, R; cin >> n >> P >> L >> R; vector<int> S (n); for(int i=0; i<n; i++) cin >> S[i]; vector<int> step (n, -1), que = {0}, dx = {-L, R}; int head = 0; step[0] = 0; while(head < (int)que.size() && step[P] == -1) { int u = que[head]; head++; for(int d : dx) { int v = u + d; if (v < 0 || v >= n) continue; int x = S[v]; if (x < 0 || x >= n || step[x] != -1) continue; step[x] = step[u] + 1; que.push_back(x); } } cout << step[P] << "\n"; return 0; } ``` <br /><br /> ### 寫法2,用佇列儲存待走訪位置 費時最久約為 0.1 s,使用記憶體最多約為 7.1 MB,通過測試。 ```cpp= #include <iostream> #include <queue> #include <cstring> // for memset using namespace std; int main() { ios::sync_with_stdio(0); cin.tie(0); // 加速 int n, P, L, R; cin >> n >> P >> L >> R; // 讀取格數 n,終點 P,向左走格數 L,向右走格數 R int S[n]; // 儲存傳送點的陣列 for(int i=0; i<n; i++) { // 執行 n 次 cin >> S[i]; // 讀取傳送點 if (S[i] < 0 || S[i] >= n) S[i] = n; // 傳送後會出界的位置,重設為 n,比較方便檢查是否出界 } int steps[n]; // 走到此格的步數 memset(steps, -1, sizeof(steps)); // 預設為 -1,代表還沒有走過 steps[0] = 0; // 原點步數為 0 queue<int> st; st.push(0); // 待走訪的串列,資料為位置,先放入原點 0 int d[2] = {-L, R}; // 向左、向右走的格數 while(!st.empty() && steps[P] == -1) { // 當 st 還資料而且還沒有走到終點時繼續執行 int x = st.front(); st.pop(); // 由 st 最前面讀取資料,再移除最前面的資料 int cur = steps[x]; // 現在這格的步數 for(int i=0; i<2; i++) { // 向左走 L 格,向右走 R 格 int xn = x + d[i]; // 試著走訪的位置 if (xn >= 0 && xn < n && S[xn] != n && steps[S[xn]] == -1) { // 如果 xn 及 S[xn] 沒有出界,而且 S[xn] 沒有走訪過 steps[S[xn]] = cur + 1; // 最後停留的位置步數為 cur + 1 st.push(S[xn]); // 將 S[xn] 加入 st } } } cout << steps[P] << "\n"; // 印出終點對應的步數 return 0; } ``` <br /><br /> --- ###### tags:`APCS`、`Python`、`C++`