2022 暑期 DP 极限单兵计划
前言
LJ 认为我的 DP 是我的一大弱项,便精心为我准备了 毒瘤DP 12 题(然后发现原来给的 T1 是个树套树,就变成 毒瘤DP 11 题
感谢 LJ 教练。。。。。
为了方便复习,代码均格式化
PS : 如果 BZOJ 打不开可以用这个 link
T1 LibreOJ#6089. 小 Y 的背包计数问题
直接搞背包 dp 显然直接挂掉
考虑根号分治
\(i≤\sqrt{n}\) :做多重背包,加前缀和优化
\(i>\sqrt{n}\) :每种物品可以看作有无限个,做完全背包,但仍然是 \(O(n^2)\) 的。继续优化。
观察到物品个数至多 \(n−\sqrt{n}\) 个,设 \(g[i][j]\) 表示在 \(i\) 个物品体积为 \(j\) 的方案数。转移的话这里有一个技巧,即分两种特殊的情况,
第一种是 \(i\) 个物品的体积全部都大于 \(\sqrt{n}+1\),这样的话 \(g[i][j]+=g[i][j−i]\),可以看成前一种状态所有的物品的体积全部 \(+1\) ,
最后还要算上包括了体积为 \(\sqrt{n}+1\) 的情况,即 \(g[i][j]+=g[i−1][j−\sqrt{n}−1]\)。
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e5 + 5;
const int M = 3e2 + 2e1;
const int mod = 23333333;
int n, m, f[M][N], g[M][N], sum[N], ans;
signed main() {
cin >> n;
m = sqrt(n);
f[0][0] = 1;
for (rint i = 1; i <= m; i++) {
for (rint j = 0; j <= n; j++) {
sum[j] = f[i - 1][j];
}
for (rint j = i; j <= n; j++) {
sum[j] = (sum[j] + sum[j - i]) % mod;
}
for (rint j = 0; j <= n; j++) {
f[i][j] = sum[j];
if (j >= i * i + i) {
f[i][j] = (f[i][j] - sum[j - i * i - i] + mod) % mod;
}
}
}
ans = f[m][n];
g[0][0] = 1;
for (rint i = 1; i <= m; i++) {
for (rint j = i * m + i; j <= n; j++) {
g[i][j] = (g[i][j - i] + g[i - 1][j - m - 1]) % mod;
ans = (ans + (long long)g[i][j] * f[m][n - j] % mod) % mod;
}
}
cout << ans << endl;
return 0;
}
T2 P3959 [NOIP2017 提高组] 宝藏
状态压缩 \(DP\)
位二进制数,表示每个点是否存在。
状态 \(f[i][j]\) 表示:
集合:所有包含 \(i\) 中所有点,且树的高度等于 \(j\) 的生成树
属性:最小花费
状态计算:枚举 \(i\) 的所有非全集子集 \(S\) 作为前 \(j - 1\) 层的点,剩余点作为第 \(j\) 层的点。
核心: 求出第 \(j\) 层的所有点到 \(S\) 的最短边,将这些边权和乘以 \(j\),直接加到 \(f[S][j - 1]\) 上,即可求出 \(f[i][j]\)。
证明:
将这样求出的结果记为 \(f'[i][j]\)
\(f[i][j]\) 中花费最小的生成树一定可以被枚举到,因此 \(f[i][j] >= f'[i][j]\);
如果第 \(j\) 层中用到的某条边 \((a, b)\) 应该在比j小的层,假设 \(a\) 是 \(S\) 中的点,\(b\) 是第 \(j\) 层的点,则在枚举 \(S + {b}\) 时会得到更小的花费,即这种方式枚举到的所有花费均大于等于某个合法生成树的花费,因此 \(f[i][j] <= f'[i][j]\)
所以有 \(f[i][j] = f'[i][j]\)
#include <bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e1 + 2;
const int M = 5e3 + 5;
int n, m, f[M], g[N][N], d[N], ans = 0x3f3f3f3f;
int inline min(int a, int b) {
return a < b ? a : b;
}
inline int read() {
int x = 0, falg = 0;
char c = getchar();
while (c > '9' || c < '0') {
if (c == '-')
falg = 1;
c = getchar();
}
while (c <= '9' && c >= '0') {
x = x * 10 + c - '0';
c = getchar();
}
return falg ? -x : x;
}
inline void write(int x) {
if (x < 0) {
putchar('-');
x = -x;
}
if (x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
void inline dfs(int u) {
if (u == (1 << n) - 1) {
return ;
}
for (rint i = 0; i < n; i++) {
if (u >> i & 1) {
for (rint j = 0; j < n; j++) {
if (u >> j & 1) {
continue;
}
if (g[i][j] == 0x3f3f3f3f) {
continue;
}
int x = u | (1 << j);
if (f[x] > f[u] + d[i]*g[i][j]) {
f[x] = f[u] + d[i] * g[i][j];
int t = d[j];
d[j] = d[i] + 1;
dfs(x);
d[j] = t; //恢复现场
}
}
}
}
return ;
}
signed main() {
n = read();
m = read();
memset(g, 0x3f, sizeof g);
while (m--) {
int a = read() - 1, b = read() - 1, c = read();
g[a][b] = g[b][a] = min(g[a][b], c);
}
for (rint i = 0; i < n; i++) {
memset(f, 0x3f, sizeof f);
f[1 << i] = 0;
for (rint j = 0; j < n; j++) {
d[j] = n;
}
d[i] = 1;
int p = 1 << i;
int q = 1 << n;
dfs(p);
ans = min(ans, f[q - 1]);
}
write(ans);
puts("");
return 0;
}
T3 BZOJ#1996. [Hnoi2010]chorus
\(dp[i][j][0]\) : 第 \(i\) 人从左边进来的方案数
\(dp[i][j][1]\) : 第 \(j\) 人从右边进来的方案数
剩下的看代码就可以了,这个题比较水
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int mod = 19650827ll;
const int N = 5e3 + 5;
const int M = 1e1 - 7;
int n, a[N], dp[N][N][M];
signed main() {
cin >> n;
for (rint i = 1; i <= n; i++) {
cin >> a[i];
}
for (rint i = 1; i <= n; i++)
dp[i][i][0] = 1;
for (rint len = 2; len <= n; len++)
for (rint l = 1; l <= n; l++) {
int r = l + len - 1;
if (a[l] < a[l + 1])
dp[l][r][0] += dp[l + 1][r][0];
if (a[l] < a[r])
dp[l][r][0] += dp[l + 1][r][1];
if (a[r] > a[l])
dp[l][r][1] += dp[l][r - 1][0];
if (a[r] > a[r - 1])
dp[l][r][1] += dp[l][r - 1][1];
dp[l][r][0] %= mod, dp[l][r][1] %= mod;
}
cout << (dp[1][n][0] + dp[1][n][1]) % mod);
return 0;
}
T4 P2577 [ZJOI2004]午餐
设 \(f[i][j][k]\) 表示前 \(i\) 个人在 \(1\) 窗口打饭时间为 \(j\),在 2 窗口打饭为 \(k\) 的最优解
但是 \(j\) 和 \(k\) 这两维都得开到 \(40000\)
考虑优化空间。发现只要对打饭时间维护一个前缀和,那么就可以只保存一个时间了,另外一个时间则是 \(sum[i]−j\)
考虑转移(方程里面的 \(s[i]\) 是前缀和)
对于在1窗口打饭的情况:\(dp[i][j]=min(dp[i][j],max(dp[i−1][j−a[i].x],j+a[i].y))\)
对于在2窗口打饭的情况:\(dp[i][j]=min(dp[i][j],max(dp[i−1][j],s[i]−j+a[i].y))\)
那么最优解就是在 \(dp[n][1...s[n]]\)里面找了
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 3e2 + 5;
const int M = 4e4 + 5;
struct S {
int x, y;
} a[N];
int h1, h2, n, s[N], dp[N][M];
bool inline cmp(S a, S b) {
return a.y > b.y;
}
int inline min(int a, int b) {
return a < b ? a : b;
}
int inline max(int a, int b) {
return a > b ? a : b;
}
signed main() {
cin >> n;
for (rint i = 1; i <= n; i++)
cin >> a[i].x >> a[i].y;
memset(dp, 0x3f, sizeof dp);
sort(a + 1, a + n + 1, cmp);
for (rint i = 1; i <= n; i++) {
s[i] = s[i - 1] + a[i].x;
}
dp[0][0] = 0;
for (rint i = 1; i <= n; i++) {
for (rint j = 0; j <= s[i]; j++) {
dp[i][j] = min(dp[i][j], max(dp[i - 1][j], s[i] - j + a[i].y));
if (j - a[i].x >= 0)
dp[i][j] = min(dp[i][j], max(dp[i - 1][j - a[i].x], j + a[i].y));
}
}
int ans = 0x3f3f3f3f;
for (int i = 0; i <= s[n]; i++) {
ans = min(ans, dp[n][i]);
}
cout << ans << endl;
return 0;
}
T5 51NOD#.1522 上下序列
区间 dp
从放第一本书开始推,每次放两本相同高度的,设 $ f_{i,j}$ 为左边放 \(i\) 本书,右边放 \(j\) 本书的方案
如果合法 , \(f[i][j]+=f[i-2][j],f[i-1][j-1],f[i][j-2]\),
邻接表正反都存一遍
得到的结果要除以 \(3\) , 当放最后两本书时 , \(f[i-2][j]=f[i-1][j-1]=f[i][j-2]\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 4e2 + 5;
const int M = 8e2 + 5;
int n, f[N][N], idx, k, ans, h[N], ne[M], w[M], e[M];
void add(int a, int b, int c) {
ne[++idx] = h[a], w[idx] = b, e[idx] = c, h[a] = idx;
}
//建立邻接表
bool spfa(int l, int r, int ll, int rr) {
//寻找出边;
//l表示放的两本书中左边的那本,r是右边那本
//ll表示没有放书的区间的左端点,rr表示右端点
//可知 ll~rr 中的书高度都大于将要放的两本书
for (rint i = h[l]; i; i = ne[i]) {
int y = w[i];
int z = e[i];
if (y == r) {
if (z == 2 || z == 4) {
return 0;
}
}//等于的情况只可能:出边为放的另一本书的位置
else if (z <= 2) {
if (y < ll || y > rr) {
return 0;
}
}
else if (z >= 4) {
if (y > ll && y < rr) {
return 0;
}
}
else if (z == 3) {
return 0;
}
}//寻找与左端点有关的条件
for (rint i = h[r]; i; i = ne[i]) {
int y = w[i];
int z = e[i];
if (y == l) {
if (z == 2 || z == 4) {
return 0;
}
}
else if (z <= 2) {
if (y < ll || y > rr) {
return 0;
}
}
else if (z >= 4) {
if (y >= ll && y <= rr) {
return 0;
}
}
else if (z == 3) {
return 0;
}
}//寻找与右端点有关的条件
return 1;
}
signed main() {
cin >> n >> k;
for (rint i = 1; i <= k; i++) {
int x, y;
int z = 0;
string c;
cin >> x >> c >> y;
if (c[1] == '=') {
if (c[0] == '<') {
z = 1;
} else {
z = 5;
}
}
else {
if (c[0] == '=') {
z = 3;
}
if (c[0] == '<') {
z = 2;
}
if (c[0] == '>') {
z = 4;
}
}
if (x == y) {
if (z == 2 || z == 4) {
puts("0");
exit(0);
} else {
continue;
}
}
add(x, y, z);
add(y, x, 6 - z);
}
f[0][0] = 1;
for (rint i = 1; i <= n; i++) {
for (rint j = 0; j <= 2 * i; j++) {
if (j >= 2 && (k == 0 || spfa(j - 1, j, j + 1, 2 * n - 2 * i + j))) {
f[j][2 * i - j] += f[j - 2][2 * i - j];
}
if (j >= 1 && 2 * i - j >= 1 && (k == 0 || spfa(j, 2 * n - 2 * i + j + 1, j + 1, 2 * n - 2 * i + j))) {
f[j][2 * i - j] += f[j - 1][2 * i - j - 1];
}
if (2 * i - j >= 2 && (k == 0 ||
spfa(2 * n - 2 * i + j + 1, 2 * n - 2 * i + j + 2, j + 1, 2 * n - 2 * i + j))) {
f[j][2 * i - j] += f[j][2 * i - j - 2];
}
}
}
for (rint i = 0; i <= 2 * n; i++) {
ans += f[i][2 * n - i];
}
cout << ans / 3 << endl;
return 0;
}
T6 BZOJ#1084. [SCOI2005]最大子矩阵
\(m=1\) ,题目等于求的是一段,直接计算最大 \(m\) 子段和。
\(m=2\) , \(f[i][j][k]\) 表示扫描到第一列 \(i\) 和第 \(2\) 列 \(j\) 时选取了 \(k\) 个矩阵的答案。
有 \(3\) 种转移:第一列取一段,第二列取一段,两列一起取一个宽度为 \(2\) 的矩阵。
转移方程:
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e2 + 5;
const int M = 1e1 - 5;
int n, m, k, dp[N][M * 3], dp2[N][N][M * 3], a[N][M * 2], s[M][N];
int inline max(int a, int b) {
return a > b ? a : b;
}
signed main() {
cin >> n >> m >> k;
for (rint i = 1; i <= n; i++) {
for (rint j = 1; j <= m; j++) {
cin >> a[i][j];
}
s[1][i] = s[1][i - 1] + a[i][1];
s[2][i] = s[2][i - 1] + a[i][2];
}
if (m == 1) {
memset(dp, -0x3f, sizeof dp);
for (rint i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (rint i = 1; i <= n; i++) {
for (rint j = 1; j <= k; j++) {
dp[i][j] = dp[i - 1][j];
for (rint p = 0; p <= i - 1; p++) {
dp[i][j] = max(dp[i][j], dp[p][j - 1] + s[1][i] - s[1][p]);
}
}
}
cout << dp[n][k] << endl;
return 0;
}
memset(dp2, -0x3f, sizeof dp2);
for (rint i = 0; i <= n; i++) {
for (rint j = 0; j <= n; j++) {
dp2[i][j][0] = 0;
}
}
for (rint i = 1; i <= n; i++) {
for (rint j = 1; j <= n; j++) {
for (rint l = 1; l <= k; l++) {
dp2[i][j][l] = max(dp2[i][j - 1][l], dp2[i - 1][j][l]);
for (rint p = 0; p <= i - 1; p++) {
dp2[i][j][l] = max(dp2[i][j][l], dp2[p][j][l - 1] + s[1][i] - s[1][p]);
}
for (rint p = 0; p <= j - 1; p++) {
dp2[i][j][l] = max(dp2[i][j][l], dp2[i][p][l - 1] + s[2][j] - s[2][p]);
}
if (i == j) {
for (rint p = 0; p <= i - 1; p++) {
dp2[i][j][l] = max(dp2[i][j][l], dp2[p][p][l - 1] + s[1][i] - s[1][p] + s[2][i] - s[2][p]);
}
}
}
}
}
cout << dp2[n][n][k] << endl;
return 0;
}
T7 BZOJ#2748. [HAOI2012]音量调节
背包 dp
定义状态 \(dp[i][j]\) 表示到了第 \(j\) 次操作,能否达到 \(i\) 的音量。初始状态 \(dp[beginLevel][0]=1\)
状态转移方程:$dp[i+c[j]][j]=dp[i−c[j]][j]=1 $ $ $ $ $ $ $ \((dp[i][j−1]==1)\)
然后倒序找满足 \(dp[i][n]=1\) 的最大的 \(i\)。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 2e3 + 5;
const int M = 5e1 + 5;
int n, s, e, c[N], dp[N][M];
//s 为 beginLevel ; e 为 maxlevel。
signed main() {
cin >> n >> s >> e;
for (rint i = 1; i <= n; i++) {
cin >> c[i];
}
dp[s][0] = 1;
for (rint i = 1; i <= n; i++) {
for (rint j = 0; j <= e; j++) {
if (dp[j][i - 1] != 0) {
int p = j - c[i];
int q = j + c[i];
if (p >= 0) {
dp[p][i] = 1;
}
if (q <= e) {
dp[q][i] = 1;
}
}
}
}
for (rint i = e; i >= 0; i--) {
if (dp[i][n] != 0) {
cout << i;
return 0;
}
}
cout << -1 << endl;
return 0;
}
T8 BZOJ#1915. [USACO2010]奶牛的跳格子游戏
\(dp[i]\) 表示跳到 \(i\) 位置,\(i-1\) 是回来要跳的格子
格子肯定都是正数,我们不可能去跳负数,所以前缀和不能按照正常的情况计算,要分类讨论
使用单调队列来优化, 单调队列中比较 \(dp[j] - sum[j]\) 即可
转移方程为 \(ans = max(ans, dp[i] + s[min(n, i - 1 + m)] - s[i])\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 3e5 + 5;
int ans, n, m, dp[N], s[N], a[N], q[N];
int inline min(int a, int b) {
return a < b ? a : b;
}
int inline max(int a, int b) {
return a > b ? a : b;
}
signed main() {
cin >> n >> m;
for (rint i = 1; i <= n; i++) {
cin >> a[i];
if (a[i] > 0) {
s[i] = s[i - 1] + a[i];
} else {
s[i] = s[i - 1];
}
}
int p = min(n, m);
ans = s[p];
int h = 0;
int t = 0;
q[t++] = 1;
dp[1] = a[1];
for (rint i = 2, j = 0; i <= n; i++, j++) {
while (h < t && i - q[h] > m) {
h++;
}
int val = dp[j] - s[j];
while (h < t && val >= dp[q[t - 1]] - s[q[t - 1]]) {
t--;
}
q[t++] = j;
int x = q[h];
dp[i] = dp[x] + a[i] + a[i - 1] + s[i - 2] - s[x];
}
for (rint i = 1; i <= n; i++) {
ans = max(ans, dp[i] + s[min(n, i - 1 + m)] - s[i]);
}
cout << ans << endl;
return 0;
}
T9 P1131 [ZJOI2007] 时态同步
树形 dp
\(dp[i]\) 记录以 \(i\) 为根时的最大时间
首先一个边的循环递归,找出每个节点的最大时间,再进行一次循环,每一次 \(ans\) 加上当前节点的最大时间减去子节点加上边权,最后得出答案即可。
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 1e6 + 5;
int n, s, h[N], e[N], ne[N], w[N], idx, ans, maxn, p[N], tot;
bool v[N];
int inline max(int a, int b) {
return a > b ? a : b;
}
void add(int a, int b, int c) {
e[++idx] = b, ne[idx] = h[a], h[a] = idx, w[idx] = c;
}
void init(int x) {
v[x] = true;
for (rint i = h[x]; i; i = ne[i]) {
if (!v[e[i]]) {
init(e[i]);
p[x] = max(p[x], p[e[i]] + w[i]);
}
}
return ;
}
void dfs(int x) {
v[x] = false;
for (rint i = h[x]; i; i = ne[i]) {
if (v[e[i]]) {
dfs(e[i]);
ans += p[x] - p[e[i]] - w[i];
}
}
return ;
}
signed main() {
cin >> n >> s;
for (rint i = 1; i < n; i++) {
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
add(y, x, z);
}
init(s);
dfs(s);
cout << ans << endl;
return 0;
}
T10 UVA10559 方块消除
区间 dp + dfs
读入时预处理,将各个连续同色区间处理为一个点,记录颜色和长度
对于一个连续的同色区间,可以直接消掉,或者从左边或者右边搞到和它同色的区间和在一起再一起消掉
\(dp[i][j][k]\) 表示区间 \(i\) 到区间 \(j\) 且在区间 \(j\) 右边添加 \(k\) 个格子的最大分数
- 直接消除,即左边不考虑,为 \(dp[i][j-1][0]+(len[j]+k)^2\)
- 考虑左边,在 \(j\) 右边枚举 \(p\),且 \(color[j]==color[p]\) ,则为 \(ans=max(ans,dp(l,p,k+len[r])+dp(p+1,r-1,0))\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 2e2 + 5;
int c[N], dp[N][N][N], idx, len[N];
//c 表示颜色
int inline dfs(int l, int r, int k) {
int &res = dp[l][r][k];
if (res > -1) {
return res;
}
res = dfs(l, r - 1, 0) + (len[r] + k) * (len[r] + k);
for (rint p = l; p < r; p++) {
if (c[p] != c[r]) {
continue;
}
res = max(res, dfs(l, p, k + len[r]) + dfs(p + 1, r - 1, 0));
}
return res;
}
signed main() {
int T;
cin >> T;
int T1 = T;
while (T--) {
memset(dp, -1, sizeof dp);
memset(len, 0, sizeof len);
int L, a, l = 1;
cin >> L >> a;
idx = 0;
for (rint j = 2, b; j <= L; j++) {
cin >> b;
if (b == a) {
l++;
} else {
c[++idx] = a;
len[idx] = l;
l = 1;
a = b;
}
}
c[++idx] = a;
len[idx] = l;
for (rint j = 1; j <= idx; j++) {
dp[j][j][0] = (len[j]) * (len[j]);
dp[j][j - 1][0] = 0;
}
printf("Case %lld: %lld", T1 - T, dfs(1, idx, 0));
puts("");
}
return 0;
}