# 【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'; } ```