AT_dp 个人练习

附题单

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(fi1+aiai1,fi2+aiai2)
其中 f i f_i fi 表示跳到第 i i i 个位置的最小花费。

需要注意的是,当 f i > 1 f_i > 1 fi>1 时才能与 f i − 1 f_{i - 1} fi1 比较,不然会越界。
同理,当 f i > 2 f_i > 2 fi>2 时才能与 f i − 2 f_{i - 2} fi2 比较。

#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=1kfi+j=min(fi+j,fi+aiai+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(fi1,1,fi1,2)+aifi,1=max(fi1,0,fi1,2)+bifi,2=max(fi1,0,fi1,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] jt[i],剩下 i − 1 i-1 i1 个物品,价值为 f i − 1 , j − t [ i ] + w i f_{i-1,j-t[i]}+w_i fi1,jt[i]+wi

  • 不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi1,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] jw[i] 的价值,剩下 i − 1 i-1 i1 个物品,价值为 f i − 1 , j − w [ i ] + t i f_{i-1,j-w[i]}+t_i fi1,jw[i]+ti

  • 不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi1,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={fi1,j1+1max(fi1,j,fi,j1)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=i1,j=j1

  • 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=i1j=j1fi1,j>fi,j1fi1,j<fi,j1

直到 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=(fi1,j+fi,j1)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} 1fi,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=fi1,j1×ai+fi1,j×(1ai)

边界条件为 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=fi1,0×(1ai) 计算前 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 1ai3 。所以考虑三维 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+kifi1,j,k+i+j+kjfi+1,j1,k+i+j+kkfi,j+1,k1

#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=fiaj,而当 i − a j = 0 i-a_j=0 iaj=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={i1iaj0aj0fiaj=0fiaj=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 alar,当前取数的人取数所能获得的最大数字和。

列出状态转移方程

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(srsl1fl+1,r,srsl1fl,r1)

边界条件

f i , i = a i ( 1 ≤ i ≤ n ) f_{i,i}=a_i(1\le i \le n) fi,i=ai(1in)

#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=si1,jsi1,jai1

e…因为 f i f_i fi 只跟 f i − 1 f_{i-1} fi1 有关,所以可以去掉一维。

#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=1kfi+j=min(fi+j,fi+aiai+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(fi1,1,fi1,2)+aifi,1=max(fi1,0,fi1,2)+bifi,2=max(fi1,0,fi1,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] jt[i],剩下 i − 1 i-1 i1 个物品,价值为 f i − 1 , j − t [ i ] + w i f_{i-1,j-t[i]}+w_i fi1,jt[i]+wi

  • 不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi1,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] jw[i] 的价值,剩下 i − 1 i-1 i1 个物品,价值为 f i − 1 , j − w [ i ] + t i f_{i-1,j-w[i]}+t_i fi1,jw[i]+ti

  • 不选择第 i i i 件物品,那么价值为 f i − 1 , j f_{i-1,j} fi1,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={fi1,j1+1max(fi1,j,fi,j1)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=i1,j=j1

  • 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=i1j=j1fi1,j>fi,j1fi1,j<fi,j1

直到 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=(fi1,j+fi,j1)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} 1fi,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=fi1,j1×ai+fi1,j×(1ai)

边界条件为 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=fi1,0×(1ai) 计算前 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 1ai3 。所以考虑三维 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+kifi1,j,k+i+j+kjfi+1,j1,k+i+j+kkfi,j+1,k1

#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=fiaj,而当 i − a j = 0 i-a_j=0 iaj=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={i1iaj0aj0fiaj=0fiaj=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 alar,当前取数的人取数所能获得的最大数字和。

列出状态转移方程

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(srsl1fl+1,r,srsl1fl,r1)

边界条件

f i , i = a i ( 1 ≤ i ≤ n ) f_{i,i}=a_i(1\le i \le n) fi,i=ai(1in)

#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=si1,jsi1,jai1

e…因为 f i f_i fi 只跟 f i − 1 f_{i-1} fi1 有关,所以可以去掉一维。

#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';
}
  • 21
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值