置物櫃分配 (APCS 2018-10 高級) 的題解
前置知識
這題是 0/1 背包換個說法,下面的講解直接把這些當成已知,沒讀過的先讀;同一個主題兩邊都有的,對照著讀會更快上手:
- 0/1 背包:NTUCPC Guide〈背包問題〉——「每件選或不選、容量不能超」的表格定義與轉移;〈滾動 DP〉說明一維陣列為什麼容量要從大到小更新。吳邦一《AP325》第 6 章 6.2.2 的 Q-6-10「置物櫃出租」正是本題(教材 PDF 第 183 頁,前面 P-6-9 的解說就是 0/1 背包;Python 版第 6 章(II))。
簡潔題意
\(M\) 個置物櫃已租給 \(N\) 位客人,第 \(i\) 位借了 \(x_i\) 個(\(\sum x_i \le M\))。新客人要 \(S\) 個空櫃,你可以取消某些客人的租借(一位客人要嘛全取消、要嘛全保留),求最少要取消幾個櫃子才能湊出至少 \(S\) 個空櫃;原本空櫃就夠就是 \(0\)(\(M \le 10^5\)、\(S \le M\)、\(N \le 100\);APCS 逐筆給分)。
反過來想:不是「取消最少」,是「保留最多」
取消的櫃子=全部借出的 − 保留的,全部借出的是定值,所以取消最少=保留最多。保留有什麼限制?新客人要 \(S\) 個,所以保留的總數不能超過 \(M - S\)(圖 1)。於是題目變成:
\(N\) 位客人,每位「留」或「不留」,留下的總櫃數不能超過 \(M - S\),問最多能留幾個。
這就是 0/1 背包:容量 \(M - S\),每位客人是一件「重量 \(x_i\)、價值也是 \(x_i\)」的物品。
填表:keep[c] = 容量 c 裡最多能留幾個櫃子
用一維陣列 keep[c] 記「容量不超過 \(c\) 時最多能留的櫃數」,一開始全是 \(0\)。一位一位客人加進來:對每個容量 \(c \ge x_i\),keep[c] = max(keep[c], keep[c - x_i] + x_i)——不留他就維持原值,留他就從「少 \(x_i\) 的容量」那格接上來。\(c\) 要從大到小跑:由小到大的話 keep[c - x_i] 已經是「加了這位客人之後」的值,同一位客人會被留兩次。
最後答案=全部借出的總數 \(- \text{keep}[M - S]\)。原本空櫃就夠(\(M - \sum x_i \ge S\))時,所有客人加起來都放得進容量,keep[M - S] 就是全部借出的總數,答案自然是 \(0\)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int m, s, n;
cin >> m >> s >> n;
vector<int> x(n);
int rented = 0; // 目前借出的總數
for (int i = 0; i < n; i++) {
cin >> x[i];
rented += x[i];
}
int capacity = m - s; // 既有客人最多只能留這麼多
vector<int> keep(capacity + 1, 0); // keep[c]:容量 c 裡最多能留幾個櫃子
for (int i = 0; i < n; i++)
for (int c = capacity; c >= x[i]; c--) // 從大到小:每位客人只能留一次
keep[c] = max(keep[c], keep[c - x[i]] + x[i]);
cout << rented - keep[capacity] << '\n';
return 0;
}
\(N \le 100\)、容量 \(\le 10^5\),最多 \(10^7\) 次更新。借 \(0\) 個的客人留不留都一樣,迴圈裡 c >= 0 全跑一遍也不會出錯。
測過再交:範例裡沒有空櫃、也沒有「剛好湊滿」
範例的 \(\sum x_i = 20 = M\),一個空櫃都沒有。沒蓋到的:原本空櫃就夠、剛好能留滿 \(M - S\)、只有一位客人、\(S = M\)(全部都要取消)。
| 輸入 | 正確輸出 | 這一筆在測什麼 |
|---|---|---|
10 3 2 / 4 2 |
0 |
原本空櫃 \(4 \ge S\):一個都不用取消 |
10 6 3 / 4 4 1 |
5 |
容量 \(4\) 剛好留一位借 \(4\) 的,取消 \(4 + 1\) |
5 5 1 / 5 |
5 |
\(S = M\):容量 \(0\),全部取消 |
8 3 3 / 2 2 2 |
2 |
容量 \(5\) 留兩位(\(4\)),取消一位 |
10 4 2 / 3 5 |
3 |
容量 \(6\) 留 \(5\) 比留 \(3\) 好;容量由小到大更新會把借 \(3\) 的留兩次、印 2 |
五筆都親眼看過正確,這題就穩了。
常犯錯誤
- 忘了原本就有的空櫃(以為要湊的是 \(S\) 而不是 \(S - \text{空櫃}\)):範例剛好沒有空櫃所以過,自測表第 1 列印
4。 - 容量由小到大更新:同一位客人被重複保留——範例印
14、自測表第 5 列印2。 - 貪心:從小的開始留、或從大的開始取消:範例剛好過(留 \(1, 2, 2\));自測表第 2 列從小的留會先留 \(1\)、剩下的容量放不下 \(4\),印
8。 - 陣列只開 \(M - S\) 個格子但用到
keep[capacity]:差一格就越界,開capacity + 1。