[动态规划] 背包 dp
背包 dp
AcWing 278. 数字组合
\(n\) 个数就是 $n $ 个物品,每个物品的价值就是它本身的数值,只能用一次,要求价值和为 \(m\) 的方案数。直接 01 背包即可。
int n, m;
int a[N], f[M];
signed main()
{
cin >> n >> m;
for (rint i = 1; i <= n; i++) cin >> a[i];
memset(f, 0, sizeof f);
f[0] = 1;
for (rint i = 1; i <= n; i++)
for (rint j = m; j >= a[i]; j--)
f[j] += f[j - a[i]];
cout << f[m] << endl;
return 0;
}
AcWing 279. 自然数拆分
若干个正整数就是若干个物品,要想能相加为 \(n\),且至少两个数相加,能用上的数就是 \(1\) ~ \(n - 1\),共 \(n - 1\) 个物品,价值就是数值本身。完全背包即可。
int n;
int f[N];
signed main()
{
cin >> n;
memset(f, 0, sizeof f);
f[0] = 1;
for (rint i = 1; i <= n - 1; i++)
for (rint j = i; j <= n; j++)
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
UVA323 Jury Compromise
本题是一个 01 背包问题,我们将 \(n\) 个人看作 \(n\) 个物品,那么每个物品会有三个体积:
-
- 人数,每个候选人都是 \(1\) 个人,最终要选出 \(m\) 个人
-
- 辩方得分,即辩方给每个候选人打的分数 \(a[i]\)
-
- 控方得分,即控方给每个候选人打的分数 \(b[i]\)
因此我们需要依次考虑每个候选人是否选入评审团,当外层循环到阶段 \(i\) 时,表示已经考虑了前 \(i\) 个候选人的入选情况,用 \(bool\) 数组 \(f[j][d][p]\) 表示已有 \(j\) 人被选入评审团,当前辩方总分为 \(d\)、控方总分为 \(p\) 的状态是否可行。
第 \(i\) 个候选人有选和不选两种情况,得出状态转移方程:
起始状态 \(f[0][0][0] = 1\),目标是 \(f[m][d][p] = 1\),要求 \(|d - p|\) 尽量小,\(d + p\) 尽量大。
到此我们初步分析出了一个算法,但是并没有很好的利用价值这一维度,我们可以进一步优化,我们可以将每个候选人的辩方、控方双方得分的差 \(a[i] - b[i]\) 作为体积之一,把辩方、控方双方得分的和作为该物品
的价值。
当外层循环到 \(i\) 时,设 \(f[j][k]\) 表示已经在前 \(i\) 个候选人中选出了 \(j\) 个,此时辩方与控方总分的差为 $ k$ 时,辩方与控方总分的和的最大值。
同样有选和不选两种情况,状态转移方程:
起始状态 \(f[0][0] = 0\),目标是 \(f[m][k]\),满足 \(|k|\) 尽量小,当 \(|k|\) 相同时 \(f[m][k]\) 尽量大。
最终还要输出具体方案,用一个 \(d[i][j][k]\) 表示外层循环到 \(i\) 时,状态 \(f[j][k]\) 是从哪个候选人转移过来的。递归找出整个方案即可。
int n, m;
int f[M][K];
int d[N][M][K];
//表示 f[i][j][k] 是从哪个候选人转移过来的
int a[N], b[N];
//每个候选人的辩方、控方得分
vector<int> path;
//选择的候选人编号
int suma, sumb;
//辩方、控方总分
void get_path(int i, int j, int k)
//从最优状态回推方案
{
if (!j) return;
//回推完所有候选人结束程序
int last = d[i][j][k];
get_path(last - 1, j - 1, k - (a[last] - b[last]));
//继续递归
path.push_back(last);
//将当前候选人加入方案
suma += a[last], sumb += b[last];
//累加辩方、控方总分
}
signed main()
{
int T = 1;
while (cin >> n >> m && n || m)
{
for (rint i = 1; i <= n; i++) cin >> b[i] >> a[i];
memset(f, -0x3f, sizeof f);
f[0][400] = 0; //f[0][0] -> f[0][400] (k 平移 400)
//01背包
for (rint i = 1; i <= n; i++)
{
for (rint j = m; j > 0; j--)
{
for (rint k = 0; k <= 800; k++)
{
//不选 i
d[i][j][k] = d[i - 1][j][k];
//选 i
if (k - (a[i] - b[i]) < 0 || k - (a[i] - b[i]) > 800) continue;
//状态不合法直接跳过
if (f[j][k] < f[j - 1][k - (a[i] - b[i])] + (a[i] + b[i]))
//如果辩方、控方总和之和更大,更新
{
f[j][k] = f[j - 1][k - (a[i] - b[i])] + (a[i] + b[i]);
//状态转移
d[i][j][k] = i; //记录从哪个候选人转移过来
}
}
}
}
//找出最优方案对应的 k
int res = 0;
for (rint k = 0; k <= 400; k++) // k 尽可能的小
{
if (f[m][400 + k] >= 0 && f[m][400 + k] >= f[m][400 - k])
//辩方、控方总分的和尽量大
{
res = 400 + k; //选双方总和的和较大的一个 k
break; //第一个有解的 k 一定最小
}
else if (f[m][400 - k] >= 0)
{
res = 400 - k;
break;
}
}
path.clear(); //清空方案
suma = sumb = 0; //重置总分
get_path(n, m, res); //从最优状态回推方案
//输出
printf("Jury #%lld\n", T++);
printf("Best jury has value %d for prosecution and value %lld for defence:\n", sumb, suma);
for (rint i = 0; i < path.size(); i++) printf(" %lld", path[i]);
printf("\n\n");
}
return 0;
}
AcWing 281. 硬币
本题问的是有几个结果是可以组成的,是一个可行性问题,不是一个最优性问题。可以看出是一个多重背包问题。因此设 \(bool\) 数组 \(f[i][j]\) 表示前 \(i\) 种硬币能否拼成面值 \(j\)
可以把多重背包拆成01背包来做,状态转移方程:
但是这样时间复杂度太高了,需要进行优化,用二进制拆分法。
int n, m;
int a[N];
//a[i] 表示第 i 个硬币的面值,s[i] 表示第 i 个硬币的数量
int v[N], cnt;
//二进制拆分后每个物品的面值
bool f[M];
//设 f[i][j] 表示前 i 个硬币能否拼成面值 j,降掉 i 维
signed main()
{
while (cin >> n >> m, n || m)
{
for (rint i = 1; i <= n; i++) cin >> a[i];
memset(f, 0, sizeof f);
f[0] = 1;
//二进制拆分
cnt = 0;
for (rint i = 1; i <= n; i++)
{
int s;
cin >> s;
int k = 1;
while (k <= s)
{
cnt++;
v[cnt] = a[i] * k;
s -= k;
k *= 2;
}
if (s > 0)
{
cnt++;
v[cnt] = a[i] * s;
}
}
//01背包
for (rint i = 1; i <= cnt; i++)
for (rint j = m; j >= v[i]; j--)
f[j] |= f[j - v[i]];
int ans = 0;
for (rint i = 1; i <= m; i++) ans += f[i];
cout << ans << endl;
}
return 0;
}