2022.10.10 总结
1. 洛谷 P1164(01 背包)
题意
有 \(n\) 种菜,第 \(i\) 种菜卖 \(a_i\) 元,每种菜只有一份。
你口袋里有 \(m\) 元,你要把所有钱都用完,请问有多少种点菜方法。
思路
45 分
子集搜索,对于每种菜品,枚举是否选择。
时间复杂度
每种菜品都有两种转移,总共 \(n\) 种菜品。
总时间复杂度为 \(O(2 ^ n)\)。
空间复杂度
变量记录方案数,\(O(1)\)。
代码
void dfs(int t, int sum){
if (t == n + 1) {
cnt += sum == m; // 判断相等
return ;
}
dfs(t + 1, sum); // 选
dfs(t + 1, sum + a[t]); // 不选
}
90 分
子集搜索 + 剪枝。判断钱数是否超过 \(m\) 元。
时间复杂度
每种菜品都有两种转移,总共 \(n\) 种菜品。
时间复杂度上限为 \(O(2 ^ n)\)。
空间复杂度
变量记录方案数,\(O(1)\)。
代码
void dfs(int t, int sum){
if (sum > m) { // 剪枝
return ;
}
if (t == n + 1) {
cnt += sum == m;
return ;
}
dfs(t + 1, sum);
dfs(t + 1, sum + a[t]);
}
100 分(记忆化搜索)
判断是否出现过这种状态,出现过,直接返回,否则,搜索。
时间复杂度
每种状态遍历一次,\(O(m \times n)\)
空间复杂度
数组记录状态是否出现过,\(O(m \times n)\)。
代码
int S(int ans, int t){
if (t == n + 1) {
if (ans == m) {
return 1;
}
return 0;
}
if (ans + a[t] <= m && !w[ans + a[t]][t + 1]) { // 如果没有出现过并且状态合法
f[ans + a[t]][t + 1] = S(ans + a[t], t + 1);
w[ans + a[t]][t + 1] = 1;
}
if (!w[ans][t + 1]) {
f[ans][t + 1] = S(ans, t + 1);
w[ans][t + 1] = 1;
}
return f[ans + a[t]][t + 1] + f[ans][t + 1];
}
100 分(DP)
\(01\) 背包,先枚举钱数,再枚举每种菜。
状态:\(dp_{i, j}\) 表示当前买了第 \(j\) 种,花了 \(i\) 元的方案数。
转移方程:
\(dp_{i, j} = dp_{i, j - 1} (a_j > j)\)
\(dp_{i, j} = dp_{i, j - 1} + dp_{i - a_j, j - 1} (a_j \leq j)\)
初始状态:\(dp_{0, 0} = 1\)
目标状态:\(dp_{m, n}\)
时间复杂度
枚举钱数,\(O(m)\)。
枚举每种菜,\(O(n)\)。
总时间复杂度为 \(O(m \times n)\)。
空间复杂度
\(dp\) 数组,\(O(m \times n)\)。
代码
dp[0][0] = 1; // 初始状态
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = dp[i][j - 1];
if (a[j] <= i) {
dp[i][j] += dp[i - a[j]][j - 1]; // 转移
}
}
}
cout << dp[m][n]; // 目标状态
2. 洛谷 P1077(多重背包)
题意
总共有 \(n\) 种花,从 \(1\) 到 \(n\) 编号,第 \(i\) 种花最多摆 \(a_i\) 盆,可以摆 \(m\) 盆花,摆花时,同一种花摆在一起,请你求出有多少种摆花方案。
思路
30 分
搜索 + 可行性剪枝。枚举每种花,再枚举这种花摆几盆。
时间复杂度
总共有 \(n\) 盆花,\(O(n)\)。
第 \(i\) 种花可以摆 \(a_i\) 盆,\(O(a_i)\)。
总时间复杂度为 \(O(n \times a_i)\)。
空间复杂度
变量记录方案数,\(O(1)\)。
代码
void dfs(int t, int ans){
if (t > n + 1 || ans > m) { // 可行性剪枝
return ;
}
if (ans == m) {
cnt = (cnt + 1) % MOD;
return ;
}
for (int j = 0; j <= a[t]; j++) { // 枚举盆数
dfs(t + 1, ans + j);
}
}
100 分(DP)
多重背包,枚举当前摆哪种花,再枚举一共摆了多少盆,最后枚举这种花摆了多少盆。
状态:\(dp_{i, j}\) 表示当前选择第 \(i\) 种花,总共摆了 \(j\) 盆。
转移方程:\(dp_{i, j} = \sum \limits _ {k = 1} ^ {min(j, a_i)} {dp_{i - 1, j - k}}\)
初始状态:\(dp_{0, 0} = 1\)
目标状态:\(dp_{n, m}\)
时间复杂度
枚举摆哪种花,\(O(n)\)。
枚举一共摆多少盆,\(O(m)\)。
枚举这种摆多少盆,\(O(a_i)\)。
总时间复杂度为 \(O(n \times m \times a_i)\)。
空间复杂度
二维 \(dp\) 数组记录方案数,\(O(n \times m)\)。
代码
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= min(j, a[i]); k++) {
dp[i][j] = (dp[i][j] + dp[i - 1][j - k]) % MOD; // 转移
}
}
}
cout << dp[n][m];
3. 洛谷 P1616(完全背包)
题意
有 \(m\) 种草药,每种草药都有采摘的时间和对应的价值。
你有 \(t\) 个单位的时间来采药,每种药都可以采无数个,请求出在规定时间内,最多可以采多少价值的草药。
思路
搜索
搜索 #1(50 分)
枚举每种草药采摘多少种,类似多重背包。
代码
void dfs(int i, int sumt, long long sumv) {
if (i == m + 1) {
ans = max(ans, sumv);
return;
}
for (int j = 0; j <= (t - sumt) / a[i]; j++) {
dfs(i + 1, sumt + j * a[i], sumv + 1ll * j * b[i]);
}
}
搜索 #2(40 分)
对于第 \(i\) 种的某一次,有选与不选两种情况,并且选了之后还可以再选。
代码
void dfs(int i, int sumt, long long sumv) {
if (i == m + 1) {
ans = max(ans, sumv);
return;
}
dfs(i + 1, sumt, sumv);
if (sumt + a[i] <= t) {
dfs(i, sumt + a[i], sumv + b[i]);
}
}
搜索 #3(40 分)
每次枚举选哪种草药,直到时间超过限制。
代码
void dfs(int sumt, long long sumv) {
ans = max(ans, sumv);
for (int i = 1; i <= m; i++) {
if (sumt + a[i] <= t) {
dfs(sumt + a[i], sumv + b[i]);
}
}
}
时间复杂度
不好算,不算了。
空间复杂度
变量记录最大值,\(O(1)\)。
100 分(DP)
记录在每个时刻可以采的最大价值。
状态:\(dp_i\) 表示在时刻 \(i\) 能采的草药的最大值
转移方程:
\(dp_i = max(dp_i, dp_{i - a_j} + b_j) (j \leq m)\)
初始状态:\(dp_i = 0\)
目标状态:\(dp_t\)
时间复杂度
枚举时间,\(O(t)\)。
枚举草药,\(O(m)\)。
总时间复杂度为 \(O(t \times m)\)。
空间复杂度
一维 \(dp\) 数组记录时间,\(O(t)\)。
代码
for (int i = 0; i <= t; i++) {
for (int j = 1; j <= m; j++) {
if (i >= a[j]) {
dp[i] = max(dp[i], dp[i - a[j]] + b[j]);
}
}
}
cout << dp[t];
4. 洛谷 P1509
题意
sqybi 有 \(m\) 元,\(r\) 的人品,他看中了 \(N\) 个 MM,编号为 \(1 \sim n\),他需要请其中一些 MM 吃饭。
请 \(i\) 号 MM 吃饭要花 \(rmb_i\) 大洋,\(rp_i\) 的人品,并且 sqybi 需要花 \(time_i\) 来搞定这个 MM。
请你求出 sqybi 泡最多的 MM 最少需要多少时间。
注意:sqybi 一次只能泡一个 MM。
思路
100 分
要解决这个问题,首先我们需要确定状态。
\((i, time, rmb, rp, cnt)\):
表示当前在泡第 \(i\) 个 MM,一共花了 \(time\) 个单位的时间,\(rmb\) 元,\(rp\) 的人品,已经泡到了 \(cnt\) 个 MM。
也就是:
\(dp_i, j, k\) 表示花 \(j\) 元,\(k\) 的人品去泡前 \(i\) 个 MM 能跑到的最多 MM 和花费的最少时间(先考虑数量,再考虑时间,用结构体实现)。
之后再确定转移。
\((i, time, rmb, rp, cnt) -> (i + 1, time, rmb, rp, cnt), (i + 1, time + t_i, rmb + rm_i, rp + r_i, cnt + 1)\)。
所以转移方程就是:
\(dp_{i, j, k} = dp_{i - 1, j, k} (i \le n)\)
\(dp_{i, j, k} = max(dp_{i, j, k}, {dp_{i - 1, j - rmb_i, k - rp_i}.cnt + 1, dp_{i - 1, j - rmb_i, k - rp_i} + t_i}) (j \ge rmb_i, k \ge rp_i)\)
时间复杂度
有 \(n\) 个 MM,\(O(n)\)。
有 \(m\) 元,\(O(m)\)。
有 \(r\) 个人品,\(O(r)\)。
总时间复杂度为 \(O(n \times m \times r)\)。
空间复杂度
\(dp\) 数组记录数量和时间,\(O(n \times m \times r)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 110, RMB = 110, RP = 110, T = 1010 * N;
int n, rmb[N], rp[N], t[N], m, r;
struct C{
int cnt, t; // 最多数量和最少时间
} dp[N][RMB][RP];
int main(){
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> rmb[i] >> rp[i] >> t[i];
}
cin >> m >> r;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= r; k++) {
dp[i][j][k] = dp[i - 1][j][k];
if (j >= rmb[i] && k >= rp[i]) {
C x = dp[i - 1][j - rmb[i]][k - rp[i]];
if (dp[i][j][k].cnt < x.cnt + 1) { // 先考虑数量
dp[i][j][k] = {x.cnt + 1, x.t + t[i]};
} else if (dp[i][j][k].cnt == x.cnt + 1) { // 再考虑时间
dp[i][j][k].t = min(dp[i][j][k].t, x.t + t[i]);
}
}
}
}
}
cout << dp[n][m][r].t;
return 0;
}