20230418 训练记录:dp
LCS
求两字符串 \(s, t\) 的最长公共子序列。
\(|s|, |t| \leq 3000\)
注意要求的不只是长度,如果要求长度就直接:
- 如果 \(s_i = t_j\):\(f_{i, j} = f_{i - 1, j - 1} + 1\)。
- 否则选其中一个,即 \(f_{i, j} = \max\{ f_{i, j - 1}, f_{i - 1, j} \}\)。
要得到 dp 转移过程,需要在更新的时候记录来源,这里将三种来源标记为 \(0 / 1 / 2\),最后从后往前跑即可。
展开代码
#include <bits/stdc++.h>
using ll = long long;
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
std::string s, t;
std::cin >> s >> t;
int n = s.size(), m = t.size();
std::vector f(n + 1, std::vector<int>(m + 1, 0));
std::vector pre(n + 1, std::vector<int>(m + 1, -1));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
f[i][j] = f[i - 1][j - 1] + 1;
pre[i][j] = 0;
} else {
int f1 = f[i - 1][j];
int f2 = f[i][j - 1];
if (f1 > f2) {
pre[i][j] = 1;
} else {
pre[i][j] = 2;
}
f[i][j] = std::max(f1, f2);
}
}
}
int i = n, j = m;
std::string ans;
while (~i && ~j) {
if (int p = pre[i][j]; p == 0) {
ans += s[i - 1];
i -= 1;
j -= 1;
} else if (p == 1) {
i -= 1;
} else if (p == 2) {
j -= 1;
} else {
break;
}
}
std::reverse(ans.begin(), ans.end());
std::cout << ans << '\n';
return 0;
}
Longest Path
DAG 求最长路。
\(n \leq 10^5; m \leq 10^5\)。
按拓扑排序更新。
展开代码
#include <bits/stdc++.h>
using ll = long long;
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n, m;
std::cin >> n >> m;
std::vector<std::vector<int>> g(n + 1);
std::vector<int> deg(n + 1);
for (int i = 0; i < m; i++) {
int x, y;
std::cin >> x >> y;
g[x].push_back(y);
deg[y] += 1;
}
std::queue<int> q;
std::vector<int> f(n + 1, 0);
for (int i = 1; i <= n; i++) if (!deg[i]) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto &v : g[u]) {
f[v] = f[u] + 1;
if (!--deg[v]) q.push(v);
}
}
std::cout << *std::max_element(f.begin(), f.end()) << '\n';
return 0;
}
Grid 1
有障碍的矩阵从左上走到右下的方案数。
根据落脚点是否为障碍从 \(f_{i, j}\) 转移到 \(f_{i, j + 1}, f_{i + 1, j}\)。
展开代码
#include <bits/stdc++.h>
using ll = long long;
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int h, w;
std::cin >> h >> w;
std::vector<std::string> g(h);
for (int i = 0; i < h; i++) {
std::cin >> g[i];
}
const int mod = 1000000007;
std::vector f(h, std::vector<ll>(w, 0));
f[0][0] = 1;
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
if (i + 1 < h && g[i + 1][j] != '#') {
f[i + 1][j] += f[i][j];
f[i + 1][j] %= mod;
}
if (j + 1 < w && g[i][j + 1] != '#') {
f[i][j + 1] += f[i][j];
f[i][j + 1] %= mod;
}
}
}
std::cout << f[h - 1][w - 1] % mod << '\n';
return 0;
}
Coins
给 \(n\) 个硬币,向上的概率分别为 \(p_i\)。问抛掷它们之后,得到向上的硬币多于向下的硬币的概率为多少?
\(n \leq 2999\)。
用 \(f_{i, j}\) 表示投掷前 \(i\) 个硬币后,有 \(j\) 个硬币朝上的概率,则有:
展开代码
#include <bits/stdc++.h>
using ll = long long;
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
std::vector<double> p(n);
for (auto &i : p) std::cin >> i;
std::vector f(n + 1, std::vector<double>(n + 1, 0));
f[0][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) {
f[i + 1][j] += f[i][j] * (1 - p[i]);
if (j + 1 <= n) {
f[i + 1][j + 1] += f[i][j] * p[i];
}
}
}
std::cout << std::fixed << std::setprecision(10);
std::cout << std::accumulate(f[n].begin() + ((n + 1) / 2), f[n].end(), 0.) << '\n';
return 0;
}
Sushi
旋转寿司,共 \(n\) 盘,每一盘有 \(1 - 3\) 个寿司,接下来每次操作,等概率转到了 \(i\) 这盘子,吃掉其中一个。问吃完所有寿司的期望操作次数。
\(n \leq 300\)。
注意到盘子是哪个并不重要,装多少才关键。若当前有 \(1 / 2 / 3\) 个寿司的盘子分别有 \(a / b / c\) 个,那么转到它们的概率分别为 \(\dfrac{a}{n} / \dfrac{b}{n} / \dfrac{c}{n}\),当然还有 \(1 - \dfrac{a + b + c}{n}\) 转到空盘子。即 \(f_{a, b, c}\) 表示当前局面到达 \(\{0, 0, 0\}\) 的期望步数,有:
整理可得,
由于是求期望,终点的值很好求(为 \(0\)),因此考虑逆推。用记忆化搜索的方式处理出上面的过程。
展开代码
#include <bits/stdc++.h>
using ll = long long;
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n;
std::cin >> n;
int a{}, b{}, c{};
std::vector<int> arr(n);
for (auto &i : arr) {
std::cin >> i;
(i == 1 ? a : i == 2 ? b : c) += 1;
}
const int maxn = 301;
std::vector f(maxn, std::vector(maxn, std::vector<double>(maxn, -1)));
std::cout << std::fixed << std::setprecision(10);
std::cout << [&, sol{[&](auto &&self, int a, int b, int c) -> double {
if (f[a][b][c] >= 0) return f[a][b][c];
if (!a && !b && !c) return f[a][b][c] = 0;
double ans = 1;
if (a >= 1) ans += 1. * a / n * self(self, a - 1, b, c);
if (b >= 1) ans += 1. * b / n * self(self, a + 1, b - 1, c);
if (c >= 1) ans += 1. * c / n * self(self, a, b + 1, c - 1);
return f[a][b][c] = ans * n / (a + b + c);
}}]{
return sol(sol, a, b, c);
}() << '\n';
return 0;
}
Stones
Alice 和 Bob 正在玩游戏,面前有 \(k\) 个石子,现在要求轮流操作,每次操作必定是给定的某个 \(a_i\)。问谁胜。
\(n \leq 100; k \leq 10^5\)
用 \(f_{i, 0 / 1}\) 表示剩下 \(i\) 个石子时,先手/后手能否赢。有:
定义 必胜状态 为 先手必胜的状态,必败状态 为 先手必败的状态。
通过推理[1],我们可以得出下面三条定理:
定理 1:没有后继状态的状态是必败状态。
定理 2:一个状态是必胜状态当且仅当存在至少一个必败状态为它的后继状态。
定理 3:一个状态是必败状态当且仅当它的所有后继状态均为必胜状态。
即 \(f_{i, 0} = 1\) 当且仅当前面的任一 \(f_{i - x, 1} = 0, (\{x \leq i | x \in \{A\}\})\)。
展开代码
#include <bits/stdc++.h>
using ll = long long;
int main() {
std::cin.tie(nullptr)->sync_with_stdio(false);
int n, k;
std::cin >> n >> k;
std::vector<int> a(n);
for (int &i : a) std::cin >> i;
std::vector f(k + 1, std::array<int, 2>{});
f[0][0] = f[0][1] = false;
for (int i = 1; i <= k; i++) {
for (int j = 0; j < n; j++) if (i - a[j] >= 0) {
f[i][0] |= !f[i - a[j]][1];
f[i][1] |= !f[i - a[j]][0];
}
}
std::cout << (f[k][0] ? "First" : "Second") << '\n';
return 0;
}