# 【C++】競程筆記(背包問題 習題練習)
[TOC]
練習題目參考自:NTUCPC Guide,此筆記僅為個人學習用途。
最後更新時間:2026/1/23 下午3:46:12
## 電車遊戲
這道題是 0/1 背包問題或集合劃分的變形題。
Problem Source:https://oj.ntucpc.org/problems/803
該題目標是將 $N$ 個移動距離 $a_i$ 分配到兩個集合中:一個集合是「向左走($X$軸)」,另一個集合是「向上走($Y$軸)」。
設所有 $a_i$ 的總和為 $S$,且向左走的總距離為 $x$,那麼向上走的總距離就是 $S - x$。
題目要求最小化距離的平方:$x^2 + (S - x)^2$。
1. 定義狀態:Boolean 陣列 $dp[j]$ 為是否能夠湊出總和為 $j$ 的移動距離。
- 若 $dp[j]$ 為 true,表示可從給定的數字中選出若干個,使其總和剛好為 $j$(作為 $X$ 軸的移動距離)。
2. 定義轉移式:
對每個新移動距離 $v$(為輸入的 $a_i$),更新所有可能的 $j$。
若在之前的步驟中已能湊出 $j$($dp[j] = true$),則加上現在的 $v$ 後,即可湊出 $j + v$。
因此,
- 若選 $v$ ,狀態則是 $dp[j - v]$ 。(選 $v$ 之前能湊出 $j-v$)
- 若不選 $v$ ,則狀態依然是 $dp[j]$ 。(不選 $v$ 就能湊出 $j$)
最後得到完整轉移式:$$dp[j] = dp[j] \lor dp[j - v]$$
3. 定義初始狀態:$dp[0] = \text{true}$ ,其餘都是 false。
範例程式碼:
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int MAXS = 500005; // N = 1000, a_i = 500, MAXS = N * a_i
bool dp[MAXS];
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr);
int N;
cin >> N;
vector <int> a(N);
int total_sum = 0;
for (int i = 0; i < N; ++i){
cin >> a[i];
total_sum += a[i];
}
dp[0] = true;
for (int i = 0; i < N; ++i){
int v = a[i];
for (int j = total_sum; j >= v; --j)
dp[j] = dp[j] || dp[j - v];
}
long long ans = -1; // 因為距離平方肯定為 0 或正, 故設 -1
// 找到一個 x 使 x^2 + y^2 最小
for (int x = 0; x <= total_sum; ++x){
if (dp[x]){
long long y = total_sum - x;
long long curr_dist = (long long)x * x + y * y;
// ans == -1 的判斷是表示要先找到一個合法解
// curr_dist < ans 找最小值
if (ans == -1 || curr_dist < ans) ans = curr_dist;
}
}
cout << ans << '\n';
}
```
## APCS 2018/10 P4 置物櫃出租
該題為 0/1 背包問題的變形題。
Problem Source:https://oj.ntucpc.org/problems/804
題目要求最小化「被退租客戶的容量總和」,等同於要最大化「被保留客戶的容量總和」。
因此思路就可以轉換。
- 限制條件:王老先生需要 $S$ 個格子,置物櫃總共有 $M$ 個格子,因此,能夠留給客戶的最大空間為 $Limit = M - S$。
- 目標:在不超過 $Limit$ 的容量限制下,盡可能塞入最多的客戶容量。
- 計算結果:全部客戶原本的總容量 - 在 $Limit$ 限制下能保留的最大容量 = 最小損失。
1. 定義狀態: $dp[j]$ 為容量限制 $j$ 的情況下,所能裝入客戶物品的最大容量和。(根據題目,在此的價值等同於容量)
2. 定義轉移式:
對每個客戶 $i$(容量為 $v$),決定要不要留他,假設容量 $j$ 足夠放入該客戶($j \ge v$),可選擇:
- 不保留: $dp[j]$
- 保留: $dp[j - v] + v$
取最大值得到完整轉移式:$$dp[j] = \max(dp[j], \quad dp[j - v] + v)$$
由於每個客戶只能選一次,因此內層迴圈需由大到小遍歷(由 $Limit$ 遞減到 $v$),以避免重複選取同一個物品。
3. 定義初始狀態: $dp[0 ... j]$ 全都 0。
範例程式碼:
```cpp=
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr);
int n, M, S;
cin >> n >> M >> S;
vector <int> f(n);
int total_occupied = 0;
for (int i = 0; i < n; ++i){
cin >> f[i];
total_occupied += f[i];
}
int limit = M - S;
if (total_occupied <= limit){
cout << 0 << '\n';
return 0;
}
vector <int> dp(limit + 5, 0);
for (int i = 0; i < n; ++i){
int v = f[i];
for (int j = limit; j >= v; --j){
dp[j] = max(dp[j], dp[j - v] + v);
}
}
cout << (total_occupied - dp[limit]) << '\n';
}
```
## Minimizing Coins
此題為 DP 著名的「零錢兌換問題」。
Problem Source:https://cses.fi/problemset/task/1634
1. 定義狀態: $dp[i]$ 為湊成金額 $i$ 所需的最少硬幣數量。
2. 定義轉移式:
要算湊成金額 $i$ 的最少硬幣數,可以考慮最後加入的那一枚硬幣是哪一種。
假設硬幣面額集合 $C = \{c_1, c_2, \dots, c_n\}$
若選擇硬幣 $c_j$ 作為最後一枚硬幣,則剩下的金額為 $i - c_j$,此時的硬幣總數就是 $dp[i - c_j] + 1$。
為了求最少數量,則遍歷所有可能的硬幣面額,找出最小值:$$dp[i] = \min_{c \in C} (dp[i - c]) + 1$$
註:當 $i - c \ge 0$ 時才能轉移。
3. 定義初始狀態:
- 金額 = 0, $dp[0] = 0$
- 其餘金額,$dp[1 \cdots x] = INF$ or 極大值(x 為湊出的目標金額)
範例程式碼:
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr);
int n, x;
cin >> n >> x;
vector <int> coin(n);
for (int i = 0; i < n; ++i) cin >> coin[i];
vector <int> dp(x + 1, INF);
dp[0] = 0;
for (int i = 1; i <= x; ++i){
for (int c : coin)
if (i - c >= 0)
dp[i] = min(dp[i], dp[i - c] + 1);
}
cout << (dp[x] == INF ? -1 : dp[x]) << '\n';
}
```
## Coin Combinations I
Problem Source:https://cses.fi/problemset/task/1635
該題與上題類似,只是從原本要求用「最少硬幣數」湊出目標金額,變成求「有幾種方法數」可湊出目標金額。
注意題目說順序不同的組合被視為不同的方法(如 `1+2` 和 `2+1` 是不同的方法)。(該題為求排列數方法)
1. 定義狀態:$dp[i]$ 為湊成金額 $i$ 的方法數。
2. 定義轉移式:
假設要湊成金額 $i$,最後一步可加上任何一種面額的硬幣 $c$。
如果最後加上的硬幣是 $c$,則先前的金額一定是 $i-c$。因此,湊成金額 $i$ 的方法數,就是所有能透過加上一枚硬幣到達 $i$ 的前一個狀態的方法數總和。
完整轉移式:$$dp[i] = \sum_{c \in C} dp[i - c]$$
條件:$i - c \ge 0$
3. 定義轉移式: $dp[0] = 1$ ,當金額為 0 時只有一種方法數,其他初始化為 0。
範例程式碼:
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7;
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr);
int n, x;
cin >> n >> x;
vector <int> coin(n);
for (int i = 0; i < n; ++i) cin >> coin[i];
vector <int> dp(x + 1, 0);
dp[0] = 1;
for (int i = 1; i <= x; ++i){
for (int c : coin)
if (i - c >= 0)
dp[i] = (dp[i] + dp[i - c]) % MOD;
}
cout << dp[x] << '\n';
}
```
## Coin Combinations II
Problem Source:https://cses.fi/problemset/task/1636
與上題相似,但這題改求組合方法數,因為順序不同的組合視為同一種方法(如 `1+2` 和 `2+1` 是相同的)。
1. 定義狀態:$dp[i]$ 為湊成金額 $i$ 的有序組合方法數。
2. 定義轉移式:
原本 Coin Combinations I 是因為不同順序也算一種方法數,所以外層是從 1 遍歷到 x,內層遍歷 coin。
而在 Coin Combinations II 這個問題中,順序不同算是同一種方法,因此得將遍歷的順序對調,改成外層遍歷 coin,內層遍歷 1 到 x。
在轉移式上還是一樣的:$$dp[i] = \sum_{c \in C} dp[i - c]$$
條件:$i - c \ge 0$
3. 定義初始狀態:同上題。
範例程式碼:
```cpp=
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7;
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr);
int n, x;
cin >> n >> x;
vector <int> coin(n);
for (int i = 0; i < n; ++i) cin >> coin[i];
vector <int> dp(x + 1, 0);
dp[0] = 1;
for (int c : coin){
for (int i = 1; i <= x; ++i)
if (i - c >= 0)
dp[i] = (dp[i] + dp[i - c]) % MOD;
}
cout << dp[x] << '\n';
}
```