# APCS實作題2024年10月第3題:連鎖反應
> 日期:2024年10月31日
> 作者:王一哲
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=o713)
<br />
## 題目
### 問題描述
在一個 $M \times N$ 的網格中,每一格可能是石頭或是炸彈,具體包含以下數字:
- $-1$ 表示石頭,無法被炸彈引爆,炸彈震波無法通過該格傳遞。
- $-2$ 表示初始炸彈的起始點,可以設定爆炸半徑。
- 其他整數表示該位置存在爆炸半徑為該數值的炸彈。
炸彈爆炸時,會影響周圍一定範圍內的格子。炸彈會以該格為中心擴散,若一個炸彈的爆炸半徑為 $v$,則會將從該格開始沿著上、下、左或右走 $v$ 步內可以走到的格子都引爆。若某一顆炸彈引爆的範圍中涵蓋到尚未引爆的炸彈,則會引發鏈鎖反應將尚未引爆的炸彈引爆,並依循著相同的規則發生鏈鎖反應。
目標是找出初始炸彈所需設置的最小爆炸半徑,使得至少有 $q$ 個格子被引爆。
<br />
### 輸入格式
第一行有三個正整數 $M, N, q ~(2 \leq M, N \leq 500,~1 < q \leq M, N)$,接下來有 $M$ 行,每一行有 $N$ 個數字。保證炸彈半徑為正數的數量最多只有 $1500$ 個,且在場上的炸彈半徑不超過 $30$。
保證一定存在一個初始炸彈的爆炸半徑使得引爆的格子數量至少 $q$。
子題配分
- 20%:$1 \leq M, N \leq 100$,炸彈半徑為正的數量不超過 $100$ 且場上沒有任何石頭。
- 40%:$1 \leq M, N \leq 200$,炸彈半徑為正的數量不超過 $200$,場上的炸彈半徑不超過 $20$。
- 40%:無限制
<br />
### 輸出格式
輸出至少需要設置爆炸半徑多大的炸彈,才能使得至少有 $q$ 個格子被引爆。
<br />
### 範例輸入1
```
3 5 10
0 2 0 0 1
0 0 0 1 0
-2 0 0 0 0
```
### 範例輸出1
```
3
```
### 範例輸入2
```
4 6 10
0 0 -1 -1 -1 0
0 0 -1 1 -1 2
0 -1 0 -1 0 0
2 -2 0 0 0 -1
```
### 範例輸出2
```
4
```
<br />
## Python 程式碼
第6筆測資開始超時。
```python=
M, N, q = map(int, input().split()) # 地圖列數 M、欄數 N,目標格數 q
grid = [] # 地圖
r0, c0 = 0, 0 # 放入炸彈位置預設值
for i in range(M): # 讀取 M 行資料
g = list(map(int, input().split())) # 讀取一行資料
grid.append(g) # 將 g 加入 grid
if -2 in g: # 如果 g 之中有 -2,找到放入炸彈
r0 = i; c0 = g.index(-2) # 更新 (r0, c0)
def bfs(R0): # 代入放置炸彈的爆炸半徑 R0
if q == 1: return True # 例外狀況,q 等於 1,一定成立,直接回傳 True
que = [(r0, c0, R0)] # 要搜尋的佇列
head = 0 # 由 que 讀取資料的索引值
count = 1 # 放炸彈的位置一定會炸掉,至少有 1 格
visited = [[0]*N for _ in range(M)] # 走訪狀態,若已走訪,存入炸彈於此格剩下的爆炸格數加 30
visited[r0][c0] = R0 + 30 # 起點已走訪
while head < len(que): # 如果 head 還沒有出界繼續執行
r, c, R = que[head]; head += 1 # 由 que 讀取資料並更新 head
if R == 0: continue # 如果已經不會再向外延伸,找下一筆資料
for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)): # 四方向搜尋
nr, nc = r + dr, c + dc # 要檢驗的位置 (nr, nc)
if nr < 0 or nr >= M or nc < 0 or nc >= N or grid[nr][nc] == -1:
continue # 出界或遇到石頭,找下一個位置
nR = max(grid[nr][nc], R-1) # 下次檢測時剩下的爆炸格數,取新炸彈的爆炸半徑及 R-1 較大者
if nR > visited[nr][nc] - 30: # 這次檢測時爆炸範圍大於前一次檢測結果
if visited[nr][nc] == 0: count += 1 # 此格還沒有炸過,count 加 1
visited[nr][nc] = nR + 30 # 已走訪
que.append((nr, nc, nR)) # 更新 que
if count >= q: return True # 如果 count 大於等於 q,回傳 True
# end of while loop
return False # 已走訪完時已爆炸格數小於 q,回傳 False
low, high, ans = 0, 5000, 0 # 放置炸彈爆炸半徑最小值 0、最大值,答案先預設為 0
while high >= low: # 二分搜尋法
mid = (high - low) // 2 + low # 求中間值
if bfs(mid):
ans = mid; high = mid-1
else:
low = mid+1
# end of while loop
print(ans)
```
<br /><br />
改用 collections.deque 儲存要走訪的資料,第6筆測資開始超時。
```python=
from collections import deque
M, N, q = map(int, input().split()) # 地圖列數 M、欄數 N,目標格數 q
grid = [] # 地圖
r0, c0 = 0, 0 # 放入炸彈位置預設值
for i in range(M): # 讀取 M 行資料
g = list(map(int, input().split())) # 讀取一行資料
grid.append(g) # 將 g 加入 grid
if -2 in g: # 如果 g 之中有 -2,找到放入炸彈
r0 = i; c0 = g.index(-2) # 更新 (r0, c0)
def bfs(R0): # 代入放置炸彈的爆炸半徑 R0
if q == 1: return True # 例外狀況,q 等於 1,一定成立,直接回傳 True
que = deque([(r0, c0, R0)]) # 要搜尋的佇列
count = 1 # 放炸彈的位置一定會炸掉,至少有 1 格
visited = [[0]*N for _ in range(M)] # 走訪狀態,若已走訪,存入炸彈於此格剩下的爆炸格數加 30
visited[r0][c0] = R0 + 30 # 起點已走訪
while que: # 如果 que 還有資料繼續執行
r, c, R = que.popleft() # 由 q 開頭讀取並移除資料
if R == 0: continue # 如果已經不會再向外延伸,找下一筆資料
for dr, dc in ((0, 1), (1, 0), (0, -1), (-1, 0)): # 四方向搜尋
nr, nc = r + dr, c + dc # 要檢驗的位置 (nr, nc)
if nr < 0 or nr >= M or nc < 0 or nc >= N or grid[nr][nc] == -1:
continue # 出界或遇到石頭,找下一個位置
nR = max(grid[nr][nc], R-1) # 下次檢測時剩下的爆炸格數,取新炸彈的爆炸半徑及 R-1 較大者
if nR > visited[nr][nc] - 30: # 這次檢測時爆炸範圍大於前一次檢測結果
if visited[nr][nc] == 0: count += 1 # 此格還沒有炸過,count 加 1
visited[nr][nc] = nR + 30 # 已走訪
que.append((nr, nc, nR)) # 更新 que
if count >= q: return True # 如果 count 大於等於 q,回傳 True
# end of while loop
return False # 已走訪完時已爆炸格數小於 q,回傳 False
low, high, ans = 0, 5000, 0 # 放置炸彈爆炸半徑最小值 0、最大值,答案先預設為 0
while high >= low: # 二分搜尋法
mid = (high - low) // 2 + low # 求中間值
if bfs(mid):
ans = mid; high = mid-1
else:
low = mid+1
# end of while loop
print(ans)
```
<br /><br />
## C++ 程式碼
費時最久約 0.3 s,使用記憶體最多 113.4 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int M, N, q, r0, c0; // 地圖列數 M、欄數 N,目標格數 q,放入炸彈位置預設值
vector<vector<int>> grid (500, vector<int> (500)); // 地圖,在 main 之中 resize
int dr[4] = {0, 1, 0, -1}, dc[4] = {1, 0, -1, 0}; // 四方向搜尋用的陣列
bool bfs(int R0) { // 代入放置炸彈的爆炸半徑 R0
if (q == 1) return true; // 例外狀況,q 等於 1,一定成立,直接回傳 true
vector<vector<int>> que = {{r0, c0, R0}}; // 要搜尋的佇列
size_t head = 0; // 由 que 讀取資料的索引值
int count = 1; // 放炸彈的位置一定會炸掉,至少有 1 格
vector<vector<int>> visited (M, vector<int> (N, 0)); // 走訪狀態,若已走訪,存入炸彈於此格剩下的爆炸格數加 30
visited[r0][c0] = R0 + 30; // 起點已走訪
while(head < que.size()) { // 如果 head 還沒有出界繼續執行
int r = que[head][0], c = que[head][1], R = que[head][2]; head++; // 由 que 讀取資料並更新 head
if (R == 0) continue; // 如果已經不會再向外延伸,找下一筆資料
for(int i=0; i<4; i++) { // 四方向搜尋
int nr = r + dr[i], nc = c + dc[i]; // 要檢驗的位置 (nr, nc)
if (nr < 0 || nr >= M || nc < 0 || nc >= N || grid[nr][nc] == -1) continue; // 出界或遇到石頭,找下一個位置
int nR = max(grid[nr][nc], R-1); // 下次檢測時剩下的爆炸格數,取新炸彈的爆炸半徑及 R-1 較大者
if (nR > visited[nr][nc] - 30) { // 這次檢測時爆炸範圍大於前一次檢測結果
if (visited[nr][nc] == 0) count++; // 此格還沒有炸過,count 加 1
visited[nr][nc] = nR + 30; // 已走訪
que.push_back({nr, nc, nR}); // 更新 que
}
}
if (count >= q) return true; // 如果 count 大於等於 q,回傳 true
}
return false; // 已走訪完時已爆炸格數小於 q,回傳 false
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
cin >> M >> N >> q; // 讀取 M, N, q
grid.resize(M); // 更新 grid 的維度為 M*N
for(int i=0; i<M; i++) grid[i].resize(N);
for(int i=0; i<M; i++) { // 讀取 M 行資料
for(int j=0; j<N; j++) {
int g; cin >> g; // 讀取 N 個數字
grid[i][j] = g; // 更新 grid
if (g == -2) { // 找到放入炸彈位置
r0 = i; c0 = j; // 更新 (r0, c0)
}
}
}
int low = 0, high = M*N, ans = 0; // 放置炸彈爆炸半徑最小值 0、最大值,答案先預設為 0
while(high >= low) { // 二分搜尋法
int mid = (high - low) / 2 + low; // 求中間值
if (bfs(mid)) {
ans = mid; high = mid-1;
} else {
low = mid+1;
}
}
cout << ans << "\n";
return 0;
}
```
<br /><br />
改用 queue 儲存要走訪的資料,費時最久約 0.2 s,使用記憶體最多 16.7 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
int M, N, q, r0, c0; // 地圖列數 M、欄數 N,目標格數 q,放入炸彈位置預設值
vector<vector<int>> grid (500, vector<int> (500)); // 地圖,在 main 之中 resize
int dr[4] = {0, 1, 0, -1}, dc[4] = {1, 0, -1, 0}; // 四方向搜尋用的陣列
bool bfs(int R0) { // 代入放置炸彈的爆炸半徑 R0
if (q == 1) return true; // 例外狀況,q 等於 1,一定成立,直接回傳 true
queue<vector<int>> que; que.push({r0, c0, R0}); // 要搜尋的佇列
int count = 1; // 放炸彈的位置一定會炸掉,至少有 1 格
vector<vector<int>> visited (M, vector<int> (N, 0)); // 走訪狀態,若已走訪,存入炸彈於此格剩下的爆炸格數加 30
visited[r0][c0] = R0 + 30; // 起點已走訪
while(!que.empty()) { // 如果 que 還有資料繼續執行
int r = que.front()[0], c = que.front()[1], R = que.front()[2]; que.pop(); // 由 que 開頭讀取並移除資料
if (R == 0) continue; // 如果已經不會再向外延伸,找下一筆資料
for(int i=0; i<4; i++) { // 四方向搜尋
int nr = r + dr[i], nc = c + dc[i]; // 要檢驗的位置 (nr, nc)
if (nr < 0 || nr >= M || nc < 0 || nc >= N || grid[nr][nc] == -1) continue; // 出界或遇到石頭,找下一個位置
int nR = max(grid[nr][nc], R-1); // 下次檢測時剩下的爆炸格數,取新炸彈的爆炸半徑及 R-1 較大者
if (nR > visited[nr][nc] - 30) { // 這次檢測時爆炸範圍大於前一次檢測結果
if (visited[nr][nc] == 0) count++; // 此格還沒有炸過,count 加 1
visited[nr][nc] = nR + 30; // 已走訪
que.push({nr, nc, nR}); // 更新 que
}
}
if (count >= q) return true; // 如果 count 大於等於 q,回傳 true
}
return false; // 已走訪完時已爆炸格數小於 q,回傳 false
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
cin >> M >> N >> q; // 讀取 M, N, q
grid.resize(M); // 更新 grid 的維度為 M*N
for(int i=0; i<M; i++) grid[i].resize(N);
for(int i=0; i<M; i++) { // 讀取 M 行資料
for(int j=0; j<N; j++) {
int g; cin >> g; // 讀取 N 個數字
grid[i][j] = g; // 更新 grid
if (g == -2) { // 找到放入炸彈位置
r0 = i; c0 = j; // 更新 (r0, c0)
}
}
}
int low = 0, high = M*N, ans = 0; // 放置炸彈爆炸半徑最小值 0、最大值,答案先預設為 0
while(high >= low) { // 二分搜尋法
int mid = (high - low) / 2 + low; // 求中間值
if (bfs(mid)) {
ans = mid; high = mid-1;
} else {
low = mid+1;
}
}
cout << ans << "\n";
return 0;
}
```
<br /><br />
---
###### tags:`APCS`、`Python`、`C++`