# APCS實作題2018年:置物櫃分配
> 第1版:2023年9月9日
> 第2版:2024年12月21日
> 作者:王一哲
> 題目來源:107年6月實作題
> [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=e465)
<br />
## 題目
### 問題描述
你是個櫃子租借商,總共擁有 $M$ 個櫃子。現在這 $M$ 個櫃子分別被 $N$ 個人借用,借用量分別為 $(x_0, x_1, x_2, \dots, x_{N-1})$ 個櫃子,其中 $x_0 + x_1 + x_2 + \dots + x_{N-1} \leq M$。
現在你想要使用 $S$ 個空櫃子,在被借用的櫃子只能夠**全退**或**全不退**之下,最少需要請這 N 個人退還多少櫃子?也就是當有一個人借用10個櫃子時,不能夠只請他退還5個櫃子。
舉例來說,對於 $M = 20$ 個櫃子,現在分別被5個人借用 (1, 7, 2, 8, 2) 個櫃子,在需要使用 $S = 14$ 個櫃子之下,最少需要請他們退還 $7 + 8 = 15$ 個櫃子。
<br />
### 輸入格式
第一行有三個正整數 $M$、$S$、$N$,分別代表櫃子總數 $M$、想要使用的櫃子數 $S$、借用人數 $N$。
$$
M \leq 100000, ~~~~~ S \leq M, ~~~~~ N \leq 100000
$$
第二行有 $N$ 個非負整數 $x_0, x_1, x_2, \dots, x_{N-1}$,代表每個人所借用的櫃子數量。
其中 $x_0 + x_1 + x_2 + \dots + x_{N-1} \leq M$。
<br />
### 輸出格式
最少需要請這 $N$ 個人退還的櫃子總數
<br />
### 範例:輸入
```
20 14 5
1 7 2 8 2
```
### 範例:正確輸出
```
15
```
<br />
## Python 程式碼
### 寫法1,使用 bitset
以下的程式碼主要參考[這個網頁](https://david-chien.github.io/zj/zj_e465_1.html),使用 bitset 求指定資料可能的總合主要參考 LeetCode 416. Partition Equal Subset Sum 討論區 [C++ Bitset Solution](https://leetcode.com/problems/partition-equal-subset-sum/solutions/950597/c-bitset-solution/)。其中最難理解的部分是第3、4行,以 ZeroJudge 網頁的範例來說明,由於已借出的格子數為 $1, 7, 2, 8, 2$,可能的總合有 $1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20$,如果 possSum 先設定為1,先將 possSum 向左移1位再與原本的值取 or 運算,其2進位值為
```python
0b11
```
這代表 possSum 可能為 1。接下來再向左平移7位並與原本的值取 or 運算,其2進位值為
```python
0b110000011
```
這代表 possSum 可能為 1、7、8,也就是看2進位值中哪幾個位數是1,就代表總合可能是這幾個位數。接下再代入 2、8、2 的結果依序為
```python
0b11110001111
0b1111000111110001111
0b111111011111110111111
```
再由最後一位向前搜尋字串1,如果 target 對應的位數不是1,再繼續向前找,找到的位數就是答案。
<br />
於 ZeroJudge 測試結果,最長運算時間約為 46 ms,使用記憶體最多約為 3.5 MB,通過測試。
```python=
def solve(array, tar):
possSum = 1
for x in array:
possSum |= (possSum << x)
s = bin(possSum)[2:]
for i in range(len(s)):
if i >= tar and s[len(s)-i-1] == '1':
return i
M, S, N = map(int, input().split())
data = list(map(int, input().split()))
left = M - sum(data) # 目前剩下的空格數
target = S - left # 需要歸還的格子數量
if target <= 0: # 如果 target <= 0 不需要歸還格子,印出0
print(0)
else:
print(solve(data, target))
```
<br />
### 寫法2,使用二維陣列及 DP
於 ZeroJudge 測試結果,有6筆測資需要的記憶體爆掉。
```python=
M, S, N = map(int, input().split()) # M 個櫃子,需要 S 個櫃子,有 N 個人借用
xs = list(map(int, input().split())) # 每個客戶租用的格子數量
ans = M # 答案預設為 M
goal = S - (M - sum(xs)) # 目標需要收回的格子數量
if goal <= 0: ans = 0 # 不需要歸還任何櫃子,答案為 0
else: # 需要歸還櫃子
dp = [[0]*(M+1) for _ in range(N+1)] # dp[i][j] 為第 i 個客戶累計收回 j 個格子
if xs[0] >= goal: ans = min(ans, xs[0]) # 更新 ans,取較小的值
for j in range(xs[0], M+1): dp[1][j] = xs[0] # 處理第 1 項
for i, x in enumerate(xs[1:], start=2): # 處理第 2 ~ n 項
for j in range(1, M+1): # 依序檢查收回格子數量 1 ~ M
if x > j: dp[i][j] = dp[i-1][j] # 如果 x 大於 j,沒有回數更多的數量,與檢查第 i-1 個客戶時相同
else: # 回數更多的數量,取檢查第 i-1 個客戶時的數量與加上 x 時較大者
dp[i][j] = max(dp[i-1][j], dp[i-1][j-x] + x)
if dp[i][j] >= goal: ans = min(ans, dp[i][j]) # 大於等於目標,更新 ans,取較小的值
print(ans)
```
<br />
### 寫法3,使用滾動陣列及 DP
於 ZeroJudge 測試結果,最後一筆測資超時。
```python=
M, S, N = map(int, input().split()) # M 個櫃子,需要 S 個櫃子,有 N 個人借用
xs = list(map(int, input().split())) # 每個客戶租用的格子數量
ans = M # 答案預設為 M
goal = S - (M - sum(xs)) # 目標需要收回的格子數量
if goal <= 0: ans = 0 # 不需要歸還任何櫃子,答案為 0
else: # 需要歸還櫃子
d, p = [0]*(M+1), [0]*(M+1) # 為第 i 個客戶累計收回 j 個格子
if xs[0] >= goal: ans = min(ans, xs[0]) # 更新 ans,取較小的值
for j in range(xs[0], M+1): p[j] = xs[0] # 處理第 1 項
for x in xs[1:]: # 處理第 2 ~ n 項
for j in range(1, M+1): # 依序檢查收回格子數量 1 ~ M
if x > j: d[j] = p[j] # 如果 x 大於 j,沒有回數更多的數量,與檢查第 i-1 個客戶時相同
else: # 回數更多的數量,取檢查第 i-1 個客戶時的數量與加上 x 時較大者
d[j] = max(p[j], p[j-x] + x)
if d[j] >= goal: ans = min(ans, d[j]) # 大於等於目標,更新 ans,取較小的值
d, p = p, d # 交換資料
print(ans)
```
<br /><br />
## C++ 程式碼
### 寫法1,使用 bitset
於 ZeroJudge 測試結果,最長運算時間約為 7 ms,使用記憶體最多約為 704 kB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <bitset>
using namespace std;
int solve(vector<int> array, int tar);
int main() {
int M, S, N; cin >> M >> S >> N;
vector<int> data (N, 0);
int total = 0;
for(int i=0; i<N; i++) {
cin >> data[i];
total += data[i];
}
int left = M - total; // 目前剩下的空格數
int target = S - left; // 需要歸還的格子數量
if (target <= 0) // 如果 target <= 0 不需要歸還格子,印出0
cout << 0 << endl;
else
cout << solve(data, target) << endl;
return 0;
}
int solve(vector<int> array, int tar) {
bitset<1000000> possSum;
possSum[0] = 1;
for(auto x : array)
possSum |= (possSum << x);
for(int i=tar; i<1000000; i++) {
if (possSum[i] == 1) return i;
}
return 0;
}
```
<br />
### 寫法2,使用二維陣列及 DP
於 ZeroJudge 測試結果,最長運算時間約為 39 ms,使用記憶體最多約為 39.4 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int M, S, N, isum = 0; cin >> M >> S >> N; // M 個櫃子,需要 S 個櫃子,有 N 個人借用
vector<int> xs (N, 0); // 每個客戶租用的格子數量
for(int i=0, x; i<N; i++) {
cin >> x; xs[i] = x; isum += x; // isum 為已出借的櫃子數量
}
int ans = M, goal = S - (M - isum); // 答案預設為 M,目標需要收回的格子數量
if (goal <= 0) ans = 0; // 不需要歸還任何櫃子,答案為 0
else { // 需要歸還櫃子
vector<vector<int>> dp (N+1, vector<int> (M+1, 0)); // dp[i][j] 為第 i 個客戶累計收回 j 個格子
if (xs[0] >= goal) ans = min(ans, xs[0]); // 更新 ans,取較小的值
for(int j=xs[0]; j<=M; j++) dp[1][j] = xs[0]; // 處理第 1 項
for(int i=2; i<=N; i++) { // 處理第 2 ~ n 項
for(int j=1, x; j<=M; j++) { // 依序檢查收回格子數量 1 ~ M
x = xs[i-1]; // 目前在檢查的格子數量
if (x > j) dp[i][j] = dp[i-1][j]; // 如果 x 大於 j,沒有回數更多的數量,與檢查第 i-1 個客戶時相同
else { // 回數更多的數量,取檢查第 i-1 個客戶時的數量與加上 x 時較大者
dp[i][j] = max(dp[i-1][j], dp[i-1][j-x] + x);
if (dp[i][j] >= goal) ans = min(ans, dp[i][j]); // 大於等於目標,更新 ans,取較小的值
}
}
}
}
cout << ans << "\n";
return 0;
}
```
<br />
### 寫法3,使用滾動陣列及 DP
於 ZeroJudge 測試結果,最長運算時間約為 17 ms,使用記憶體最多約為 1.1 MB,通過測試。
```cpp=
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int M, S, N, isum = 0; cin >> M >> S >> N; // M 個櫃子,需要 S 個櫃子,有 N 個人借用
vector<int> xs (N, 0); // 每個客戶租用的格子數量
for(int i=0, x; i<N; i++) {
cin >> x; xs[i] = x; isum += x; // isum 為已出借的櫃子數量
}
int ans = M, goal = S - (M - isum); // 答案預設為 M,目標需要收回的格子數量
if (goal <= 0) ans = 0; // 不需要歸還任何櫃子,答案為 0
else { // 需要歸還櫃子
vector<int> d (M+1, 0), p (M+1, 0); // 為第 i 個客戶累計收回 j 個格子
if (xs[0] >= goal) ans = min(ans, xs[0]); // 更新 ans,取較小的值
for(int j=xs[0]; j<=M; j++) p[j] = xs[0]; // 處理第 1 項
for(int i=2; i<=N; i++) { // 處理第 2 ~ n 項
for(int j=1, x; j<=M; j++) { // 依序檢查收回格子數量 1 ~ M
x = xs[i-1]; // 目前在檢查的格子數量
if (x > j) d[j] = p[j]; // 如果 x 大於 j,沒有回數更多的數量,與檢查第 i-1 個客戶時相同
else { // 回數更多的數量,取檢查第 i-1 個客戶時的數量與加上 x 時較大者
d[j] = max(p[j], p[j-x] + x);
if (d[j] >= goal) ans = min(ans, d[j]); // 大於等於目標,更新 ans,取較小的值
}
}
swap(d, p); // 交換資料
}
}
cout << ans << "\n";
return 0;
}
```
<br /><br />
---
###### tags:`APCS`、`Python`、`C++`