AtCoder DP Contest
前言
whkzbl,来水谷。
本篇博客是作者整理的 AtCoder DP Contest 的题解,其中还有几题待补。
题单简介:dp(即动态规划)是 OI 中一种非常重要的算法,这份 AtCoder DP Contest 题单里面的
A.Frog 1
\texttt{Description}
-
有
n 个石头,第i\,(i\le n) 个石头的高度为h_i 。有一只青蛙一开始站在石头1 上,要跳到石头n 去。青蛙站在石头i 上时可以跳到石头i+1 或石头i+2 。青蛙从石头i 跳到石头j 需要花费|h_i-h_j| 的体力。求青蛙从石头1 跳到石头n 所需花费的最小体力值之和。 -
\texttt{Solution}
非常基础的动态规划了,设
这样转移考虑“当前状态转移给谁”,可以避免数组越界的问题,而且正确性可以保证。
初始化
时间复杂度和空间复杂度均为
\texttt{Code}
#include <bits/stdc++.h>
using namespace std;
#define N 100005
int n, f[N], h[N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", h + i);
}
memset(f, 0x3f, sizeof(f));
f[1] = 0;
for (int i = 1; i <= n; ++i) {
f[i + 1] = min(f[i + 1], f[i] + abs(h[i + 1] - h[i]));
f[i + 2] = min(f[i + 2], f[i] + abs(h[i + 2] - h[i]));
}
printf("%d", f[n]);
}
B.Frog 2
\texttt{Description}
-
有
n 个石头,第i\,(i\le n) 个石头的高度为h_i 。有一只青蛙一开始站在石头1 上,要跳到石头n 去。青蛙站在石头i 上时可以跳到石头i+1\sim i+k 。青蛙从石头i 跳到石头j 需要花费|h_i-h_j| 的体力。求青蛙从石头1 跳到石头n 所需花费的最小体力值之和。 -
\texttt{Solution}
类似于 A.Frog 1,非常基础的动态规划了,设
这样转移考虑“当前状态转移给谁”,可以避免数组越界的问题,而且正确性可以保证。这里不需要考虑上界,因为超过
初始化
时间复杂度为
\texttt{Code}
#include <bits/stdc++.h>
using namespace std;
#define N 200005
int n, f[N], h[N], k;
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) {
scanf("%d", h + i);
}
memset(f, 0x3f, sizeof(f));
f[1] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= k; ++j) {
f[i + j] = min(f[i + j], f[i] + abs(h[i] - h[i + j]));
}
}
printf("%d", f[n]);
}
C.Vacation
\texttt{Description}
-
太郎的暑假有
n 天,第i 天他可以选择做以下三种事情:- 游泳,获得
a_i 点幸福值。 - 捉虫,获得
b_i 点幸福值。 - 写作业,获得
c_i 点幸福值。
但他不能连续两天进行同一种活动,请求大郎能获得出幸福值总和的最大值。
- 游泳,获得
-
\texttt{Solution}
写作业居然有幸福值。
不难看出这题的状态为时间和事件,所以设
设
时间复杂度和空间复杂度均为
\texttt{Code}
#include <bits/stdc++.h>
using namespace std;
int a[3][100005], f[3][100005], n;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d%d%d", &a[0][i], &a[1][i], &a[2][i]);
}
f[0][1] = a[0][1];
f[1][1] = a[1][1];
f[2][1] = a[2][1];
for (int i = 2; i <= n; ++i) {
f[0][i] = max(f[1][i - 1], f[2][i - 1]) + a[0][i];
f[1][i] = max(f[0][i - 1], f[2][i - 1]) + a[1][i];
f[2][i] = max(f[0][i - 1], f[1][i - 1]) + a[2][i];
}
printf("%d\n",max({f[0][n], f[1][n], f[2][n]}));
}
D.Knapsack 1
\texttt{Description}
-
有
n 种物品,每种物品只有一个,第i 个物品有体积w_i 和价值v_i 。你有一个容量为m 体积的背包,问在不超过背包容积的情况下,所放物品的价值和最大是多少? -
\texttt{Solution}
01 背包模板题了,设
若当前体积
否则只能从前
时间复杂度和空间复杂度均为
不过可以用滚动数组将空间优化成
\texttt{Code}
不开 long long 见祖宗!
- naive 版本:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 105, M = 1e5 + 1;
int n, m, w[N], v[N], f[N][M];
signed main() {
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%lld%lld", w + i, v + i);
}
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
if (j < w[i]) {
f[i][j] = f[i - 1][j];
} else {
f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]);
}
}
}
printf("%lld", f[n][m]);
}
- 滚动数组优化版本:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 105, M = 1e5 + 1;
int n, m, w[N], v[N], f[M];
signed main() {
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%lld%lld", w + i, v + i);
}
for (int i = 1; i <= n; ++i) {
for (int j = m; j >= w[i]; --j) {//倒序
f[j] = max(f[j], f[j - w[i]] + v[i]);
//由于倒着循环以及 = 是从右到左执行,所以执行 max 语句时 f[j] 和 f[j - w[i]] 仍未被更新,是由 i - 1 时的状态转移的
//对于 j < w[i] 的部分,应该由 f[i - 1][j] 来转移,这里没去动它,它仍旧是 i - 1 时的状态
}
}
printf("%lld", f[m]);
}
E.Knapsack 2
\texttt{Description}
-
有
n 种物品,每种物品只有一个,第i 个物品有体积w_i 和价值v_i 。你有一个容量为m 体积的背包,问在不超过背包容积的情况下,所放物品的价值和最大是多少? -
\texttt{Solution}
题面与 D.Knapsack 1 类似,但是一看值域,
然而价值的值域很小,于是我们反过来想,对于取成同一种价值,显然体积越小越优,所以可以设
有初始化
最后从大到小枚举价值,如果取成该价值的最小体积不超过容积,则该价值为答案(因为最小的体积不超过,那么去成最小的体积就是一种合法的方案,否则最小的体积都超过容积了,其它方案肯定都超过容积了,故不是一个合法的解),因为是从大到小枚举,所以可以保证最优。
同理用滚动数组优化,做到时间复杂度为
\texttt{Code}
不开 long long 见祖宗!貌似可以不开。
这里只给出滚动数组优化版本:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 105, M = 1e5 + 5;
int f[M], v[N], w[N], n, m, s;
signed main() {
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%lld%lld", w + i, v + i);
s += v[i];
}
memset(f, 0x3f, sizeof(f));
f[0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = s; j >= v[i]; --j) {
f[j] = min(f[j], f[j - v[i]] + w[i]);
}
}
for (int j = s; ~j; --j) {
if (f[j] <= m) {
return !printf("%lld",j);
}
}
}
F.LCS
\texttt{Description}
-
给出字符串
s,t ,求s,t 的最长公共子序列,并输出任意一个解。 -
\texttt{Solution}
求最长公共子序列是比较模板的了,设
解释一下:
-
若
s_i=t_j ,那么显然将s_i 和t_j 对应是最优的,因为若s_i 或t_j 与j 或i 之前的字符对应,那么让它们与第t_j 或s_i 对应,不会影响答案最优。 -
若
s_i\ne t_j ,那么s_i 不能和t_j 对应,这两个字符可能和j 之前或i 之前的字符对应。
这样就可以推出上面那个方程,
关键在于构造方案。考虑倒着枚举
时间复杂度和空间复杂度均为
\texttt{Code}
#include<bits/stdc++.h>
using namespace std;
#define N 3005
char s[N], t[N], ans[N];
int f[N][N], n, m, k;
int main() {
scanf("%s%s", s+1, t+1);
n = strlen(s + 1);
m = strlen(t + 1);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
f[i][j] = s[i] == t[j] ? f[i - 1][j - 1] + 1 : max(f[i - 1][j], f[i][j - 1]);
}
}
while (n && m) {
if (s[n] == t[m]) {
ans[++k] = s[n--];
--m;
} else if (f[n][m] == f[n - 1][m]) {
--n;
} else {
--m;
}
}
for (int i = k; i; --i) {
putchar(ans[i]);
}
}