AT_dp_a
很简单的一道。
因为每一个位置都可以由它前两个位置得到,所以很容易想到状态转移为
f
i
=
min
(
f
i
−
1
+
∣
a
i
−
a
i
−
1
∣
,
f
i
−
2
+
∣
a
i
−
a
i
−
2
∣
)
f_i=\min(f_{i-1}+\left\vert a_i-a_{i-1}\right\vert,f_{i-2}+\left\vert a_i-a_{i-2}\right\vert)
fi=min(fi−1+∣ai−ai−1∣,fi−2+∣ai−ai−2∣)
其中
f
i
f_i
fi 表示跳到第
i
i
i 个位置的最小花费。
需要注意的是,当
f
i
>
1
f_i > 1
fi>1 时才能与
f
i
−
1
f_{i - 1}
fi−1 比较,不然会越界。
同理,当
f
i
>
2
f_i > 2
fi>2 时才能与
f
i
−
2
f_{i - 2}
fi−2 比较。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n;
int a[maxn];
int f[maxn];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
memset(f, 0x3f, sizeof f);
f[1] = 0;
for (int i = 1; i <= n; ++i) {
if (i > 1) f[i] = min(f[i - 1] + abs(a[i] - a[i - 1]), f[i]);
if (i > 2) f[i] = min(f[i - 2] + abs(a[i] - a[i - 2]), f[i]);
}
cout << f[n];
}
AT_dp_b
和上一题很像,只不过每次要取后 k k k 个。
则状态转移方程可以写成
∑ j = 1 k f i + j = min ( f i + j , f i + ∣ a i − a i + j ∣ ) \sum _{j=1}^{k} f_{i+j}=\min(f_{i+j},f_i+\left\vert a_i-a_{i+j}\right\vert) j=1∑kfi+j=min(fi+j,fi+∣ai−ai+j∣)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n, k;
int a[maxn];
int f[maxn];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[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(a[i] - a[i + j]));
}
}
cout << f[n];
}
AT_dp_c
对于每一天,我们可以从三种活动中任选一个前一天没做过的活动,则
{ f i , 0 = max ( f i − 1 , 1 , f i − 1 , 2 ) + a i f i , 1 = max ( f i − 1 , 0 , f i − 1 , 2 ) + b i f i , 2 = max ( f i − 1 , 0 , f i − 1 , 1 ) + c i \begin{cases} f_{i,0}=\max(f_{i-1,1},f_{i-1,2})+a_i \\ f_{i,1}=\max(f_{i-1,0},f_{i-1,2})+b_i \\ f_{i,2}=\max(f_{i-1,0},f_{i-1,1})+c_i \end{cases} ⎩ ⎨ ⎧fi,0=max(fi−1,1,fi−1,2)+aifi,1=max(fi−1,0,fi−1,2)+bifi,2=max(fi−1,0,fi−1,1)+ci
其中 f i , 0 , f i , 1 , f i , 2 f_{i,0},f_{i,1},f_{i,2} fi,0,fi,1,fi,2 分别表示第 i i i 天完第 1 , 2 , 3 1,2,3 1,2,3 中活动所获得的最大幸福值。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n;
int a[maxn], b[maxn], c[maxn];
int f[maxn][3];
signed main(){
cin>> n;
for (int i = 1; i <= n; ++i) scanf("%d%d%d", &a[i], &b[i], &c[i]);
for (int i = 1; i <= n; ++i) {
f[i][0] = max(f[i - 1][1], f[i - 1][2]) + a[i];
f[i][1] = max(f[i - 1][0], f[i - 1][2]) + b[i];
f[i][2] = max(f[i - 1][0], f[i - 1][1]) + c[i];
}
cout << max({f[n][0], f[n][1], f[n][2]});
}
AT_dp_d
01背包裸模板,顺便复习下。
首先,定义 f i , j f_{i,j} fi,j 表示取前 i i i 个物品并用 j j j 的花费所能得到的最大价值。
那对于 f i , j f_{i,j} fi,j 有两种情况需要考虑:
-
选择第 i i i 件物品,那么花费了 j − t [ i ] j - t[i] j−t[i],剩下 i − 1 i-1 i−1 个物品,价值为 f i − 1 , j − t [ i ] + w i f_{i-1,j-t[i]}+w_i fi−1,j−t[i]+wi
-
不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi−1,j
然后我们再压缩一维,并改变循环条件。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn = 1e5 + 10;
int T, M;
int t[maxn], w[maxn];
long long f[maxn];
signed main() {
cin >> M >> T;
for (int i = 1; i <= M; ++i) scanf("%d%d", &t[i], &w[i]);
for (int i = 1; i <= M; ++i)
for (int j = T; j >= t[i]; --j)
f[j] = max(f[j], f[j - t[i]] + w[i]);
cout << f[T] << endl;
}
AT_dp_e
这一题是上一题的加强版,只是改变了数据范围,背包容量从 1 0 5 10^5 105 变成了 1 0 9 10^9 109,但是物品的价值范围从 1 0 9 10^9 109 变成了 1 0 3 10^3 103。 这时候再用上一题枚举花费的话就会超时。
看到物品的个数 n n n 最大是 100 100 100 也就是说如果全部物品都取完了能得到的最大价值也就是 1 0 6 10^6 106。所以我们考虑枚举价值。
这时候我们的 f i , j f_{i,j} fi,j 表示的就是拿前 i i i 个物品取到价值为 j j j 的最小花费。
那对于 f i , j f_{i,j} fi,j 依旧是有两种情况需要考虑:
-
选择第 i i i 件物品,那么还能取了 j − w [ i ] j - w[i] j−w[i] 的价值,剩下 i − 1 i-1 i−1 个物品,价值为 f i − 1 , j − w [ i ] + t i f_{i-1,j-w[i]}+t_i fi−1,j−w[i]+ti
-
不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi−1,j
同样的,压一维,循环条件随之改变。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn = 1e5 + 10;
int T, M;
int t[maxn], w[maxn];
long long f[maxn];
signed main() {
cin >> M >> T;
int MM = M * 1000;
memset(f, 0x3f, sizeof f);
f[0] = 0;
for (int i = 1; i <= M; ++i) scanf("%d%d", &t[i], &w[i]);
for (int i = 1; i <= M; ++i)
for (int j = MM; j >= w[i]; --j)
f[j] = min(f[j], f[j - w[i]] + t[i]);
for (int i = MM; i; --i){
if (f[i] <= T) {
cout << i;
return 0;
}
}
}
AT_dp_f
求两个字符串的最长公共子序列(不需要连续)。
其实,求最长公共子序列的长度十分简单。下面给出思路和代码。
设两个字符串分别为
s
,
t
s,t
s,t(下标从
1
1
1 开始)。
对于
s
i
s_i
si 和
t
j
t_j
tj,有两种情况:
f i , j = { f i − 1 , j − 1 + 1 s i = t j max ( f i − 1 , j , f i , j − 1 ) s i ≠ t j f_{i,j}=\begin{cases} f_{i-1,j-1}+1&s_i=t_j \\ \max(f_{i-1,j},f_{i,j-1})&s_i\ne t_j\end{cases} fi,j={fi−1,j−1+1max(fi−1,j,fi,j−1)si=tjsi=tj
其中 f i , j f_{i,j} fi,j 表示 s s s 的前 i i i 个字符组成的字符串与 t t t 的前 j j j 个字符组成的字符串的最长公共子序列。
很明显,一开始所有的 f i , j f_{i,j} fi,j 都应为 0 0 0。
得出的 f i , j f_{i,j} fi,j 就是最终的序列的长度。
那么已知长度求序列,我们可以从后往前推。
-
当 s i = t j s_i=t_j si=tj 时, a n s f i , j = s i ( t j ) , i = i − 1 , j = j − 1 ans_{f_{i,j}}=s_i(t_j),i=i-1,j=j-1 ansfi,j=si(tj),i=i−1,j=j−1
-
当 s i ≠ t j s_i\ne t_j si=tj 时, { i = i − 1 f i − 1 , j > f i , j − 1 j = j − 1 f i − 1 , j < f i , j − 1 \begin{cases} i=i-1&f_{i-1,j}>f_{i,j-1} \\ j = j - 1&f_{i-1,j}<f_{i,j-1}\end{cases} {i=i−1j=j−1fi−1,j>fi,j−1fi−1,j<fi,j−1
直到 f i , j = 0 f_{i,j}=0 fi,j=0 时结束。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e3 + 10;
char s[maxn], t[maxn], ans[maxn];
int f[maxn][maxn];
signed main() {
scanf("%s%s", s + 1, t + 1);
int n = strlen(s + 1), m = strlen(t + 1);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (s[i] == t[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i][j - 1], f[i - 1][j]);
}
}
int i = n, j = m;
while (f[i][j]) {
if (s[i] == t[j]) ans[f[i][j]] = s[i], --i, --j;
else if (f[i - 1][j] > f[i][j - 1]) --i;
else --j;
}
for (int i = 1; i <= f[n][m]; ++i) printf("%c", ans[i]);
}
AT_dp_g
求有向无环图上的最长路长度。
考虑记忆化搜索每一个点。
f i f_i fi 表示从第 i i i 个点开始的最长路长度。
状态转移方程就是:
f i = max ( f i , f j + 1 ) f_i=\max(f_i,f_j+1) fi=max(fi,fj+1) 其中 i i i 到 j j j 有一条有向边。
而每一个点都将只遍历一次。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n, m, ans = 0;
int f[maxn];
vector<int> e[maxn];
int dfs(int u) {
if (f[u]) return f[u];
for (auto v: e[u]) f[u] = max(f[u], dfs(v) + 1);
return f[u];
}
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, u, v; i <= m; ++i) cin >> u >> v, e[u].push_back(v);
for (int i = 1; i <= n; ++i) ans = max(ans, dfs(i));
cout << ans << '\n';
}
AT_dp_h
f i , j f_{i,j} fi,j 表示走到第 ( i , j ) (i,j) (i,j) 个点所获得的最大价值。
如果当前的点是 ‘.’,那么可以走。则状态转移方程为
f i , j = ( f i − 1 , j + f i , j − 1 ) m o d ( 1 0 9 + 7 ) ( c i , j = ′ . ′ ) f_{i,j}=(f_{i-1,j}+f_{i,j-1})\bmod (10^9+7) (c_{i,j}='.') fi,j=(fi−1,j+fi,j−1)mod(109+7)(ci,j=′.′)
注意初始 f 1 , 1 = 1 f_{1,1}=1 f1,1=1
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e3 + 10, mod = 1e9 + 7;
int n, m;
char x;
int f[maxn][maxn];
signed main() {
cin >> n >> m;
f[1][1] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> x;
if (i == 1 && j == 1) continue;
if (x == '.') f[i][j] = (f[i - 1][j] + f[i][j - 1]) % mod;
}
}
cout << f[n][m] << '\n';
}
AT_dp_i
题目要求“面朝上的银币数比反面朝上的银币数多”。
很容易想到 f i , j f_{i,j} fi,j 表示前 i i i 个中有 j j j 个朝上的概率,那么朝下的概率就是 1 − f i , j 1-f_{i,j} 1−fi,j。
那么分类讨论正反面的概率再求和,即:
f i , j = f i − 1 , j − 1 × a i + f i − 1 , j × ( 1 − a i ) f_{i,j}=f_{i-1,j-1}×a_i +f_{i-1,j}×(1-a_i) fi,j=fi−1,j−1×ai+fi−1,j×(1−ai)
边界条件为 f 0 , 0 = 1 f_{0,0}=1 f0,0=1,每次往后推前计算 f i , 0 = f i − 1 , 0 × ( 1 − a i ) f_{i,0}=f_{i-1,0}×(1-a_i) fi,0=fi−1,0×(1−ai) 计算前 i i i 个全部朝下的概率。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e3 + 10;
int n;
double ans, f[maxn][maxn], a[maxn];
signed main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
f[0][0] = 1;
for (int i = 1; i <= n; ++i) {
f[i][0] = f[i - 1][0] * (1 - a[i]);
for (int j = 1; j <= n; ++j)
f[i][j] = f[i - 1][j - 1] * a[i] + f[i - 1][j] * (1 - a[i]);
}
for (int i = 1; i <= n; ++i) if (n < 2 * i) ans += f[n][i];
printf("%.10lf\n", ans);
}
AT_dp_j
首先,注意到 1 ≤ a i ≤ 3 1≤a_i≤3 1≤ai≤3 。所以考虑三维 dp。
f i , j , k = n i + j + k + i i + j + k f i − 1 , j , k + j i + j + k f i + 1 , j − 1 , k + k i + j + k f i , j + 1 , k − 1 f_{i,j,k}=\dfrac{n}{i+j+k}+\dfrac{i}{i+j+k}f_{i-1,j,k}+\dfrac{j}{i+j+k}f_{i+1,j-1,k}+\dfrac{k}{i+j+k}f_{i,j+1,k-1} fi,j,k=i+j+kn+i+j+kifi−1,j,k+i+j+kjfi+1,j−1,k+i+j+kkfi,j+1,k−1
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e2 + 10;
int n;
int a[4];
double f[maxn][maxn][maxn];
signed main() {
cin >> n;
for (int i = 1, x; i <= n; ++i) {
cin >> x;
a[x]++;
}
for (int k = 0; k <= n; ++k) {
for (int j = 0; j <= n; ++j) {
for (int i = 0; i <= n; ++i) {
if (!i && !j && !k) continue;
int t = i + j + k;
if (i) f[i][j][k] += f[i - 1][j][k] * i / t;
if (j) f[i][j][k] += f[i + 1][j - 1][k] * j / t;
if (k) f[i][j][k] += f[i][j + 1][k - 1] * k / t;
f[i][j][k] += (double)n / t;
}
}
}
printf("%.15lf\n", f[a[1]][a[2]][a[3]]);
}
AT_dp_k
f i f_i fi 表示剩余 i i i 块石头时,当前这个人的胜负情况。
显然, f i = f i − a j f_i=f_{i-a_j} fi=fi−aj,而当 i − a j = 0 i-a_j=0 i−aj=0 时,当前这个人就输了。
所以列出状态转移方程
f i = { i − 1 a j ≥ 0 f i − a j = 0 i a j ≤ 0 f i − a j ≠ 0 f_i=\begin{cases} i-1&a_j\ge 0&f_{i-a_j}=0 \\ i&a_j\le 0&f_{i-a_j}\ne0\end{cases} fi={i−1iaj≥0aj≤0fi−aj=0fi−aj=0
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n, k;
int a[maxn];
bool f[maxn];
signed main() {
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= k; ++i) {
for (int j = 1; j <= n; ++j) {
f[i] += i - a[j] >= 0 && !f[i - a[j]];
}
}
cout << (f[k] ? "First" : "Second");
}
AT_dp_l
明显地,是 d p dp dp 并且是区间 d p dp dp。
用 s i s_i si 作前缀和。
f i , j f_{i,j} fi,j 表示目前剩下 a l ∼ a r a_l ∼ a_r al∼ar,当前取数的人取数所能获得的最大数字和。
列出状态转移方程
f i , j = max ( s r − s l − 1 − f l + 1 , r , s r − s l − 1 − f l , r − 1 ) f_{i,j}=\max(s_r-s_{l-1}-f_{l+1,r},s_r-s_{l-1}-f_{l,r-1}) fi,j=max(sr−sl−1−fl+1,r,sr−sl−1−fl,r−1)
边界条件
f i , i = a i ( 1 ≤ i ≤ n ) f_{i,i}=a_i(1\le i \le n) fi,i=ai(1≤i≤n)
#include <iostream>
#define int long long
using namespace std;
const int maxn = 3e3 + 10;
int n;
int a[maxn], s[maxn];
int f[maxn][maxn];
signed main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i], f[i][i] = a[i], s[i] = s[i - 1] + a[i];
for (int len = 2; len <= n; ++len) {
for (int l = 1, r = l + len - 1; r <= n; ++l, ++r)
f[l][r] = max(s[r] - s[l - 1] - f[l + 1][r], s[r] - s[l - 1] - f[l][r - 1]);
}
cout << f[1][n] * 2 - s[n];
}
AT_dp_m
f i , j f_{i,j} fi,j 表示前 i i i 堆共分 j j j 个糖果的方案数。
状态转移方程为
f i , j = s i − 1 , j − s i − 1 , j − a i − 1 f_{i,j}=s_{i-1,j}-s_{i-1,j}-a_{i-1} fi,j=si−1,j−si−1,j−ai−1
e…因为 f i f_i fi 只跟 f i − 1 f_{i-1} fi−1 有关,所以可以去掉一维。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e5 + 10, mod = 1e9 + 7;
int n, k, a[110];
int s[maxn],f[maxn];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[i];
f[0] = 1;
for (int i = 1; i <= n; ++i) {
s[0] = f[0];
for (int j = 1; j <= k; ++j) s[j] = (s[j - 1] + f[j]) % mod;
for (int j = 0; j <= k; ++j) f[j] = ((s[j] - (j <= a[i] ? 0 : s[j - a[i] - 1])) % mod + mod) % mod;
}
cout << f[k] << '\n';
}
```[附题单](https://www.luogu.com.cn/training/492477)
[AT_dp_a](https://www.luogu.com.cn/problem/AT_dp_a)
-
很简单的一道。
因为每一个位置都可以由它前两个位置得到,所以很容易想到状态转移为
$$
f_i=\min(f_{i-1}+\left\vert a_i-a_{i-1}\right\vert,f_{i-2}+\left\vert a_i-a_{i-2}\right\vert)
$$
其中 $f_i$ 表示跳到第 $i$ 个位置的最小花费。
需要注意的是,当 $f_i > 1$ 时才能与 $f_{i - 1}$ 比较,不然会越界。
同理,当 $f_i > 2$ 时才能与 $f_{i - 2}$ 比较。
```cpp
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n;
int a[maxn];
int f[maxn];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
memset(f, 0x3f, sizeof f);
f[1] = 0;
for (int i = 1; i <= n; ++i) {
if (i > 1) f[i] = min(f[i - 1] + abs(a[i] - a[i - 1]), f[i]);
if (i > 2) f[i] = min(f[i - 2] + abs(a[i] - a[i - 2]), f[i]);
}
cout << f[n];
}
AT_dp_b
和上一题很像,只不过每次要取后 k k k 个。
则状态转移方程可以写成
∑ j = 1 k f i + j = min ( f i + j , f i + ∣ a i − a i + j ∣ ) \sum _{j=1}^{k} f_{i+j}=\min(f_{i+j},f_i+\left\vert a_i-a_{i+j}\right\vert) j=1∑kfi+j=min(fi+j,fi+∣ai−ai+j∣)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n, k;
int a[maxn];
int f[maxn];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[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(a[i] - a[i + j]));
}
}
cout << f[n];
}
AT_dp_c
对于每一天,我们可以从三种活动中任选一个前一天没做过的活动,则
{ f i , 0 = max ( f i − 1 , 1 , f i − 1 , 2 ) + a i f i , 1 = max ( f i − 1 , 0 , f i − 1 , 2 ) + b i f i , 2 = max ( f i − 1 , 0 , f i − 1 , 1 ) + c i \begin{cases} f_{i,0}=\max(f_{i-1,1},f_{i-1,2})+a_i \\ f_{i,1}=\max(f_{i-1,0},f_{i-1,2})+b_i \\ f_{i,2}=\max(f_{i-1,0},f_{i-1,1})+c_i \end{cases} ⎩ ⎨ ⎧fi,0=max(fi−1,1,fi−1,2)+aifi,1=max(fi−1,0,fi−1,2)+bifi,2=max(fi−1,0,fi−1,1)+ci
其中 f i , 0 , f i , 1 , f i , 2 f_{i,0},f_{i,1},f_{i,2} fi,0,fi,1,fi,2 分别表示第 i i i 天完第 1 , 2 , 3 1,2,3 1,2,3 中活动所获得的最大幸福值。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n;
int a[maxn], b[maxn], c[maxn];
int f[maxn][3];
signed main(){
cin>> n;
for (int i = 1; i <= n; ++i) scanf("%d%d%d", &a[i], &b[i], &c[i]);
for (int i = 1; i <= n; ++i) {
f[i][0] = max(f[i - 1][1], f[i - 1][2]) + a[i];
f[i][1] = max(f[i - 1][0], f[i - 1][2]) + b[i];
f[i][2] = max(f[i - 1][0], f[i - 1][1]) + c[i];
}
cout << max({f[n][0], f[n][1], f[n][2]});
}
AT_dp_d
01背包裸模板,顺便复习下。
首先,定义 f i , j f_{i,j} fi,j 表示取前 i i i 个物品并用 j j j 的花费所能得到的最大价值。
那对于 f i , j f_{i,j} fi,j 有两种情况需要考虑:
-
选择第 i i i 件物品,那么花费了 j − t [ i ] j - t[i] j−t[i],剩下 i − 1 i-1 i−1 个物品,价值为 f i − 1 , j − t [ i ] + w i f_{i-1,j-t[i]}+w_i fi−1,j−t[i]+wi
-
不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi−1,j
然后我们再压缩一维,并改变循环条件。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn = 1e5 + 10;
int T, M;
int t[maxn], w[maxn];
long long f[maxn];
signed main() {
cin >> M >> T;
for (int i = 1; i <= M; ++i) scanf("%d%d", &t[i], &w[i]);
for (int i = 1; i <= M; ++i)
for (int j = T; j >= t[i]; --j)
f[j] = max(f[j], f[j - t[i]] + w[i]);
cout << f[T] << endl;
}
AT_dp_e
这一题是上一题的加强版,只是改变了数据范围,背包容量从 1 0 5 10^5 105 变成了 1 0 9 10^9 109,但是物品的价值范围从 1 0 9 10^9 109 变成了 1 0 3 10^3 103。 这时候再用上一题枚举花费的话就会超时。
看到物品的个数 n n n 最大是 100 100 100 也就是说如果全部物品都取完了能得到的最大价值也就是 1 0 6 10^6 106。所以我们考虑枚举价值。
这时候我们的 f i , j f_{i,j} fi,j 表示的就是拿前 i i i 个物品取到价值为 j j j 的最小花费。
那对于 f i , j f_{i,j} fi,j 依旧是有两种情况需要考虑:
-
选择第 i i i 件物品,那么还能取了 j − w [ i ] j - w[i] j−w[i] 的价值,剩下 i − 1 i-1 i−1 个物品,价值为 f i − 1 , j − w [ i ] + t i f_{i-1,j-w[i]}+t_i fi−1,j−w[i]+ti
-
不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi−1,j
同样的,压一维,循环条件随之改变。
#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn = 1e5 + 10;
int T, M;
int t[maxn], w[maxn];
long long f[maxn];
signed main() {
cin >> M >> T;
int MM = M * 1000;
memset(f, 0x3f, sizeof f);
f[0] = 0;
for (int i = 1; i <= M; ++i) scanf("%d%d", &t[i], &w[i]);
for (int i = 1; i <= M; ++i)
for (int j = MM; j >= w[i]; --j)
f[j] = min(f[j], f[j - w[i]] + t[i]);
for (int i = MM; i; --i){
if (f[i] <= T) {
cout << i;
return 0;
}
}
}
AT_dp_f
求两个字符串的最长公共子序列(不需要连续)。
其实,求最长公共子序列的长度十分简单。下面给出思路和代码。
设两个字符串分别为
s
,
t
s,t
s,t(下标从
1
1
1 开始)。
对于
s
i
s_i
si 和
t
j
t_j
tj,有两种情况:
f i , j = { f i − 1 , j − 1 + 1 s i = t j max ( f i − 1 , j , f i , j − 1 ) s i ≠ t j f_{i,j}=\begin{cases} f_{i-1,j-1}+1&s_i=t_j \\ \max(f_{i-1,j},f_{i,j-1})&s_i\ne t_j\end{cases} fi,j={fi−1,j−1+1max(fi−1,j,fi,j−1)si=tjsi=tj
其中 f i , j f_{i,j} fi,j 表示 s s s 的前 i i i 个字符组成的字符串与 t t t 的前 j j j 个字符组成的字符串的最长公共子序列。
很明显,一开始所有的 f i , j f_{i,j} fi,j 都应为 0 0 0。
得出的 f i , j f_{i,j} fi,j 就是最终的序列的长度。
那么已知长度求序列,我们可以从后往前推。
-
当 s i = t j s_i=t_j si=tj 时, a n s f i , j = s i ( t j ) , i = i − 1 , j = j − 1 ans_{f_{i,j}}=s_i(t_j),i=i-1,j=j-1 ansfi,j=si(tj),i=i−1,j=j−1
-
当 s i ≠ t j s_i\ne t_j si=tj 时, { i = i − 1 f i − 1 , j > f i , j − 1 j = j − 1 f i − 1 , j < f i , j − 1 \begin{cases} i=i-1&f_{i-1,j}>f_{i,j-1} \\ j = j - 1&f_{i-1,j}<f_{i,j-1}\end{cases} {i=i−1j=j−1fi−1,j>fi,j−1fi−1,j<fi,j−1
直到 f i , j = 0 f_{i,j}=0 fi,j=0 时结束。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e3 + 10;
char s[maxn], t[maxn], ans[maxn];
int f[maxn][maxn];
signed main() {
scanf("%s%s", s + 1, t + 1);
int n = strlen(s + 1), m = strlen(t + 1);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (s[i] == t[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i][j - 1], f[i - 1][j]);
}
}
int i = n, j = m;
while (f[i][j]) {
if (s[i] == t[j]) ans[f[i][j]] = s[i], --i, --j;
else if (f[i - 1][j] > f[i][j - 1]) --i;
else --j;
}
for (int i = 1; i <= f[n][m]; ++i) printf("%c", ans[i]);
}
AT_dp_g
求有向无环图上的最长路长度。
考虑记忆化搜索每一个点。
f i f_i fi 表示从第 i i i 个点开始的最长路长度。
状态转移方程就是:
f i = max ( f i , f j + 1 ) f_i=\max(f_i,f_j+1) fi=max(fi,fj+1) 其中 i i i 到 j j j 有一条有向边。
而每一个点都将只遍历一次。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n, m, ans = 0;
int f[maxn];
vector<int> e[maxn];
int dfs(int u) {
if (f[u]) return f[u];
for (auto v: e[u]) f[u] = max(f[u], dfs(v) + 1);
return f[u];
}
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, u, v; i <= m; ++i) cin >> u >> v, e[u].push_back(v);
for (int i = 1; i <= n; ++i) ans = max(ans, dfs(i));
cout << ans << '\n';
}
AT_dp_h
f i , j f_{i,j} fi,j 表示走到第 ( i , j ) (i,j) (i,j) 个点所获得的最大价值。
如果当前的点是 ‘.’,那么可以走。则状态转移方程为
f i , j = ( f i − 1 , j + f i , j − 1 ) m o d ( 1 0 9 + 7 ) ( c i , j = ′ . ′ ) f_{i,j}=(f_{i-1,j}+f_{i,j-1})\bmod (10^9+7) (c_{i,j}='.') fi,j=(fi−1,j+fi,j−1)mod(109+7)(ci,j=′.′)
注意初始 f 1 , 1 = 1 f_{1,1}=1 f1,1=1
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e3 + 10, mod = 1e9 + 7;
int n, m;
char x;
int f[maxn][maxn];
signed main() {
cin >> n >> m;
f[1][1] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> x;
if (i == 1 && j == 1) continue;
if (x == '.') f[i][j] = (f[i - 1][j] + f[i][j - 1]) % mod;
}
}
cout << f[n][m] << '\n';
}
AT_dp_i
题目要求“面朝上的银币数比反面朝上的银币数多”。
很容易想到 f i , j f_{i,j} fi,j 表示前 i i i 个中有 j j j 个朝上的概率,那么朝下的概率就是 1 − f i , j 1-f_{i,j} 1−fi,j。
那么分类讨论正反面的概率再求和,即:
f i , j = f i − 1 , j − 1 × a i + f i − 1 , j × ( 1 − a i ) f_{i,j}=f_{i-1,j-1}×a_i +f_{i-1,j}×(1-a_i) fi,j=fi−1,j−1×ai+fi−1,j×(1−ai)
边界条件为 f 0 , 0 = 1 f_{0,0}=1 f0,0=1,每次往后推前计算 f i , 0 = f i − 1 , 0 × ( 1 − a i ) f_{i,0}=f_{i-1,0}×(1-a_i) fi,0=fi−1,0×(1−ai) 计算前 i i i 个全部朝下的概率。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e3 + 10;
int n;
double ans, f[maxn][maxn], a[maxn];
signed main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
f[0][0] = 1;
for (int i = 1; i <= n; ++i) {
f[i][0] = f[i - 1][0] * (1 - a[i]);
for (int j = 1; j <= n; ++j)
f[i][j] = f[i - 1][j - 1] * a[i] + f[i - 1][j] * (1 - a[i]);
}
for (int i = 1; i <= n; ++i) if (n < 2 * i) ans += f[n][i];
printf("%.10lf\n", ans);
}
AT_dp_j
首先,注意到 1 ≤ a i ≤ 3 1≤a_i≤3 1≤ai≤3 。所以考虑三维 dp。
f i , j , k = n i + j + k + i i + j + k f i − 1 , j , k + j i + j + k f i + 1 , j − 1 , k + k i + j + k f i , j + 1 , k − 1 f_{i,j,k}=\dfrac{n}{i+j+k}+\dfrac{i}{i+j+k}f_{i-1,j,k}+\dfrac{j}{i+j+k}f_{i+1,j-1,k}+\dfrac{k}{i+j+k}f_{i,j+1,k-1} fi,j,k=i+j+kn+i+j+kifi−1,j,k+i+j+kjfi+1,j−1,k+i+j+kkfi,j+1,k−1
#include <bits/stdc++.h>
using namespace std;
const int maxn = 3e2 + 10;
int n;
int a[4];
double f[maxn][maxn][maxn];
signed main() {
cin >> n;
for (int i = 1, x; i <= n; ++i) {
cin >> x;
a[x]++;
}
for (int k = 0; k <= n; ++k) {
for (int j = 0; j <= n; ++j) {
for (int i = 0; i <= n; ++i) {
if (!i && !j && !k) continue;
int t = i + j + k;
if (i) f[i][j][k] += f[i - 1][j][k] * i / t;
if (j) f[i][j][k] += f[i + 1][j - 1][k] * j / t;
if (k) f[i][j][k] += f[i][j + 1][k - 1] * k / t;
f[i][j][k] += (double)n / t;
}
}
}
printf("%.15lf\n", f[a[1]][a[2]][a[3]]);
}
AT_dp_k
f i f_i fi 表示剩余 i i i 块石头时,当前这个人的胜负情况。
显然, f i = f i − a j f_i=f_{i-a_j} fi=fi−aj,而当 i − a j = 0 i-a_j=0 i−aj=0 时,当前这个人就输了。
所以列出状态转移方程
f i = { i − 1 a j ≥ 0 f i − a j = 0 i a j ≤ 0 f i − a j ≠ 0 f_i=\begin{cases} i-1&a_j\ge 0&f_{i-a_j}=0 \\ i&a_j\le 0&f_{i-a_j}\ne0\end{cases} fi={i−1iaj≥0aj≤0fi−aj=0fi−aj=0
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n, k;
int a[maxn];
bool f[maxn];
signed main() {
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= k; ++i) {
for (int j = 1; j <= n; ++j) {
f[i] += i - a[j] >= 0 && !f[i - a[j]];
}
}
cout << (f[k] ? "First" : "Second");
}
AT_dp_l
明显地,是 d p dp dp 并且是区间 d p dp dp。
用 s i s_i si 作前缀和。
f i , j f_{i,j} fi,j 表示目前剩下 a l ∼ a r a_l ∼ a_r al∼ar,当前取数的人取数所能获得的最大数字和。
列出状态转移方程
f i , j = max ( s r − s l − 1 − f l + 1 , r , s r − s l − 1 − f l , r − 1 ) f_{i,j}=\max(s_r-s_{l-1}-f_{l+1,r},s_r-s_{l-1}-f_{l,r-1}) fi,j=max(sr−sl−1−fl+1,r,sr−sl−1−fl,r−1)
边界条件
f i , i = a i ( 1 ≤ i ≤ n ) f_{i,i}=a_i(1\le i \le n) fi,i=ai(1≤i≤n)
#include <iostream>
#define int long long
using namespace std;
const int maxn = 3e3 + 10;
int n;
int a[maxn], s[maxn];
int f[maxn][maxn];
signed main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i], f[i][i] = a[i], s[i] = s[i - 1] + a[i];
for (int len = 2; len <= n; ++len) {
for (int l = 1, r = l + len - 1; r <= n; ++l, ++r)
f[l][r] = max(s[r] - s[l - 1] - f[l + 1][r], s[r] - s[l - 1] - f[l][r - 1]);
}
cout << f[1][n] * 2 - s[n];
}
AT_dp_m
f i , j f_{i,j} fi,j 表示前 i i i 堆共分 j j j 个糖果的方案数。
状态转移方程为
f i , j = s i − 1 , j − s i − 1 , j − a i − 1 f_{i,j}=s_{i-1,j}-s_{i-1,j}-a_{i-1} fi,j=si−1,j−si−1,j−ai−1
e…因为 f i f_i fi 只跟 f i − 1 f_{i-1} fi−1 有关,所以可以去掉一维。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e5 + 10, mod = 1e9 + 7;
int n, k, a[110];
int s[maxn],f[maxn];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[i];
f[0] = 1;
for (int i = 1; i <= n; ++i) {
s[0] = f[0];
for (int j = 1; j <= k; ++j) s[j] = (s[j - 1] + f[j]) % mod;
for (int j = 0; j <= k; ++j) f[j] = ((s[j] - (j <= a[i] ? 0 : s[j - a[i] - 1])) % mod + mod) % mod;
}
cout << f[k] << '\n';
}