【做题笔记】Atcoder 之 dp 专题训练
A
B
C
D
E
F
G
H
I
概率 dp。
设 \(dp_{i,j}\) 表示前 \(i\) 个硬币中有 \(j\) 个正面的概率。转移显然:
\(dp_{i,j}=dp_{i-1,j-1}\times p_i+dp_{i-1,j}\times (1-p_i)\)
当 \(j=0\) 时,前 \(i\) 个硬币中没有正面。所以只能由反面的概率转移过来,转移为:
\(dp_{i,j}=dp_{i-1,j}\times (1-p_i)\)
初始化 \(dp_{0,0}=1\)。
时间复杂度 \(O(n^2)\)。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N = 3005;
double ans, p[N], dp[N][N];
int n;
signed main() {
cin >> n;
For(i,1,n) cin >> p[i];
dp[0][0] = 1;
For(i,1,n) {
For(j,0,i) {
if(j == 0) dp[i][j] = dp[i-1][j] * (1 - p[i]);
else dp[i][j] = dp[i-1][j-1] * p[i] + dp[i-1][j] * (1 - p[i]);
}
}
For(i,n/2+1,n) {
ans += dp[n][i];
}
printf("%.10lf\n", ans);
return 0;
}
J
K
博弈论 dp。
设 \(dp_i\) 表示剩下 \(i\) 个石子的胜败态。考虑到能走到必败态的就一定是必胜态,进行转移即可。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N = 105, M = 2e5 + 10;
int n, k, a[N];
bool dp[M];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
For(i,1,n) cin >> a[i];
For(j,0,k) {
For(i,1,n) {
if(a[i] <= j) dp[j] |= (dp[j-a[i]] == 0);
}
}
cout << (dp[k] ? "First\n" : "Second\n");
return 0;
}
L
M
数数 dp。
设 \(dp_{i,j}\) 表示前 \(i\) 个人分 \(j\) 块糖果的方案数。转移为:
\(dp_{i,j} = \sum\limits_{x=0}^{min(j,a_i)}dp_{i-1,j-x} = \sum\limits_{x=j-min(j,a_i)}^{j}dp_{i-1,x}\)
答案为 \(dp_{n,k}\)。
上者使用前缀和优化即可。
时间复杂度 \(O(nk)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;ri>=l;--i)
#define mod 1000000007
using namespace std;
const int N = 105, M = 1e5 + 10;
int n, K, a[N], dp[N][M], sum[N][M];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> K;
For(i,1,n) cin >> a[i];
dp[0][0] = sum[0][0] = 1;
For(i,1,K) sum[0][i] += sum[0][i-1];
For(i,1,n) {
For(j,0,K) {
if(j == min(j,a[i])) dp[i][j] = sum[i-1][j] ;
else dp[i][j] = (sum[i-1][j] - sum[i-1][j-min(j,a[i])-1] + mod) % mod;
sum[i][j] = (sum[i][j-1] + dp[i][j]) % mod;
}
}
cout << dp[n][K] << '\n';
return 0;
}
N
O
状压 dp。
设 \(dp_{i,S}\) 表示左部点前 \(i\) 个点完全匹配,右部点状态为 \(S\) 的方案数。转移为:
\(dp_{i,S}=\sum\limits_{to(i,j)}dp_{i-1,S-j}\),其中 \(S-j\) 表示 \(S\) 状态中去掉 \(j\) 的状态。同时要保证 \(S\) 状态中 \(j\) 的状态为 \(1\)。
答案为 \(dp_{n,2^n-1}\)
时间复杂度 \(O(n^2 2^n)\),卡常可过。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define mod 1000000007
using namespace std;
const int N = 22, M = (1<<22)+1;
int n, a[N][N], dp[N][M], to[N][N], len[N];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
For(i,1,n) {
For(j,1,n) {
cin >> a[i][j];
if(a[i][j]) {
to[i][++len[i]] = j;
}
}
}
dp[0][0] = 1;
For(i,1,n) {
For(j,1,len[i]) {
int x = to[i][j]-1;
for (int S = 0; S < (1<<n); S++) {
if((S >> x) & 1) {
dp[i][S] = (dp[i][S] + dp[i-1][S ^ (1 << x)]) % mod;
}
}
}
}
cout << dp[n][(1<<n)-1] << '\n';
return 0;
}
P
数数 dp,类似 没有上司的舞会
设 \(dp_{x,0/1}\) 表示 \(x\) 节点为白或黑点的方案数。转移为:
\(dp_{x,0}=\prod_{y\in son(x)} (dp_{y,0}+dp_{y,1})\)
\(dp_{x,1}=\prod_{y\in son(x)} dp_{y,0}\)
答案为 \(dp_{1,0}+dp_{1,1}\)。
时间复杂度 \(O(n)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 1000000007
using namespace std;
const int N = 1e5 + 10;
struct Node {
int v, nx;
} e[N << 1];
int n, h[N], tot, dp[N][2];
void add(int u, int v) {
e[++tot] = (Node){v, h[u]};
h[u] = tot;
}
void dfs(int x, int fa) {
dp[x][0] = dp[x][1] = 1;
for (int i = h[x]; i; i = e[i].nx) {
int y = e[i].v;
if(y == fa) continue;
dfs(y, x);
dp[x][0] = (dp[x][0] * (dp[y][0] + dp[y][1]) % mod) % mod;
dp[x][1] = (dp[x][1] * dp[y][0]) % mod;
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
For(i,1,n-1) {
int u, v;
cin >> u >> v;
add(u, v); add(v, u);
}
dfs(1, 0);
cout << (dp[1][0] + dp[1][1]) % mod;
return 0;
}
Q
带权最长上升子序列
设 \(dp_i\) 表示以 \(i\) 结尾的最长上升子序列的最大权值。显然有:
\(dp_i=\max\limits_{j=1}^{i-1}dp_j+a_i\)
维护 \(dp\) 的前缀最大值并且支持插入数即可。树状数组即可胜任。
时间复杂度 \(O(n\log n)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 1000000007
using namespace std;
const int N = 2e5 + 10;
int n, h[N], a[N], t[N], dp[N], ans;
int lb(int x) {
return x & -x;
}
void add(int x, int val) {
for (int i = x; i <= N-2; i += lb(i)) {
t[i] = max(t[i], val);
}
}
int Max(int x) {
int res = 0;
for (int i = x; i; i -= lb(i)) {
res = max(res, t[i]);
}
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
For(i,1,n) cin >> h[i];
For(i,1,n) cin >> a[i];
For(i,1,n) {
dp[i] = Max(h[i]-1) + a[i];
add(h[i], dp[i]);
ans = max(ans, dp[i]);
}
cout << ans << '\n';
return 0;
}
R
矩阵优化 dp。
设 \(dp_{k,i}\) 表示经过长度为 \(k\),当前从某点转移至 \(i\) 的方案数。转移有:
\(dp_{k,i}=\sum\limits_{to(j,i)}dp_{k-1,j}\)
答案为 \(\sum\limits_{i=1}^n dp_{k,i}\)
可以发现每一次 sigma 内的转移都是固定的,这样重复有规律的转移可以用矩阵快速幂进行优化。
设计矩阵
对此矩阵进行 \(k\) 次快速幂,再与全 \(1\) 矩阵相乘。结果矩阵第一列即为答案。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define mod 1000000007
using namespace std;
const int N = 55;
int n, k, a[N][N], ans;
struct Matrix {
int M[N][N];
void clear() {
For(i,1,n) For(j,1,n) M[i][j] = 0;
}
void init() {
clear();
For(i,1,n) M[i][i] = 1;
}
Matrix friend operator * (const Matrix &A, const Matrix &B) {
Matrix Ans;
Ans.clear();
For(i,1,n) {
For(j,1,n) {
For(k,1,n) {
Ans.M[i][j] = (Ans.M[i][j] + (A.M[i][k] * B.M[k][j]) % mod) % mod;
}
}
}
return Ans;
}
} dp;
Matrix qpow(Matrix a, int b) {
Matrix res; res.init();
for (; b; b >>= 1, a = a * a) {
if(b & 1) res = res * a;
}
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> k;
For(i,1,n) For(j,1,n) cin >> a[i][j];
For(i,1,n) For(j,1,n) dp.M[i][j] = a[j][i];
Matrix Res; Res.clear();
For(i,1,n) Res.M[i][1] = 1;
Res = qpow(dp, k) * Res;
For(i,1,n) ans = (ans + Res.M[i][1]) % mod;
cout << ans << '\n';
return 0;
}
S
数位 dp。
设 \(dp_{pos,sum,0/1}\) 表示处理到第 \(pos\) 位,总和对 \(D\) 取模的结果为 \(sum\),是否达到上限。
考虑记忆化搜索,每位数值从 \(0\) 枚举至 \(maxx\)。\(maxx\) 取值关乎于是否达到上限。
上限的判定为从高位至低位的前缀一致(\(K\) 的前缀)。当达到上限时,\(maxx\) 取 \(K\) 的第 \(pos\) 位即可。
答案满足要求显然就是 \(sum=0\) 时。
以上为搜索统计答案,只要记忆化一下即可。
时间复杂度 \(O(可过)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
#define mod 1000000007
using namespace std;
const int N = 1e5 + 10, D = 105;
string k;
int n, d, num[N], dp[N][D][2];
int dfs(int pos, int res, int lim) {
if(pos == 0) return (res == 0);
if(dp[pos][res][lim] != -1) return dp[pos][res][lim];
int ans = 0, maxx = (lim ? num[pos] : 9);
For(i,0,maxx) {
ans = (ans + dfs(pos - 1, (res + i) % d, lim && (i == maxx))) % mod;
}
return dp[pos][res][lim] = ans;
}
int ans() {
memset(dp, -1, sizeof dp);
For(i,1,n) num[i] = k[n-i+1] - '0';
return dfs(n, 0, 1);
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> k >> d;
n = k.size();
k = " " + k;
cout << (ans()-1+mod)%mod << '\n';
return 0;
}
T
数数 dp。
设 \(dp_{i,j}\) 表示前 \(i\) 位填 \(1\) 至 \(i\),当前位填 \(j\) 的方案数。
当 \(s_i='>'\),当前位填的数要大于 \(i\),所以 \(dp_{i,j}=\sum\limits_{k=j}^{i-1}dp_{i-1,k}\)
当 \(s_i='<'\),当前位填的数要小于 \(i\),所以 \(dp_{i,j}=\sum\limits_{k=1}^{j-1}dp_{i-1,k}\)
答案为 \(\sum\limits_{i=1}^n dp_{n,i}\)
使用前缀和优化即可。
时间复杂度 \(O(n^2)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define mod 1000000007
using namespace std;
const int N = 3e3 + 10;
int n, dp[N][N], sum[N][N];
char s[N];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
cin >> s + 2;
dp[1][1] = sum[1][1] = 1;
For(i,2,n) {
For(j,1,i) {
if(s[i] == '<') dp[i][j] = sum[i-1][j-1];
else dp[i][j] = (sum[i-1][i-1] - sum[i-1][j-1] + mod) % mod;
sum[i][j] = (sum[i][j-1] + dp[i][j]) % mod;
}
}
int ans = 0;
For(i,1,n) ans = (ans + dp[n][i]) % mod;
cout << ans << '\n';
return 0;
}
U
看题解做出来的...
状压 dp。
设 \(dp_S\) 表示划分状态 \(S\) 的最大值。可以预处理出 \(val_S\) 表示状态 \(S\) 的贡献。
则转移为:
\(dp_S=\max\limits_{j\in{i的子集}} dp_{S \oplus j}+dp_{j}\)
答案为:\(dp_{2^n-1}\)
时间复杂度为 \(O(n^2 2^n+3^n)\)。可过。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N = 17, M = (1<<17);
int n, a[N][N], val[M], dp[M];
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
For(i,0,n-1) For(j,0,n-1) cin >> a[i][j];
For(S,0,(1<<n)-1) {
For(i,0,n-1) {
For(j,0,i-1) {
if(((S >> i) & 1) && ((S >> j) & 1)) val[S] += a[i][j];
}
}
}
For(S,0,(1<<n)-1) {
dp[S] = val[S];
for (int j = S; j; j = S & (j - 1)) {
dp[S] = max(dp[S], dp[S^j] + dp[j]);
}
}
cout << dp[(1<<n)-1] << '\n';
return 0;
}
V
W
X
贪心dp。
可以想到做 01 背包,但是转移拓扑序会出问题,以至于我们无法确定转移顺序。
考虑通过贪心确定转移顺序。直接按 \(w\) 或 \(s\) 排序肯定是错误的。从下往上,考虑到决策至相同塔高时,剩余能放的重量最多方案的肯定最优。对于相邻的 \(i,j\) 箱子。\(s_i-w_j\) 表示 \(j\) 在上,\(i\) 在下能剩余的放置重量,\(s_j-w_i\) 表示 \(i\) 在上,\(j\) 在下能剩余的放置重量。所以 \(s_i-w_j<s_j-w_i\) 则交换 \(i,j\)。
然后按照顺序做背包即可。时间复杂度 \(O(n\log n+nm)\)。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)
using namespace std;
const int N = 1e3 + 10, M = 1e5 + 10;
struct Node {
int w, s, v;
} a[N];
int n, dp[M], ans;
bool cmp(Node x, Node y) {
return x.w + x.s < y.w + y.s;
}
signed main() {
cin >> n;
For(i,1,n) cin >> a[i].w >> a[i].s >> a[i].v;
sort(a + 1, a + n + 1, cmp);
dp[0] = 0;
For(i,1,n) {
FOR(j,a[i].s,0) {
dp[j + a[i].w] = max(dp[j + a[i].w], dp[j] + a[i].v);
}
}
For(i,0,M-1) ans = max(ans, dp[i]);
cout << ans << '\n';
return 0;
}