AtCoder DP Contest
AtCoder DP Contest
很抱歉,退役了也没有做完这个题单,还欠了很多题,有标号的就是做过的,有缘再见。
A
简单的递推跳楼梯
int n;
int a[N], f[N];
void solve() {
n = rd();
for(int i = 1; i <= n; i++) a[i] = rd();
f[2] = abs(a[2]-a[1]);
for(int i = 3; i <= n; i++) {
f[i] = min(f[i-1]+abs(a[i]-a[i-1]), f[i-2]+abs(a[i]-a[i-2]));
}
wr(f[n]);
}
B
能跳 \(k\) 格了,直接复杂度 \(O(nk)\) 可以通过
int n, k;
int a[N], f[N];
void solve() {
n = rd(), k = rd();
for(int i = 1; i <= n; i++) a[i] = rd();
mst(f, 0x3f);
f[1] = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= k && i+j <= n; j++) {
f[i+j] = min(f[i+j], f[i]+abs(a[i+j]-a[i]));
}
}
wr(f[n]);
}
C
简单的递推
int n;
int f[N][3];
void solve() {
n = rd();
for(int i = 1; i <= n; i++) {
int a = rd(), b = rd(), c = rd();
f[i][0] = max(f[i-1][1], f[i-1][2])+a;
f[i][1] = max(f[i-1][0], f[i-1][2])+b;
f[i][2] = max(f[i-1][1], f[i-1][0])+c;
}
wr(max({f[n][0], f[n][1], f[n][2]}));
}
D
01 背包板子
ll n, W, ans;
ll f[100005];
void solve() {
n = rd(), W = rd();
for(ll i = 1; i <= n; i++) {
ll w = rd(), v = rd();
for(ll j = W; j >= w; j--) {
f[j] = max(f[j], f[j-w]+v);
ans = max(ans, f[j]);
}
}
wr(ans);
}
E
以权值作为下标,\(f_i\) 表示答案为 \(i\) 时的最小重量的 01 背包
int n, W, V = 1e5;
int f[100005];
void solve() {
n = rd(), W = rd();
mst(f, 0x3f);
f[0] = 0;
for(int i = 1; i <= n; i++) {
int w = rd(), v = rd();
for(int j = V; j >= v; j--) {
f[j] = min(f[j], f[j-v]+w);
}
}
for(int i = V; i >= 0; i--) {
if(f[i] <= W) {
wr(i);
return;
}
}
}
F
最长公共子序列板子,加了个输出方案
int n, m;
int f[N][N];
pii pre[N][N];
char s[N], t[N];
void solve() {
scanf("%s", s+1);
scanf("%s", 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] = max(f[i-1][j], f[i][j-1]);
if(s[i] == t[j]) f[i][j] = max(f[i][j], f[i-1][j-1]+1);
}
}
vector <char> v;
while(f[n][m]) {
if(s[n] == t[m]) {
v.eb(s[n]);
n--, m--;
}
else if(f[n][m] == f[n][m-1]) m--;
else n--;
}
reverse(v.begin(), v.end());
for(auto x : v) cout << x;
cout << endl;
}
G
DAG 上最长路,直接拓扑排序
int n, m, ans;
int f[N], d[N];
vector <int> e[N];
void toposort() {
queue <int> q;
for(int i = 1; i <= n; i++) if(!d[i]) q.push(i);
while(q.size()) {
int x = q.front(); q.pop();
ans = max(ans, f[x]);
for(auto y : e[x]) {
d[y]--, f[y] = max(f[y], f[x]+1);
if(!d[y]) q.push(y);
}
}
}
void solve() {
n = rd(), m = rd();
for(int i = 1; i <= m; i++) {
int x = rd(), y = rd();
e[x].eb(y);
d[y]++;
}
toposort();
wr(ans);
}
H
走迷宫,直接加法原理递推
int h, w;
int f[N][N];
void solve() {
h = rd(), w = rd();
f[1][1] = 1;
for(int i = 1; i <= h; i++) {
for(int j = 1; j <= w; j++) {
char c;
cin >> c;
if(i == 1 && j == 1) continue;
if(c == '.') f[i][j] = (f[i-1][j]+f[i][j-1])%M;
}
}
wr(f[h][w]);
}
I
概率 dp,直接正推,设 \(f_{i,j}\) 为前 \(i\) 个硬币正的有 \(j\) 个的概率
int n;
double a[N], f[N][N], ans;
void solve() {
n = read();
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 <= i; j++) f[i][j] = f[i-1][j-1]*a[i]+f[i-1][j]*(1-a[i]);
}
for(int i = (n+1)/2; i <= n; i++) ans += f[n][i];
printf("%.9lf", ans);
}
J
很不错的期望 dp 练习题,由剩 \(0,1,2,3\) 个寿司的盘子数量定义状态 \(f_{i,j,k}\) 为 \(i\) 个 \(1\),\(j\) 个 \(2\),\(k\) 个 \(3\),\(n-i-j-k\) 个 \(0\)。可得转移式
注意输出精度和遍历顺序
int n;
int a[N], cnt[N];
double f[N][N][N];
void solve() {
n = rd();
for(int i = 1; i <= n; i++) {
a[i] = rd();
cnt[a[i]]++;
}
for(int sum = 1; sum <= n; sum++) {
for(int i = sum; i >= 0; i--) {
for(int j = sum-i; j >= 0; j--) {
int k = sum-i-j;
if(k < 0 || k > n) continue;
f[i][j][k] = 1;
if(i) f[i][j][k] += 1.0*(i*1.0/n)*f[i-1][j][k];
if(j) f[i][j][k] += 1.0*(j*1.0/n)*f[i+1][j-1][k];
if(k) f[i][j][k] += 1.0*(k*1.0/n)*f[i][j+1][k-1];
f[i][j][k] = f[i][j][k]/(1.0-((n-i-j-k)*1.0/n));
}
}
}
printf("%.15lf", f[cnt[1]][cnt[2]][cnt[3]]);
}
K
设 \(f_i\) 为当前石头重量总和为 \(i\) 时的输赢情况
当 \(i=0\) 时,显然先手必输,则 \(f_0=0\)
若有一个重量为 \(a_i\) 的石头,则先手必赢 \(f_{0+a_i}=f_{a_i}=1\)
类似的,有转移式\(f_i = \begin{cases}1,f_{i-a_j}=0\\0,f_{i-a_j}=1\end{cases}\)
int n, k;
int a[N], f[N];
void solve() {
n = rd(), k = rd();
for(int i = 1; i <= n; i++) a[i] = rd();
f[0] = 0;
for(int i = 1; i <= k; i++) {
for(int j = 1; j <= n; j++) {
if(i-a[j] < 0) continue;
if(f[i-a[j]] == 0) f[i] = 1;
}
}
if(f[k]) puts("First");
else puts("Second");
}
L
区间 dp 关路灯类问题,设 \(f_{i,j}\) 为取了 \(i\) 到 \(j\) 内的数时符合双方要求的值,则有
ll n;
ll a[N], f[N][N];
void solve() {
n = rd();
for(ll i = 1; i <= n; i++) a[i] = rd();
for(ll len = 1; len <= n; len++) {
for(ll i = 1, j = len; j <= n; i++, j++) {
if((n-len)%2 == 0) f[i][j] = max(f[i+1][j]+a[i], f[i][j-1]+a[j]);
else f[i][j] = min(f[i+1][j]-a[i], f[i][j-1]-a[j]);
}
}
wr(f[1][n]);
}
M
设 \(f_{i,j}\) 为第 \(i\) 个孩子,有 \(j\) 颗糖时的方案数,则显然有
这样是 \(O(nk^2)\) 的,发现上式是一个前缀和的形式,则可以在每次内层循环枚举 \(j\) 结束后求当前行的前缀和,优化转移
ll n, k;
ll a[N], f[N][100005], g[N][100005];
void solve() {
n = rd(), k = rd();
for(ll i = 1; i <= n; i++) a[i] = rd();
for(ll i = 0; i <= n; i++) g[i][0] = 1;
for(ll i = 1; i <= n; i++) {
for(ll j = 0; j <= k; j++) {
if(i > 1) {
f[i][j] = g[i-1][j];
if(j-a[i]-1 >= 0) f[i][j] -= g[i-1][j-a[i]-1];
f[i][j] = (f[i][j]%M+M)%M;
}
else if(j <= a[i]) f[i][j] = 1;
}
for(ll j = 1; j <= k; j++) g[i][j] = (g[i][j-1]+f[i][j])%M;
}
wr(f[n][k]);
}
可以通过滚动数组优化空间,但没必要
N
区间 dp 合并石子类问题,前缀和优化一下即可
ll n;
ll a[N], f[N][N], s[N];
void solve() {
n = rd();
mst(f, 0x3f);
for(ll i = 1; i <= n; i++) f[i][i] = 0, a[i] = rd(), s[i] = s[i-1]+a[i];
for(ll len = 2; len <= n; len++) {
for(ll i = 1, j = i+len-1; j <= n; i++, j++) {
for(ll k = i; k < j; k++) {
f[i][j] = min(f[i][j], f[i][k]+f[k+1][j]+s[j]-s[i-1]);
}
}
}
wr(f[1][n]);
}
O
P
树形 dp 板子,设 \(f_{i,0/1}\) 为第 \(i\) 个节点白色 / 黑色的情况,则有
ll n;
ll f[N][2];
vector <ll> e[N];
void dfs(ll x, ll pre) {
f[x][0] = f[x][1] = 1;
for(auto y : e[x]) {
if(y == pre) continue;
dfs(y, x);
f[x][0] *= f[y][0]+f[y][1];
f[x][1] *= f[y][0];
f[x][0] %= M;
f[x][1] %= M;
}
}
void solve() {
n = rd();
for(ll i = 1; i < n; i++) {
ll x = rd(), y = rd();
e[x].eb(y), e[y].eb(x);
}
dfs(1, 0);
wr((f[1][0]+f[1][1])%M);
}
Q
树状数组优化 LIS 板子,只不过加了个权值,不过不影响
ll n, ans;
ll a[N], h[N], f[N], t[N];
void add(ll x, ll v) {
while(x <= n) {
t[x] = max(t[x], v);
x += x&-x;
}
}
ll ask(ll x) {
ll res = 0;
while(x) {
res = max(res, t[x]);
x -= x&-x;
}
return res;
}
void solve() {
n = rd();
for(ll i = 1; i <= n; i++) h[i] = rd();
for(ll i = 1; i <= n; i++) a[i] = rd();
for(ll i = 1; i <= n; i++) {
f[i] = ask(h[i]-1)+a[i];
add(h[i], f[i]);
ans = max(ans, f[i]);
}
wr(ans);
}
R
设 \(f_{i,j,t}\) 为长度为 \(t\), 起点在 \(i\),终点在 \(j\) 的路径个数,有
显然为 floyd 的形式,且能用矩阵快速幂加速优化
ll n, K;
struct Matrix {
ll a[N][N];
void clear() { mst(a, 0); }
}a;
Matrix operator * (Matrix x, Matrix y) {
Matrix t; t.clear();
for(ll i = 1; i <= n; i++)
for(ll j = 1; j <= n; j++)
for(ll k = 1; k <= n; k++)
t.a[i][j] = (t.a[i][j]+x.a[i][k]*y.a[k][j]%M)%M;
return t;
}
Matrix qpow(Matrix a, ll b) {
Matrix res, bse = a; res.clear();
for(int i = 1; i <= n; i++) res.a[i][i] = 1;
while(b) {
if(b&1) res = res*bse;
bse = bse*bse;
b >>= 1;
}
return res;
}
void solve() {
n = rd(), K = rd();
for(ll i = 1; i <= n; i++) {
for(ll j = 1; j <= n; j++) {
a.a[i][j] = rd();
}
}
a = qpow(a, K);
ll ans = 0;
for(ll i = 1; i <= n; i++)
for(ll j = 1; j <= n; j++)
ans = (ans+a.a[i][j])%M;
wr(ans);
}
W
将所有条件放到右端点处理,设 \(f_i\) 为处理到第 \(i\) 个数并且第 \(i\) 个数放 \(1\),可发现 \(f_i\) 与 \(f_{i-1}\) 的区别就是那些 \(r_j=i\) 的条件,我们直接通过线段树把所有这样的条件包含的区间加上条件的权值即可,更新 \(f_i\) 时将前面所有的最大值和 \(0\) 取最大值即可。
ll n, m;
struct node {
ll l, r, v;
}a[N];
struct SegTre {
ll val[N<<2], lz[N<<2];
void pushdown(ll p) {
if(lz[p] == 0) return;
val[ls(p)] += lz[p];
val[rs(p)] += lz[p];
lz[ls(p)] += lz[p];
lz[rs(p)] += lz[p];
lz[p] = 0;
}
void modify(ll p, ll l, ll r, ll L, ll R, ll v) {
if(L <= l && r <= R) {
val[p] += v;
lz[p] += v;
return;
}
pushdown(p);
ll mid = (l+r) >> 1;
if(L <= mid) modify(ls(p), l, mid, L, R, v);
if(mid < R) modify(rs(p), mid+1, r, L, R, v);
val[p] = max(val[ls(p)], val[rs(p)]);
}
}t;
void solve() {
n = rd(), m = rd();
for(ll i = 1; i <= m; i++) {
a[i] = {rd(), rd(), rd()};
}
sort(a+1, a+1+m, [](node x, node y){ return x.r < y.r; });
for(ll i = 1, j = 1; i <= n; i++) {
t.modify(1, 1, n, i, i, max(0ll, t.val[1]));
while(j <= m && i == a[j].r) t.modify(1, 1, n, a[j].l, a[j].r, a[j].v), j++;
}
wr(max(0ll, t.val[1]));
}
X
显然 \(01\) 背包,但怎样才能保证物品放置时力量和重量间的关系最优呢?我们发现,对于物品 \(x\) 和 物品 \(y\),调换他们的顺序之和他们两个有直接影响,即当 \(x\) 在 \(y\) 下时,\(x\) 上能放的东西重量为 \(s_x-w_y\),而当 \(y\) 在 \(x\) 下时,\(y\) 上能放的东西重量为 \(s_y-w_x\),比大小并移向后可得
所以直接按这个排序即可保证,然后直接跑 \(01\) 背包即可
ll n, ans;
ll f[10*N*N];
struct node {
ll w, s, v; // weight strength value
}a[N];
bool cmp(node x, node y) { return x.s+x.w < y.s+y.w; }
void solve() {
n = rd();
for(ll i = 1; i <= n; i++) {
a[i] = {rd(), rd(), rd()};
}
sort(a+1, a+1+n, cmp);
for(ll i = 1; i <= n; i++) {
for(ll j = a[i].s; j >= 0; j--) {
f[j+a[i].w] = max(f[j+a[i].w], f[j]+a[i].v);
ans = max(ans, f[j+a[i].w]);
}
}
wr(ans);
}
Y
迷宫路线,但长宽变为了 \(10^5\),我们正难则反,考虑容斥,用总方案数减去被障碍挡住的方案数,而如果按经典的减去一个障碍的方案数加上两个障碍的方案数减去三个的……时间复杂度明显会超,换个思路,设 \(f_i\) 为没有被任何障碍挡住抵达第 \(i\) 个障碍的方案数,可得转移式
因为每个 \(f_i\) 都是没有被前面任何障碍挡住的,所以方案显然各不相同,直接累加是对的。我们可以将 \((h,w)\) 作为第 \(n+1\) 个障碍,并一起计算,答案即为 \(f_{n+1}\)。
ll h, w, n;
ll f[N], fac[N], inv[N], finv[N];
struct node {
ll x, y;
}p[N];
void init() {
fac[0] = fac[1] = inv[0] = inv[1] = finv[0] = finv[1] = 1;
for(ll i = 2; i <= 200000; i++) {
fac[i] = fac[i-1]*i%M;
inv[i] = (M-M/i)*inv[M%i]%M;
finv[i] = finv[i-1]*inv[i]%M;
}
}
ll C(ll a, ll b) {
if(a < b) return 0;
if(a == b) return 1;
return fac[a]*finv[b]%M*finv[a-b]%M;
}
bool cmp(node x, node y) {
if(x.x == y.x) return x.y < y.y;
return x.x < y.x;
}
void solve() {
h = rd(), w = rd(), n = rd();
init();
for(ll i = 1; i <= n; i++) p[i] = {rd(), rd()};
sort(p+1, p+1+n, cmp);
p[n+1] = {h, w};
for(ll i = 1; i <= n+1; i++) {
f[i] = C(p[i].x+p[i].y-2, p[i].x-1);
for(ll j = 1; j < i; j++) {
if(p[j].x <= p[i].x && p[j].y <= p[i].y) {
int x = p[i].x-p[j].x, y = p[i].y-p[j].y;
f[i] = (f[i]-f[j]*C(x+y, x)%M+M)%M;
}
}
}
wr(f[n+1]);
}

浙公网安备 33010602011771号