# APCS實作題2020年1月第3題:砍樹
> 日期:2024年7月10日
> 作者:王一哲
> 題目來源:2020年1月實作題第3題
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=h028)
## 題目
### 問題描述
有一個長度為 $L$ 的長條型的林場,$N$ 棵樹種在一排,現階段砍樹必須符合以下的條件:「讓它向左或向右倒下,倒下時不會超過林場的左右範圍之外,也不會壓到其它尚未砍除的樹木。」你的工作就是計算能砍除的樹木。
若 $c_i$ 代表第 $i$ 棵樹的位置座標,$h_i$ 代表高度。向左倒下壓到的範圍為 $[c_i - h_i, c_i]$,而向右倒下壓到的範圍為 $[c_i, c_i + h_i]$。如果倒下的範圍內有其它尚未砍除的樹就稱為壓到,剛好在端點不算壓到。
我們可以不斷找到滿足砍除條件的樹木,將它砍倒後移除,然後再去找下一棵可以砍除的樹木,直到沒有樹木可以砍為止。無論砍樹的順序為何,最後能砍除的樹木是相同的。
<br />
### 輸入說明
第一行為正整數 $N$ 以及一個正整數 $L$,代表樹的數量與林場右邊界的座標。$N \leq 10^5$,$L \leq 10^9$。
第二行有 $N$ 個正整數依序代表這 $N$ 棵樹的座標,座標是從小到大排序的,所有座標不超過 $L$。
第三行有 $N$ 個正整數依序代表樹的高度,所有樹高都不超過 $10^9$。
本題包含三個子題組,每個子題組配分如下:
1. 第1子題組共20分:$N \leq 10$
2. 第2子題組共40分:$N \leq 1000$
3. 第3子題組共40分:無額外限制。
<br />
### 輸出格式
輸出共有兩行,第一行輸出能被砍除之樹木數量,第二行輸出能被砍除之樹木中最高的高度,如果無法砍除任何一棵樹,則最高高度輸出 0。
<br />
### 範例輸入1
```
6 140
10 30 50 70 100 125
30 15 55 10 55 25
```
### 範例輸出1
```
4
30
```
### 範例輸入2
```
1 10
5
6
```
### 範例輸出2
```
0
0
```
<br />
## Python 程式碼
### 寫法1,使用 pop 移除資料
程式邏輯無誤,但是效率不好,主要問題在於第16、17行的 pop,第12筆測資超時。
```python=
N, L = map(int, input().split()) # 讀數數量N、長度L
ps = list(map(int, input().split())) # 讀取樹的位置
hs = list(map(int, input().split())) # 讀取樹的高度
ps = [0] + ps + [L] # 在位置兩端分別加上0及L,避免在迴圈檢查時出界
hs = [0] + hs + [0] # 在位置兩端加上0,避免在迴圈檢查時出界
flag = True # 是否有砍樹
imax = 0 # 最大高度
count = 0 # 砍樹數量
while flag: # 如果有砍樹,繼續執行
flag = False # flag 重設為 False
i = 1 # 檢查樹的索引值從1開始
while i < len(ps)-1: # 如果i小於 ps 長度減1
if ps[i] - ps[i-1] >= hs[i] or ps[i+1] - ps[i] >= hs[i]: # 如果不會壓到左側或右側的樹
flag = True # 有砍樹
ps.pop(i) # 移除這棵樹的位置 ps[i]
imax = max(imax, hs.pop(i)) # 更新 imax 並移除 hs[i]
count += 1 # 數量加1
continue # 繼續執行下一輪內層的 while 迴圈
i += 1 # 如果上面的 if 不成立,i加1,檢查下一棵樹
""" end while loop """
print(count) # 印出數量
print(imax) # 印出最大高度
```
<br /><br />
### 寫法2,記錄左側、右側樹的索引值,不移除資料
費時最久約為 0.4 s,使用記憶體最多約為 35 MB,通過測試。
```python=
N, L = map(int, input().split()) # 讀數數量N、長度L
ps = list(map(int, input().split())) # 讀取樹的位置
hs = list(map(int, input().split())) # 讀取樹的高度
ps = [0] + ps + [L] # 在位置兩端分別加上0及L,避免在迴圈檢查時出界
hs = [0] + hs + [0] # 在位置兩端加上0,避免在迴圈檢查時出界
left = list(range(-1, N+1)) # 第i棵樹對應的左側樹索引值
right = list(range(1, N+3)) # 第i棵樹對應的右側樹索引值
imax = 0 # 最大高度
count = 0 # 砍樹數量
i = 1 # 檢查樹的索引值從1開始
""" 主要的解題過程 """
while True:
if ps[i] - ps[left[i]] >= hs[i] or ps[right[i]] - ps[i] >= hs[i]: # 如果不會壓到左側或右側的樹
imax = max(imax, hs[i]) # 更新 imax
count += 1 # 數量加1
left[right[i]] = left[i] # 取出第i棵樹右側索引值 right[i],將它對應的左側索引值更新為 left[i]
right[left[i]] = right[i] # 取出第i棵樹右側索引值 left[i],將它對應的右側索引值更新為 right[i]
if left[i] != 0: i = left[i] # 如果有左側的樹,i 更新為 left[i]
else: i = right[i] # 如果沒有左側的樹,i 更新為 right[i]
else: i = right[i] # 如果沒有砍樹,找右側下一棵樹,i 更新為 right[i]
if i > N: break # 如果 i > N,出界,中止 while 迴圈
""" end while loop """
print(count) # 印出數量
print(imax) # 印出最大高度
```
<br /><br />
## C++ 程式碼
### 寫法1,使用 erase 移除資料
程式邏輯無誤,但是效率不好,主要問題在於第23、24行的 erase,第12筆測資超時。
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int N, L; cin >> N >> L; // 讀數數量N、長度L
vector<int> ps (N+2, 0), hs (N+2, 0); // 樹的位置 ps、高度 hs
ps[N+1] = L; // 在位置右端加上L,避免在迴圈檢查時出界
for(int i=1; i<=N; i++) cin >> ps[i]; // 讀取 ps
for(int i=1; i<=N; i++) cin >> hs[i]; // 讀取 hs
bool flag = true; // 是否有砍樹
int imax = 0, count = 0; // 最大高度 imax,砍樹數量 count
while(flag) { // 如果有砍樹,繼續執行
flag = false; // flag 重設為 False
int i = 1; // 檢查樹的索引值從1開始
while(i < (int)ps.size()-1) { // 如果i小於 ps 長度減1
if (ps[i] - ps[i-1] >= hs[i] || ps[i+1] - ps[i] >= hs[i]) { // 如果不會壓到左側或右側的樹
flag = true; // 有砍樹
imax = max(imax, hs[i]); // 更新 imax
count++; // 數量加1
ps.erase(ps.begin()+i); // 移除 ps[i]
hs.erase(hs.begin()+i); // 移除 hs[i]
continue; // 繼續執行下一輪內層的 while 迴圈
}
i++; // 如果上面的 if 不成立,i加1,檢查下一棵樹
}
}
cout << count << "\n" << imax << "\n"; // 印出答案
return 0;
}
```
<br /><br />
### 寫法2,記錄左側、右側樹的索引值,不移除資料
費時最久約為 32 s,使用記憶體最多約為 1.9 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int N, L; cin >> N >> L; // 讀數數量N、長度L
vector<int> ps (N+2, 0), hs (N+2, 0), left (N+2, 0), right (N+2, 0); // 樹的位置 ps、高度 hs、左側樹 left、右側樹 right
ps[N+1] = L; // 在位置右端加上L,避免在迴圈檢查時出界
for(int i=1; i<=N; i++) cin >> ps[i]; // 讀取 ps
for(int i=1; i<=N; i++) cin >> hs[i]; // 讀取 hs
for(int i=1; i<=N+1; i++) {
left[i] = i-1; // 0, 1, 2, ..., N
right[i] = i+1; // 1, 2, 3, ..., N+2
}
int imax = 0, count = 0, i = 1; // 最大高度 imax,砍樹數量 count,正在檢查的樹索引值 i 從 1 開始
/* 主要的解題過程 */
while(true) {
if (ps[i] - ps[left[i]] >= hs[i] || ps[right[i]] - ps[i] >= hs[i]) { // 如果不會壓到左側或右側的樹
imax = max(imax, hs[i]); // 更新 imax
count++; // 數量加1
right[left[i]] = right[i]; // 取出 left[i],將它對應的右側樹位置設定為 right[i]
left[right[i]] = left[i]; // 取出 right[i],將它對應的左側樹位置設定為 left[i]
if (left[i] != 0) i = left[i]; // 如果有左側樹,i 設定為 left[i]
else i = right[i]; // 如果沒有左側樹,i 設定為 right[i]
} else { // 如果沒有砍樹,找右側樹
i = right[i]; // i 設定為 right[i]
}
if (i > N) break; // 如果 i > N,出界,中止 while 迴圈
}
cout << count << "\n" << imax << "\n"; // 印出答案
return 0;
}
```
<br /><br />
---
###### tags:`APCS`、`Python`、`C++`