简单-dp
\(\text{hdu-1069}\)
给定 \(n\) 种尺寸为 \((x_i, y_i, z_i)\) 的积木,一个积木可以重新定位,以便其三个尺寸中的任意两个确定底部的尺寸,另一个尺寸是高度。每种积木都有无数个。
求这 \(n\) 种积木能堆叠的最大高度,使得上方的积木底面长宽严格小于下方的底面长宽。
\(1 \le n \le 30\)。
由于一个积木可以以三个面为底面,那么一种积木就可以看成三种使用。
之后就按类似最长上升子序列的思路 \(\text{dp}\) 即可。
\(\text{hdu-1087}\)
给定 \(n\) 个数 \(a_i\),可以从任意一个数开始跳,每次跳只能跳到后面大于当前位置的数。
求跳跃的最大价值,价值定义为跳过的所有数之和。
\(1 \le n \le 1000\)。
最长上升子序列,\(O(n^2)\) 跑 \(\text{dp}\)。
\(\text{hdu-1114}\)
给定空存钱罐的重量 \(e\) 和装满硬币后的重量 \(f\),和 \(n\) 种硬币的重量 \(v_i\) 和面值 \(w_i\)。
求存钱罐里的硬币价值最小值。
\(1 \le e \le f \le 10^4\),\(1 \le n \le 500\)。
完全背包板子。
完全背包和 \(01\) 背包唯一区别就是一件物品可以选无限个。
其实只需要把 \(01\) 背包的第二层改成正序转移就好了,其实就做到了选任意多个物品。
memset(dp, INF, sizeof dp), dp[0] = 0;
for(int i = 1; i <= n; i ++) for(int j = v[i]; j <= S; j ++)
dp[j] = min(dp[j], dp[j - v[i]] + w[i]);
\(\text{hdu-1176}\)
你在一个 \([0, 10]\) 范围的数轴上,初始 \(0\) 时刻时在 \(5\) 上。
有 \(n\) 个馅饼会在 \(t_i\) 时刻掉在 \(x_i\) 上,你每时刻只能移动一单位或者不动,求最多接到的馅饼数。
\(1 \le n,t_i \le 10^5\),\(0 \le x_i \le 10\)。
设 \(f_{i,j}\) 表示 \(i\) 时刻时在 \(j\) 上,\(i\) 时刻之后最多接到的馅饼数,那么:
其中 \(a_{i,j}\) 表示 \(i\) 时刻在 \(j\) 上会掉的馅饼数。
这样倒着转移即可,答案为 \(f_{0,5}\)。
\(\text{hdu-1260}\)
有 \(n\) 个人购票,可以每个人单独购票或者相邻两个人一起购票,若第 \(i\) 个人单独购票则花费 \(a_i\) 分钟,若和第 \(i-1\) 个人一起购票则一共花费 \(b_i\) 分钟。
求上午 \(8\) 点开始购票,最早结束的购票时间。
\(1 \le T \le 10\),\(1 \le n \le 2000\),\(0 \le a_i \le 25\),\(0 \le b_i \le 50\)。
设 \(f_i\) 表示处理完前 \(i\) 个人的最少时间,转移是简单的:
处理完 \(n\) 个人最小时间为 \(f_n\)。转化时间可以这样写,比较简洁:
int t = dp[n], h = 8 + t / 3600, m = t % 3600 / 60, s = t % 60;
if(h > 12) printf("%02d:%02d:%02d pm\n", h - 12, m, s);
else printf("%02d:%02d:%02d am\n", h, m, s);
\(\text{hdu-1160}\)
给定 \(n\) 个二元组 \((w_i, s_i)\),选出最长的序列满足 \(w_i < w_{i+1}\) 且 \(s_i > s_{i+1}\),输出长度和这个序列。
\(1 \le n \le 1000\),\(1 \le w_i,s_i \le 10^4\)。
实际上还是最长上升子序列,但是需要输出路径,有两种方法。
法一:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 10005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
struct node { long long w, s, id; } a[MAXN];
long long x, y, n, dp[MAXN];
bool cmp(node l, node r) {
if(l.w == r.w) return l.s > r.s;
return l.w < r.w;
}
int main() {
while(cin >> x >> y) a[++ n] = {x, y, n};
sort(a + 1, a + n + 1, cmp);
long long ans = 0;
for(int i = n; i >= 1; i --) {
dp[i] = 1;
for(int j = i; j <= n; j ++)
if(a[j].w > a[i].w && a[j].s < a[i].s)
dp[i] = max(dp[i], dp[j] + 1);
ans = max(ans, dp[i]);
}
cout << ans << "\n";
for(int i = 1; i <= n; i ++) if(ans > 0 && ans == dp[i])
cout << a[i].id << "\n", ans --;
return 0;
}
但感觉种方法不太好理解。
法二:
在转移时记录时从那个二元组转移过来的,这样就形成了一种链状结构。
就可以反向查找路径了。
\(\text{luogu-1441}\)
现有 \(n\) 个砝码,重量分别为 \(a_i\),在去掉 \(m\) 个砝码后,问最多能称量出多少不同的重量(不包括 \(0\))。
注意:砝码只能放在其中一边。
\(1 \le n \le 20\),\(1 \le m \le 4\),\(m < n\),\(1 \le a_i \le 100\)。
考虑二进制枚举,对于合法情况跑一遍 \(01\) 背包。
这个状态设计比较经典,所以记录一下。
设 \(f_j=0/1\) 表示处理完前 \(i\) 个砝码重量 \(j\) 能不能称出来,于是转移就很显然了,\(i = 1 \sim n\) 遍历:
最终 \(f_j=1\) 的个数就是答案,对于这题来说所有合法情况取 \(\max\) 即可。
可以用 \(\text{bitset}\) 优化。记得初始化 \(s_0=1\)。
#include<iostream>
#include<cstdio>
#include<bitset>
#include<algorithm>
using namespace std;
#define MAXN 2005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, m, a[MAXN], ans;
bitset<MAXN> s;
int main() {
n = read(), m = read();
for(int i = 0; i < n; i ++) a[i] = read();
for(int S = 0; S < (1 << n); S ++)
if(__builtin_popcount(S) == n - m) {
s.reset(), s[0] = 1;
for(int i = 0; i < n; i ++)
if(S & (1 << i)) s |= s << a[i];
ans = max(ans, (long long)s.count());
}
cout << ans - 1 << "\n";
return 0;
}
\(\text{uva-323}\)
给定两个长度为 \(n\) 的序列 \(a_i, b_i\),选择 \(m\) 个下标 \(i_1 < i_2 < \dots < i_m\),使得下式尽可能小:
若多个最小值,则取 \(\sum a_{i_j} + \sum b_{i_j}\) 较大的。
你需要输出 \(\sum a_{i_j}\)、\(\sum b_{i_j}\) 和选取的下标。
\(1 \le n \le 200\),\(1 \le m,a_i,b_i \le 20\),\(m \le n\)。
考虑按照上一题的设计方式。
设 \(f_{j,A,B}=0/1\) 表示处理完前 \(i\) 个元素,已经选出 \(j\) 个下标,选的和分别是 \(A,B\)。
初始化 \(f_{0,0,0}=1\),转移方程很显然:
之后枚举所有 \(f_{m,A,B}=1\) 的方案,找出 \(|A-B|\) 最小的方案即可。
但是这样的时间复杂度是 \(O(6.4 \times 10^8)\),显然是过不了的。
考虑另一个经典的转化,我们把 \(A-B\) 压进状态里,把 \(A+B\) 放值域里。
设 \(f_{j,k}=A+B\) 表示处理完前 \(i\) 个元素,已经选出 \(j\) 个下标,其中 \(k=A-B\)。
初始化 \(f_{0,0}=0\),记得倒序转移。转移方程就变成了这样:
答案即为 \(f_{m,k}\) 中 \(|k|\) 最小的。
然后这道题还要记录路径,所以需要记录每次转移是从哪里转移过来的。
记 \(d_{i,j,k}\) 表示处理完前 \(i\) 个人时,\(f_{j,k}\) 是从哪里转移过来的。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
#define MAXN 905
#define MAXM 205
#define MAXK 35
#define N 400
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, m, a[MAXN], b[MAXN], dp[MAXN][MAXN], d[MAXM][MAXK][MAXN];
long long cnt, ans, t, suma, sumb, p[MAXN];
void solve(long long i, long long j, long long k) {
if(!j) return;
long long lst = d[i][j][k + N];
solve(lst - 1, j - 1, k - (a[lst] - b[lst]));
p[++ t] = lst, suma += a[lst], sumb += b[lst];
return;
}
int main() {
while(true) {
n = read(), m = read();
if(!n && !m) break;
for(int i = 1; i <= n; i ++) a[i] = read(), b[i] = read();
memset(dp, -0x3f, sizeof dp), dp[0][N] = 0;
for(int i = 1; i <= n; i ++) {
for(int j = 0; j <= m; j ++) for(int k = -400; k <= 400; k ++)
d[i][j][k + N] = d[i - 1][j][k + N];
for(int j = m; j >= 1; j --) for(int k = -400; k <= 400; k ++) {
long long x = k - (a[i] - b[i]);
if(x < -400 || x > 400) continue;
if(dp[j - 1][x + N] + a[i] + b[i] > dp[j][k + N])
dp[j][k + N] = dp[j - 1][x + N] + a[i] + b[i], d[i][j][k + N] = i;
}
}
ans = 400;
for(int i = -400; i <= 400; i ++) if(dp[m][i + N] >= 0)
if(abs(i) < abs(ans) || (abs(i) == abs(ans) && dp[m][i + N] > dp[m][ans + N])) ans = i;
t = suma = sumb = 0, solve(n, m, ans);
cout << "Jury #" << (++ cnt) << "\n";
cout << "Best jury has value " << suma << " for prosecution and value " << sumb << " for defence:\n";
for(int i = 1; i <= t; i ++) cout << " " << p[i];
cout << "\n\n";
}
return 0;
}
\(\text{OpenJ_Bailian-1458}\)
求两个字符串 \(s,t\) 的最长公共子序列长度。
\(1 \le |s|,|t| \le 1000\)。
直接朴素 \(\text{dp}\) 即可,简单写一下。
设 \(f_{i,j}\) 表示分别以第 \(i,j\) 为结束的最长公共子序列长度,转移:
答案就是 \(f_{|s|,|t|}\)。
\(\text{luogu-2516}\)
给定两个字符串 \(s,t\),求这两个串的最长公共子序列长度和数量。
\(1 \le |s|,|t| \le 5000\)。
考虑在 \(\text{dp}\) 的过程中需要记录一个 \(g_{i,j}\) 表示以 \(i,j\) 为结尾的最长公共子序列的数量。
分两种情况转移 \(g\) 数组。(我们称 \(\text{lcs}\) 为”序列“)
- 当 \(s_i = t_j\) 时。
此时 \(s_i, t_j\) 不可能同时不在里序列,可能其中一个在序列里,或者都在。
因为 \(s_i = t_j\) 所以都在的情况一定存在,接下来看其中一个在的情况前提。
若只有 \(s_i\) 在序列里,那么此时 \(f_{i,j}=f_{i,j-1}\);同样的,若只有 \(t_j\) 在序列里,那么 \(f_{i,j}=f_{i-1,j}\)。
这样就可以得出 \(g_{i,j}\) 的转移:
- 当 \(s_i \ne t_j\) 时。
此时 \(s_i,t_j\) 就不可能同时在序列中了,考虑 \(f_{i,j}\) 从哪里转移过来。
若 \(f_{i-1,j} > f_{i,j-1}\),那么 \(t_j\) 一定在序列中,所以有 \(g_{i,j} \gets g_{i-1,j}\)。
若 \(f_{i-1,j} < f_{i,j-1}\),那么 \(s_i\) 一定在序列中,所以有 \(g_{i,j} \gets g_{i,j-1}\)。
重点是 \(f_{i-1,j} = f_{i,j-1}\) 的时候,此时要考虑 \(f_{i,j} = f_{i-1,j-1}\) 是否成立。
如果成立,说明 \(g_{i-1,j}\) 和 \(g_{i,j-1}\) 均包含了 \(g_{i-1,j-1}\) 这一部分,即 \(s_i,t_j\) 都不在序列中。
此时的转移需要减去算重的部分,\(g_{i,j} \gets g_{i-1,j}+g_{i,j-1}-g_{i-1,j-1}\)。
否则转移就是 \(g_{i,j} \gets g_{i-1,j}+g_{i,j-1}\)。
至此就分讨完了,直接转移即可。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define MAXN 5005
#define MOD 100000000
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long ls, lt, dp[2][MAXN], g[2][MAXN];
char s[MAXN], t[MAXN];
int main() {
cin >> (s + 1) >> (t + 1);
ls = strlen(s + 1) - 1, lt = strlen(t + 1) - 1;
for(int i = 0; i <= lt; i ++) g[0][i] = 1;
g[1][0] = 1;
for(int i = 1; i <= ls; i ++) for(int j = 1; j <= lt; j ++) {
long long x = i & 1, y = (i - 1) & 1;
if(s[i] == t[j]) {
dp[x][j] = dp[y][j - 1] + 1;
g[x][j] = g[y][j - 1] % MOD;
if(dp[x][j] == dp[y][j]) (g[x][j] += g[y][j]) %= MOD;
if(dp[x][j] == dp[x][j - 1]) (g[x][j] += g[x][j - 1]) %= MOD;
}
else {
dp[x][j] = max(dp[x][j - 1], dp[y][j]);
if(dp[y][j] > dp[x][j - 1]) g[x][j] = g[y][j] % MOD;
else if(dp[y][j] < dp[x][j - 1]) g[x][j] = g[x][j - 1] % MOD;
else if(dp[x][j] != dp[y][j - 1])
g[x][j] = (g[y][j] + g[x][j - 1]) % MOD;
else g[x][j] = ((g[x][j - 1] + g[y][j] -
g[y][j - 1]) % MOD + MOD) % MOD;
}
}
cout << dp[ls & 1][lt] << "\n" << g[ls & 1][lt] << "\n";
return 0;
}
\(\text{OpenJ_Bailian-1661}/\text{poj-1661}\)
场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。
\(\text{Jimmy}\) 在时刻 \(0\) 从高于所有平台的 \((x,y)\) 处开始下落,它的下落速度始终为 \(1m/s\)。当 \(\text{Jimmy}\) 落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是 \(1m/s\)。当 \(\text{Jimmy}\) 跑到平台的边缘时,开始继续下落。\(\text{Jimmy}\) 每次下落的高度不能超过 \(H\) 米,不然就会摔死,游戏也会结束。
设计一个程序,计算 \(\text{Jimmy}\) 到底地面时可能的最早时间。
\(0 \le T \le 20\),\(1 \le n \le 1000\),\(-20000 \le x,l_i,r_i \le 20000\),\(0 < h_i < y \le 20000\)。
设 \(f_{i,0/1}\) 表示从第 \(i\) 个平台左端/右端下落的最短时间。
很显然每个平台下落只会掉到一个平台上,或者不能下落,因此每个平台只能转移一次。
转移方程就是这样:
代码就很好写了:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAXN 1005
#define INF 0x3f3f3f3f3f3f3f3f
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
struct node { long long l, r, h; } a[MAXN];
long long T, n, x, y, maxn, dp[MAXN][2];
bool cmp(node x, node y) { return x.h > y.h; }
int main() {
T = read();
while(T --) {
n = read(), x = read(), y = read(), maxn = read();
a[0] = {x, x, y}, a[n + 1].h = 0;
for(int i = 1; i <= n; i ++)
a[i].l = read(), a[i].r = read(), a[i].h = read();
sort(a, a + n + 2, cmp);
for(int i = n + 1; i >= 0; i --) {
dp[i][0] = dp[i][1] = INF;
if(i == n) { dp[i][0] = dp[i][1] = a[i].h; continue; }
bool fg = false;
for(int j = i + 1; j <= n + 1; j ++) {
long long dis = a[i].h - a[j].h;
if(dis > maxn) break;
if(a[i].l >= a[j].l && a[i].l <= a[j].r) {
dp[i][0] = min(dp[j][0] + a[i].l - a[j].l,
dp[j][1] + a[j].r - a[i].l) + dis;
fg = true; break;
}
}
if(!fg && a[i].h <= maxn) dp[i][0] = a[i].h;
fg = false;
for(int j = i + 1; j <= n + 1; j ++) {
long long dis = a[i].h - a[j].h;
if(dis > maxn) break;
if(a[i].r >= a[j].l && a[i].r <= a[j].r) {
dp[i][1] = min(dp[j][0] + a[i].r - a[j].l,
dp[j][1] + a[j].r - a[i].r) + dis;
fg = true; break;
}
}
if(!fg && a[i].h <= maxn) dp[i][1] = a[i].h;
}
cout << min(dp[0][0], dp[0][1]) << "\n";
}
return 0;
}
\(\text{OpenJ_Bailian-2533}/\text{poj-2533}\)
求最长上升子序列的长度。
\(\text{luogu-2858}\)
给定长度为 \(n\) 的序列 \(a_i\),每次只能从两端取数,若 \(a_i\) 是第 \(x\) 个取出的,则贡献为 \(xa_i\)。
求取出所有数的最大贡献。
\(1 \le n \le 2000\),\(1 \le a_i \le 1000\)。
很典型的区间 \(\text{dp}\)。设 \(f_{l,r}\) 表示 \([l,r]\) 区间的最大贡献,转移很显然:
注意初始化 \(f_{i,i}=na_i\)。
\(\text{hdu-1078}\)
给定一个 \(n \times n\) 的矩阵 \(a_{i,j}\),初始时在 \(a_{1,1}\) 上,每次只能水平或垂直移动不超过 \(k\),且移动到的点权值必须严格大于当前点。经过的点的权值和为路径的贡献,求路径的贡献最大值。
\(1 \le n,k \le 100\)。
显然是 \(O(n^3)\) 的 \(\text{dp}\),考虑设 \(f_{i,j}\) 为从 \((i,j)\) 出发的路径贡献最大值,转移:
因为转移方式比较特殊,需要用记忆化搜索辅助转移:
#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 105
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, k, a[MAXN][MAXN], dp[MAXN][MAXN];
long long dx[] = { -1, 1, 0, 0 };
long long dy[] = { 0, 0, -1, 1 };
long long dfs(long long x, long long y) {
if(dp[x][y]) return dp[x][y];
long long res = 0;
for(int i = 1; i <= k; i ++) for(int j = 0; j < 4; j ++) {
long long px = x + dx[j] * i, py = y + dy[j] * i;
if(px < 1 || px > n || py < 1 || py > n || a[px][py] <= a[x][y]) continue;
res = max(res, dfs(px, py));
}
return dp[x][y] = res + a[x][y];
}
int main() {
while(cin >> n >> k) {
if(n == -1 && k == -1) break;
for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++)
cin >> a[i][j], dp[i][j] = 0;
cout << dfs(1, 1) << "\n";
}
return 0;
}
\(\text{hdu-2859}\)
给定一个 \(n \times n\) 的字符矩阵 \(s_{i,j}\),求对阵矩阵的最大边长。
对称矩阵定义为,关于”左下到右上“对角线对称的矩阵。
\(1 \le n \le 1000\),\(s_{i,j} \in [a, b, \dots, z]\)。
设 \(f_{i,j}\) 表示以 \((i,j)\) 为左下角的对称矩阵的最大边长,显然 \(f_{i,j}\) 可以从 \(f_{i-1,j+1}\) 转移过来。
也就是 \((i,j)\) 右上角那个点,如果扩展的一列一行完美匹配,则 \(f_{i,j}=f_{i-1,j+1}+1\)。
否则,\(f_{i,j}=p\),其中 \(p\) 表示扩展的最大匹配值,这个匹配值暴力跑就能过。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
#define MAXN 1005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, dp[MAXN][MAXN];
char s[MAXN][MAXN];
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
while(cin >> n) {
if(!n) break;
long long ans = 0;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++) cin >> s[i][j];
for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) {
dp[i][j] = 1;
if(i > 1 && j < n) {
long long k = 0;
while(k <= dp[i - 1][j + 1]
&& s[i - k][j] == s[i][j + k]) k ++;
dp[i][j] = k;
}
ans = max(ans, dp[i][j]);
}
cout << ans << "\n";
}
return 0;
}
实际上这个时间复杂度最劣是 \(O(n^3)\) 的,但能卡过去,大概是数据没有那种所有 \(s_{i,j}=a\) 这种情况。
\(\text{luogu-2889}\)
给定 \(m\) 个时间区间 \([l_i,r_i]\),若选定第 \(i\) 个区间,则有 \(w_i\) 的贡献,但是在此区间后 \(R\) 的时间内不能选其他区间。求选任意个区间的最大贡献。
\(1 \le n,w_i \le 10^6\),\(1 \le m \le 10^3\),\(1 \le l_i < r_i \le n\)。
很套路的题,设 \(f_i\) 表示以 \(i\) 为最后一个选定的时间区间的最大贡献。
当然是排完序之后的编号, 转移就是从之前的合法状态转移过来就好了:
初始化 \(f_i = w_i\),答案就是 \(\max f_i\)。
本文来自博客园,作者:So_noSlack,转载请注明原文链接:https://www.cnblogs.com/So-noSlack/p/19239411

浙公网安备 33010602011771号