9.23 动态规划测试 题解
9.23 动态规划测试 题解
A. 混合背包(0/1 背包、多重背包、完全背包)
题意
有一个容量为 \(V\) 的背包,有 \(n\) 件物品,重量分别是 \(w_1,w_2,\ldots ,w_n\),价值分别为 \(c_1,c_2,\ldots ,c_n\),每个物品可以取的次数为 \(p_i\)(若 \(p_i=0\) 则表示可以取无限次)。求将哪些物品装入背包使 物品总重量不超过背包容量 且 价值总和最大。
\(n\le 500,V\le 1000,0\le p_i\le5\)。
思路
0/1 背包、多重背包和完全背包的混合。
把每个物品属于哪一种背包存起来,dp 过程中分类计算。
由于考试时我不会多重背包的优化,所以直接把「可以取多次」等效成了「多个物品」,由于数据范围 \(p_i\) 较小也过了。
考试后机房大佬 \(\mathrm{x\color{red}bc}\) 说 Luogu 上有原题(P1833 樱花),去看了下发现 \(0\le p_i\le100\) 这样根本过不去,于是学了一下二进制优化。
二进制优化
思想是把第 \(i\) 个物品分成多个物品,这些物品的重量和价值都分别乘以一个系数,使得用这些物品可以组合成 \(0\sim p_i\) 个第 \(i\) 个物品。
具体地,令这些系数为 \(1, 2, 2^2,\dots,2^{k−1}, p_i-2^k + 1\),且 \(k\) 是满足 \(p_i-2^k + 1 > 0\) 的最大整数。发现这样分配可以满足上述条件。
比如,\(p_i=13\),令系数为 \(1,2,4,6\),那么用 \(1,2,4,6\) 可以组合成 \(0\sim13\) 的任何整数。相当于可以取 \(0\sim13\) 的任何次。
然后跑 0/1 背包即可。
代码
#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
using namespace std;
int const N = 2550, V = 1010;
int n, v, w[N], c[N], flag[N], a, dp[V];
signed main() {
cin >> v >> n;
f(i, 1, n) {
int x, y, z;
cin >> x >> y >> z;
if (z) {
int p = 1;
//二进制分解
while (z > p) {
w[++a] = x * p, c[a] = y * p, flag[a] = 1;
z -= p;
p <<= 1;
}
w[++a] = x * z, c[a] = y * z, flag[a] = 1;
}
else w[++a] = x, c[a] = y, flag[a] = 0;
}
f(i, 1, a)
if (flag[i])
g(j, v, w[i]) //倒序跑0/1背包
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
else
f(j, w[i], v) //正序跑完全背包
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
cout << dp[v] << '\n';
return 0;
}
B. 回文串
P2890 [USACO07OPEN]Cheapest Palindrome G - 洛谷
题意
给定一个长度为 \(n\)(\(n\le10^3\))的由小写字母构成的字符串 \(s\),你可以对这个字符串进行添加字符或删除字符的操作,给出添加或删除每个字符的代价,问将给出的字符串变成回文串需要的最小代价。
思路
设添加字符 \(i\) 的代价为 \(add_i\),删除字符 \(i\) 的代价为 \(del_i\)。
令 \(f(i,j)\) 表示使 \(s[i..j]\) 变为回文串的最小代价,其中 \(i\le j\)。显然 \(f(i,i)=0\)。
考虑如何转移:
首先,若 \(s[i]=s[j]\),那么 \(f(i,j)=f(i+1,j-1)\)。
否则,假设 \(s[i+1..j]\) 是回文串,要想使 \(s[i..j]\) 变成回文串,我们可以删去前面的 \(s[i]\) 或者在后面添加一个 \(s[i]\),所以 \(f(i,j)=f(i+1,j)+\min(del_{s[i]},add_{s[i]})\)。假设 \(s[i..j-1]\) 是回文串也是同理。
注意到删除和增加是等效的,所以我们只需保存 \(a_i=\min(del_i,add_i)\) 即可。
总结状态转移方程:
细节:在 dp 过程中,第一维 \(i\) 由 \(i+1\) 转移过来,所以要倒序枚举 \(i\)。
代码
#include <iostream>
#include <cstring>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
using namespace std;
const int N = 1e3 + 10;
int dp[N][N], a[N];
char ch[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> ch;
f(i, 0, 25) cin >> a[i];
f(i, 0, 25) {
int x;
cin >> x;
a[i] = min(a[i], x);
}
int l = strlen(ch);
g(i, l - 2, 0) {
f(j, i + 1, l - 1) {
if (ch[i] == ch[j])
if (j == i + 1) dp[i][j] = 0;
else dp[i][j] = dp[i + 1][j - 1];
else
dp[i][j] = min(dp[i + 1][j] + a[ch[i] - 'a'], dp[i][j - 1] + a[ch[j] - 'a']);
}
}
cout << dp[0][l - 1];
return 0;
}
C. 最长公共子序列(特殊的 LCS)
题意
给定两个长度均为 \(5n\) 的序列 \(a,b\),保证每个序列中 \(1\sim n\) 的数字均出现恰好 \(5\) 次,求最长公共子序列。
\(n\le2\times10^4\)。
思路
普通的求 LCS 问题的 dp 复杂度为 \(O(n^2)\),只能得 70pts(别问我怎么知道的),所以我们来思考一下这道题有什么特殊性质。
注意到每个数字只恰好出现 \(5\) 次,即每个数字的分布很稀疏,并且对于 \(b_i\),答案只由 \(a_j=b_i\) 更新,且之前的答案序列只可能在 \(a_1\sim a_{j-1}\) 中。
所以我们遍历 \(b\),对于每个 \(b_i\),找到 \(a_j=b_i\),查询 \(a_1\sim a_{j-1}\) 与 \(b_1\sim b_{i-1}\) 的最长 LCS 长度,加一,更新答案。
具体实现:首先读入 \(a\) 时记录每一个数在 \(a\) 中的位置 \(j\)。读入 \(b\) 时按上面所说的用树状数组进行查询,维护 LCS 的前缀最大值。
细节:由于前面更新的答案会影响到后面,所以在枚举所记录的 \(j\) 的时候要倒序枚举(类似 0/1 背包)。
代码
#include <iostream>
#include <vector>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define g(x, y, z) for (int x = (y); (x) >= (z); --(x))
#define lowbit(x) (x & (-x))
using namespace std;
const int N = 1e5 + 10;
int n, ans;
vector<int> pos[N];
int c[N];
void add(int x, int y) {
while (x <= n) {
c[x] = max(c[x], y);
x += lowbit(x);
}
return;
}
int query(int x) { //查询前缀最大值
int ret = 0;
while (x > 0) {
ret = max(ret, c[x]);
x -= lowbit(x);
}
return ret;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
n *= 5;
int x;
f(i, 1, n) {
cin >> x;
pos[x].push_back(i);
}
f(i, 1, n) {
cin >> x;
g(j, 4, 0) //倒序
add(pos[x][j], query(pos[x][j] - 1) + 1);
}
cout << query(n) << '\n';
return 0;
}
D. 书柜的尺寸(状态设计)
题意
懒得总结,自己去看吧
sto \(\mathrm{R\color{red}aymin}\) orz
思路
设 \(dp(i,j,k)\) 表示放到第 \(i\) 本书,第一层总厚度为 \(j\),第二层总厚度为 \(k\) 时的最低总高度。
处理出 \(sum_i=\sum\limits_{j=1}^it_j\),那么第三层厚度为 \(sum_i-j-k\)。
答案为 \(\min\limits_{j,k}\{dp(n,j,k)\times\max(j,k,sum_n-j-k)\}\)。
我们先将书按照高度从大到小排序,那么第一个放进某一层的书的高度即为这一层的高度。
状态转移:设放第 \(i\) 本书之前,三层书的厚度分别为 \(T_1,T_2,T_3\)。第 \(i\) 本书的厚度为 \(t_i\),高度为 \(h_i\)。
将第 \(i\) 本书放进第一层:
-
若此时第一层为空层,即 \(T_1=0\),那么 \(dp(i,T_1+t_i,T_2)=\min\{dp(i,T_1+t_i,T_2),\;dp(i-1,T_1,T_2)+h_i\}\);
-
否则 \(dp(i,T_1+t_i,T_2)=\min\{dp(i,T_1+t_i,T_2),\;dp(i-1,T_1,T_2)\}\)。
第二层、第三层同理。
第一维用滚动数组优化。
代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <climits>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
typedef long long ll;
const int N = 80, T = 2110;
int n, sum[N], dp[2][T][T];
ll ans = LLONG_MAX;
struct book {
int h, t;
bool operator<(book const &o) const { return h > o.h; }
} a[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
f(i, 1, n) cin >> a[i].h >> a[i].t;
sort(a + 1, a + n + 1);
f(i, 1, n) sum[i] = sum[i - 1] + a[i].t;
memset(dp, 0x3f, sizeof dp);
dp[0][0][0] = 0;
f(i, 1, n) {
int nw = i & 1, pr = nw ^ 1;
memset(dp[nw], 0x3f, sizeof(dp[nw]));
f(t1, 0, sum[i - 1]) {
f(t2, 0, sum[i - 1] - t1) {
int t3 = sum[i - 1] - t1 - t2;
dp[nw][t1 + a[i].t][t2] = min(dp[nw][t1 + a[i].t][t2], dp[pr][t1][t2] + a[i].h * (!(bool)(t1)));
dp[nw][t1][t2 + a[i].t] = min(dp[nw][t1][t2 + a[i].t], dp[pr][t1][t2] + a[i].h * (!(bool)(t2)));
dp[nw][t1][t2] = min(dp[nw][t1][t2], dp[pr][t1][t2] + a[i].h * (!(bool)(t3)));
}
}
}
f(t1, 1, sum[n] - 1) {
f(t2, 1, sum[n] - t1 - 1) {
int t3 = sum[n] - t1 - t2;
ans = min(ans, 1ll * max(max(t1, t2), t3) * dp[n & 1][t1][t2]);
}
}
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号