# APCS實作題2025年6月第3題:貪心闖關
> 日期:2025年6月26日
> 作者:王一哲
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=q838)
<br />
## 題目
### 問題描述
小花參加一個搬沙包挑戰賽,關卡有 $n$ 個,編號由 $1$ ~ $n$,初始時,每個關卡有若干個沙包。
選手會選擇一個仍有沙包的關卡,將其所有的沙包搬至編號順序下一個仍有沙包的關卡,若選擇的關卡為當前仍有沙包的關卡中編號最大的,則把沙包搬出關卡即可。
沙包被搬完的關卡標示為完成,意即不會再有沙包被搬進此關卡,挑戰會持續進行直到所有關卡完成,或小花選手無法一次搬動當下任何關卡的所有沙包,小花一次可搬動的沙包上限為 $t$ 個。
每成功搬動 $w$ 個沙包則得 $w$ 分,小花打算採用以下貪心的策略完成挑戰,請計算小花的總分:
- 選擇當前關卡中沙包最少的關卡,若有超過一個關卡有當前最少的沙包,則選擇編號最小的關卡
- 重複以上步驟直到挑戰結束 以測資一為範例:
- 選擇關卡4,搬1個沙包到關卡5,得1分,關卡狀態更新為 (4 4 2 0 10 3)
- 選擇關卡3,搬2個沙包到關卡5,得2分,關卡狀態更新為 (4 4 0 0 12 3)
- 選擇關卡6,已經是當前仍有沙包的關卡中編號最大的,所以將沙包移出關卡就好,得3分,關卡狀態更新為 (4 4 0 0 12 0)
- 選擇關卡1,搬4個沙包到關卡2,得4分,關卡狀態更新為 (0 8 0 0 12 0)
- 選擇關卡2,搬8個沙包到關卡5,得8分,關卡狀態更新為 (0 0 0 0 20 0)
- 所有關卡中沙包最少的為20個,超過小花一次能搬動的上限8個,所以挑戰結束,總分為 1+2+3+4+8=18
<br />
### 輸入格式
第一行有兩個正整數 $n, t ~(1 \leq n \leq 3 \times 10^5, ~ 1 \leq t \leq 10^9)$,第二行有 $n$ 個正整數代表關卡初始的沙包數量,關卡初始沙包數不超過 $10^9$。
子題配分
- 20%:$1 \leq n \leq 100, ~ 1 \leq t \leq 1000$
- 80%:無額外限制
<br />
### 輸出格式
輸出一個正整數代表小花的總分,保證總分不超過 $10^{15}$。
<br />
### 範例一:輸入
```
6 8
4 4 2 1 9 3
```
### 範例一:正確輸出
```
18
```
### 範例二:輸入
```
5 4
4 4 3 2 1
```
### 範例一:正確輸出
```
10
```
<br /><br />
## Python 程式碼
### 寫法1,線性搜尋下一個有沙包的關卡編號
使用時間約為 2.6 s,記憶體約為 76.1 MB,通過測試。
```python=
import heapq
n, t = map(int, input().split()) # n 個關卡,最多一次搬 t 個沙包
stages = list(map(int, input().split())) # 每個關卡的初始沙包數量
pq = [(bag, idx) for idx, bag in enumerate(stages) if bag > 0] # (沙包數量, 關卡編號)
heapq.heapify(pq) # 轉成最小優先佇列
score = 0 # 分數
while pq: # 如果 pq 有資料繼續執行
# 如果 pq 有資料,但是 stages[idx] 與 pq 最上方儲存的數量不相同,此關已變動,移除此項
while pq and stages[pq[0][1]] != pq[0][0]: heapq.heappop(pq)
# 如果 pq 沒有資料,終止迴圈
if not pq: break
# 從 pq 最上方取出沙包數量、關卡編號
bag, idx = heapq.heappop(pq)
# 無法一次搬完,中止迴圈
if bag > t: break
# 線性搜尋,找右邊第一個有沙包的關卡,這個步驟最慢
nxt = -1
for i in range(idx+1, n):
if stages[i] > 0:
nxt = i
break
stages[idx] = 0 # 原關卡歸零
if nxt != -1: # 有找到下一關
stages[nxt] += bag # 更新數量
heapq.heappush(pq, (stages[nxt], nxt)) # 推入新數量
score += bag # 更新分數
### 結束 while 迴圈 ###
print(score) # 印出分數
```
<br />
### 寫法2,用 SortedList 儲存有沙包的關卡編號,用 bisect 查詢指定的編號
理論上可行,但是 ZeroJudge 網站不支援 SortedList,因為 sortedcontainers 不是標準的函式庫。
```python=
import heapq, bisect
from sortedcontainers import SortedList
n, t = map(int, input().split()) # n 個關卡,最多一次搬 t 個沙包
stages = list(map(int, input().split())) # 每個關卡的初始沙包數量
pq, pos = [], SortedList() # (沙包數量, 關卡編號),有沙包的位置
for idx, bag in enumerate(stages):
if bag > 0:
pq.append((bag, idx)) # (沙包數量, 關卡編號)
pos.add(idx) # idx 加入 pos
heapq.heapify(pq) # 轉成最小優先佇列
score = 0 # 分數
while pq: # 如果 pq 有資料繼續執行
# 如果 pq 有資料,但是 stages[idx] 與 pq 最上方儲存的數量不相同,此關已變動,移除此項
while pq and stages[pq[0][1]] != pq[0][0]: heapq.heappop(pq)
if not pq: break # 如果 pq 沒有資料,終止迴圈
bag, idx = heapq.heappop(pq) # 從 pq 最上方取出沙包數量、關卡編號
if bag > t: break # 無法一次搬完,中止迴圈
stages[idx] = 0 # 此關數量歸零
score += bag # 更新分數
curr = bisect.bisect_left(pos, idx) # 從 pos 之中找 idx 的索引值
if curr < len(pos)-1: # 如果不是最後一項
nxt = pos[curr+1] # 下一個有沙包的關卡編號
stages[nxt] += bag # 更新數量
heapq.heappush(pq, (stages[nxt], nxt)) # 推入新數量
pos.discard(idx) # 從 pos 之中移除 idx
### 結束 while 迴圈 ###
print(score) # 印出分數
```
<br /><br />
## C++ 程式碼
### 寫法1,線性搜尋下一個有沙包的關卡編號
使用時間約為 0.1 s,記憶體約為 15.2 MB,通過測試。
```cpp=
#include <cstdio>
#include <queue>
#include <utility>
#include <functional>
#include <vector>
typedef long long LL;
using namespace std;
int main() {
int n, t; scanf("%d %d", &n, &t); // n 個關卡,最多一次搬 t 個沙包
vector<LL> stages (n); // 每個關卡的初始沙包數量
priority_queue<pair<LL, int>, vector<pair<LL, int>>, greater<pair<LL, int>>> pq; // (沙包數量, 關卡編號)
for(int i=0; i<n; i++) { // 讀取 n 個關卡一開始的沙包數量
LL bag; scanf("%lld", &bag);
stages[i] = bag;
if (bag > 0) pq.push(make_pair(bag, i));
}
LL score = 0; // 分數
while(!pq.empty()) { // 如果 pq 有資料繼續執行
// 如果 pq 有資料,但是 stages[idx] 與 pq 最上方儲存的數量不相同,此關已變動,移除此項
while(!pq.empty() && stages[pq.top().second] != pq.top().first) pq.pop();
if (pq.empty()) break; // 如果 pq 沒有資料,終止迴圈
// 從 pq 最上方取出沙包數量、關卡編號
LL bag = pq.top().first; int idx = pq.top().second; pq.pop();
if (bag > t) break; // 無法一次搬完,中止迴圈
stages[idx] = 0; // 原關卡歸零
score += bag; // 更新分數
// 線性搜尋找右邊第一個有沙包的關卡
int nxt = -1;
for(int i=idx+1; i<n; i++) {
if (stages[i] > 0) {
nxt = i;
break;
}
}
if (nxt != -1) { // 有找到下一關
stages[nxt] += bag; // 更新數量
pq.push(make_pair(stages[nxt], nxt)); // 推入新數量
}
}
printf("%lld\n", score); // 印出分數
return 0;
}
```
<br />
### 寫法2,用 set 儲存有沙包的關卡編號,用 lower_bound 查詢指定的編號
速度反而比線性搜尋還慢,使用時間約為 0.4 s,記憶體約為 28.8 MB,通過測試。
```cpp=
#include <cstdio>
#include <queue>
#include <utility>
#include <functional>
#include <vector>
#include <set>
typedef long long LL;
using namespace std;
int main() {
int n, t; scanf("%d %d", &n, &t); // n 個關卡,最多一次搬 t 個沙包
vector<LL> stages (n); // 每個關卡的初始沙包數量
priority_queue<pair<LL, int>, vector<pair<LL, int>>, greater<pair<LL, int>>> pq; // (沙包數量, 關卡編號)
set<int> pos; // 有沙包的關卡
for(int i=0; i<n; i++) { // 讀取 n 個關卡一開始的沙包數量
LL bag; scanf("%lld", &bag);
stages[i] = bag;
if (bag > 0) {
pq.push(make_pair(bag, i));
pos.insert(i);
}
}
LL score = 0; // 分數
while(!pq.empty()) { // 如果 pq 有資料繼續執行
// 如果 pq 有資料,但是 stages[idx] 與 pq 最上方儲存的數量不相同,此關已變動,移除此項
while(!pq.empty() && stages[pq.top().second] != pq.top().first) pq.pop();
if (pq.empty()) break; // 如果 pq 沒有資料,終止迴圈
// 從 pq 最上方取出沙包數量、關卡編號
LL bag = pq.top().first; int idx = pq.top().second; pq.pop();
if (bag > t) break; // 無法一次搬完,中止迴圈
stages[idx] = 0; // 原關卡歸零
score += bag; // 更新分數
pos.erase(idx); // 移除 idx
// 用 set 的二分搜尋法找右邊第一個有沙包的關卡
auto it = pos.lower_bound(idx);
if (it != pos.end()) { // 如果不是最後一項
int nxt = *it; // 下一個有沙包的關卡編號
stages[nxt] += bag; // 更新數量
pq.push(make_pair(stages[nxt], nxt)); // 推入新數量
}
}
printf("%lld\n", score); // 印出分數
return 0;
}
```
<br /><br />
---
###### tags:`APCS`、`C++`、`Python`