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)\) 即可。

总结状态转移方程:

\[f(i,j)= \begin{cases} f(i+1,j-1),&s[i]=s[j],\\ \min(f(i+1,j)+a_{s[i]},f(i,j-1)+a_{s[j]}),\qquad &s[i]\ne s[j]. \end{cases} \]

细节:在 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)

P4303 [AHOI2006]基因匹配 - 洛谷

题意

给定两个长度均为 \(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. 书柜的尺寸(状态设计)

P2160 [SHOI2007]书柜的尺寸 - 洛谷

题意

懒得总结,自己去看吧

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;
}
posted @ 2022-11-06 20:36  f2021ljh  阅读(9)  评论(0)    收藏  举报