2025.4.5 与 2025.5.1 DP 专题
Luogu P1850 换教室
过程中要求多次两个教室间最短路,可以先跑 floyd 把所有最短路 \(dis_{i,j}\) 求出来.
接下来可以一个一个教室考虑换/不换,由于换/不换教室会影响转移,所以加一维 \(0/1\)来分讨.
设 \(f_{i,j,0/1}\) 表示前 \(i\) 个教室换了 \(j\) 个,当前教室 \(0/1\) 换不换. 再设 \(w_{0/1,0/1}(i-1,i)\) 表示上次和这次换不换教室的期望路径.
先考虑 \(f_{i,j,0/1}\) 的转移. 如果不换教室,那么上次可能换也可能不换,两者取最小值;如果要换教室同理,就有
再考虑 \(w_{0/1,0/1}(i-1,i)\) 的值. 每次换教室就会导致路径期望发生变化,具体的,\(i-1\) 位置换教室就有 \(k_{i-1}\) 的概率从 \(d_{i-1}\) 出发,\(1-k_{i-1}\) 的概率从 \(c_i\) 出发,不换教室就只从 \(c_i\) 出发;\(i\) 位置换教室就有 \(k_i\) 的概率前往 \(d_i\),\(1-k_i\) 的概率前往 \(c_i\),不换教室就只会前往 \(c_i\). 所以就可以列出贡献
初始化 \(dis_{i,j}\) 和 \(f_{i,j,0/1}\) 为极大值,DP即可. 时间复杂度 \(O(n^3+nm)\).
代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e3 + 10, maxv = 3e2 + 10, inf = 1e9;
int n, m, v, e, c[maxn], d[maxn]; double k[maxn];
int dis[maxv][maxv];
double f[maxn][maxn][2];
inline double w00(int x, int y) {return dis[c[x]][c[y]];}
inline double w01(int x, int y) {return k[y] * dis[c[x]][d[y]] + (1.0 - k[y]) * dis[c[x]][c[y]];}
inline double w10(int x, int y) {return k[x] * dis[d[x]][c[y]] + (1.0 - k[x]) * dis[c[x]][c[y]];}
inline double w11(int x, int y) {return k[x] * (k[y] * dis[d[x]][d[y]] + (1.0 - k[y]) * dis[d[x]][c[y]]) + (1.0 - k[x]) * (k[y] * dis[c[x]][d[y]] + (1.0 - k[y]) * dis[c[x]][c[y]]);}
void init() {
for(int i = 0; i <= v; i++) for(int j = 0; j <= v; j++) dis[i][j] = inf;
for(int i = 1; i <= n; i++) for(int j = 0; j <= m; j++) f[i][j][0] = f[i][j][1] = inf;
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m >> v >> e; init();
for(int i = 1; i <= n; i++) cin >> c[i];
for(int i = 1; i <= n; i++) cin >> d[i];
for(int i = 1; i <= n; i++) cin >> k[i];
for(int i = 1; i <= e; i++) {
int x, y, w; cin >> x >> y >> w;
dis[x][y] = min(dis[x][y], w), dis[y][x] = min(dis[y][x], w);
}
for(int i = 1; i <= v; i++) dis[i][i] = dis[i][0] = dis[0][i] = 0;
for(int k = 1; k <= v; k++) {
for(int i = 1; i <= v; i++) {
for(int j = 1; j <= v; j++) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
f[1][0][0] = f[1][1][1] = 0;
for(int i = 2; i <= n; i++) {
f[i][0][0] = f[i - 1][0][0] + dis[c[i - 1]][c[i]];
for(int j = 1; j <= m; j++) {
f[i][j][0] = min(f[i - 1][j][0] + w00(i - 1, i), f[i - 1][j][1] + w10(i - 1, i));
f[i][j][1] = min(f[i - 1][j - 1][0] + w01(i - 1, i), f[i - 1][j - 1][1] + w11(i - 1, i));
}
}
double ans = inf;
for(int i = 0; i <= m; i++) ans = min(ans, min(f[n][i][0], f[n][i][1]));
cout << fixed << setprecision(2) << ans;
return 0;
}
Luogu P2473 奖励关
\(n\) 很小,考虑状压.
先设 \(f_{i,s}\) 表示考虑了前 \(i\) 次选奖励,之前选到的奖励集合为 \(s\) 的期望. 如果正序 DP ,需要考虑当前的状态是否合法,非常的麻烦,而且最终的状态也不确定. 考虑到初态 \(s=0\) 是已知的,所以可以倒序从 \(f_{k,s}\) 推到 \(f_{1,s}\),相当于从未知状态一步步推向已知状态,中间只要某个物品 \(j\) 的前置集合 \(s_j\in s\) 就考虑转移,从大状态一步步转移到小状态,最后查询 \(f_{1,0}\) 即为答案.
所以倒序 DP 的状态 \(f_{i,s}\) 应该表示从选前 \(i-1\) 个物品且状态为 \(s\) 出发,已经选完了剩下 \(k-i+1\) 个物品所得到的最大期望. \(s\) 变成了初态而并非之前的末态.
考虑怎么转移. 其实就是上面的推导过程,从后面的状态推到前面. 即
时间复杂度是 \(O(nk\cdot2^n)\).
代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn = 20, maxk = 1e2 + 10, maxs = 1 << 16;
int n, k, p, v[maxn], si[maxn];
long double f[maxk][maxs];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> k >> n;
for(int i = 1; i <= n; i++) {
cin >> v[i] >> p;
while(p != 0) si[i] |= (1 << (p - 1)), cin >> p;
}
for(int i = k; i >= 1; i--) {
for(int s = 0; s < (1 << n); s++) {
for(int j = 1; j <= n; j++) {
if((si[j] & s) == si[j])
f[i][s] += max(f[i + 1][s], f[i + 1][s | (1 << (j - 1))] + v[j]);
else f[i][s] += f[i + 1][s];
} f[i][s] /= n;
}
}
cout << fixed << setprecision(6) << f[1][0];
return 0;
}
Luogu P9428 逃跑
题意可能比较难以理解.
由于每个节点每次移动是独立的,我们可以求出从每个节点到根节点的最短时间期望,再求平均值.
考虑从根节点开始在 \(dfs\) 途中 DP. 设 \(f_u\) 表示单独从 \(u\) 出发最短时间的期望. 由于每次跳跃独立,尝试跳一次失败就会跳下一个跳板,直到所有 \(cnt\) 个跳板如果都失败,则时间 \(+1\),这部分期望时间即 \(p^{cnt}\). 但是当 \(fa_u\) 作为跳板的时候,\(u\) 跳成功或失败最终都会前往 \(fa_u\),所以时间直接 \(+1\). 其余情况 \(u\) 跳成功和 \(fa_u\) 跳成功的最小时间期望一样. 可以列出转移:
直接转移,精细实现预处理 \(p^{cnt}\) 之类的可以做到 \(O(n)\). 直接快速幂 \(O(n\log n)\) 也能过.
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long double ld;
const int maxn = 1e6 + 10;
int n, m; ld p; bool ok[maxn];
ld ans, f[maxn];
struct EDGE{
int nxt, v;
}e[maxn << 1];
int tot, head[maxn];
void add(int u, int v) {e[++tot].nxt = head[u], e[tot].v = v, head[u] = tot; return;}
ld qpow(ld x, int y) {
ld res = 1;
while(y) {
if(y & 1) res *= x;
y >>= 1, x *= x;
} return res;
}
void dfs(int u, int fa, int cnt) {
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v; if(v == fa) continue;
if(ok[u]) f[v] = f[u] + 1;
else f[v] = f[u] + qpow(p, cnt);
dfs(v, u, cnt + ok[u]);
}
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m >> p;
for(int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
add(u, v), add(v, u);
}
for(int i = 1; i <= m; i++) {
int u; cin >> u;
ok[u] = true;
}
dfs(1, 0, 0);
for(int i = 1; i <= n; i++) ans += f[i];
cout << fixed << setprecision(2) << ans / n;
return 0;
}
Luogu P10868 Points on the Number Axis B
考虑最后的结果一定是形如对于每个 \(x_i\) 有系数 \(p_i\),期望位置 \(\sum_ip_i\times x_i\). 现在要想办法对于一个中间的状态转移,就一定避不开表示左边和右边剩下点的个数. 设 \(f_{i,j}\) 表示某个点左边有 \(i\) 个点,右边有 \(j\) 个点时的系数. 答案即为 \(\sum_if_{i-1,n-i}\times x_i\).
考虑接下来选择相邻的两个点,有 \(i+j\) 个空隙可以选. 可能选到左边 \(i-1\) 个空隙或者右边 \(j-1\) 个空隙,这时系数的值不改变,从 \(f_{i-1,j}\) 和 \(f_{i,j-1}\) 转移过来;也可能选到含当前这个点的两个空隙,这时候系数会变成原来的 \(1\over2\). 可以列出初始转移:
这样 DP 是 \(O(n^2)\) 的,考虑化简. 把 \(1\over i+j\) 提出来,发现最后的结果相当于对于里面那些式子依次乘 \({1\over i+j}{1\over i+j-1}{1\over i+j-2}\cdots\),即乘上一个阶乘逆元. 不妨设 \({1\over(i+j)!}g_{i,j}=f_{i,j}\).
则 \(g_{i,j}\) 有
发现这个东西很像带系数的组合数,考虑其组合意义其实就是网格图 \((i,j)\) 到 \((0,0)\) 的方案数乘上固定的系数. 这个系数与上面的阶乘逆元很相似,都是依次 \(-1\) 再乘起来,即
预处理阶乘,阶乘逆元和下降幂,可以做到 \(O(n)\) 的时间复杂度.
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 10, mo = 998244353, inv2 = 499122177;
int n, a[maxn];
ll fac[maxn], ifac[maxn], epow[maxn];
ll f[maxn], ans;
ll qpow(ll x, ll y) {
ll res = 1;
while(y) {
if(y & 1) (res *= x) %= mo;
(x *= x) %= mo, y >>= 1;
} return res;
}
void pre_calc() {
fac[0] = ifac[0] = 1; for(int i = 1; i <= n; i++) fac[i] = (fac[i - 1] * i) % mo;
ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = ifac[i + 1] * (i + 1) % mo;
epow[0] = 1; for(int i = 1; i <= n; i++) epow[i] = epow[i - 1] * (i - inv2 + mo) % mo;
return;
}
ll C(int x, int y) {return (x < y || x < 0) ? 0 : fac[x] * ifac[y] % mo * ifac[x - y] % mo;}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n; pre_calc();
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) f[i] = C(n - 1, i - 1) * epow[i - 1] % mo * epow[n - i] % mo * ifac[n - 1] % mo;
for(int i = 1; i <= n; i++) (ans += 1ll * a[i] * f[i] % mo) %= mo;
cout << ans;
return 0;
}
Luogu P3232 游走
要总分期望最小肯定是先把每条边的期望经过次数算出来,再把次数少的编号更大的. 但是随机游走很不好的一点是有繁杂的后效性,重复经过边的次数没办法转移出来,所以 DP 方程列出来没有办法顺着推,要想办法通过某种方式来解出答案.
设每条边经过次数 \(f_{u,v}\),但是两维直接 DP 非常的困难. 不妨转换为对每个点考虑经过次数 \(g_u\),设每个点的度数为 \(d_u\),边 \(f_{u,v}\) 的经过次数就是 \({g_u\over d_u}+{g_v\over d_v}\).
考虑表示出 \(g_u\) 的关系式. 等概率的从每个相邻点转移过来,有:
把 \(g\) 挪到一边,发现得到了一堆 \(g\) 等于一个定值的式子. 对于每一个 \(u\) 都可以得到这样一个式子,那么我们可以直接把它们看做一个 \(n\) 元方程组,用高斯消元解出来.
但是这样直接解是解不出来的,因为上面的推倒没有考虑不能走到尽头以及初始经过了 \(1\) 号顶点. 实现时要特殊处理一下. 时间复杂度就是高消的 \(O(n^3)\)
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long double ld;
const int maxn = 5e2 + 10, maxm = 26e4;
int n, m, tot, head[maxn], V[maxm], nxt[maxm], st[maxm], ed[maxm], d[maxn];
ld ans, a[maxn][maxn], b[maxn], x[maxn], f[maxm];
void add(int u, int v) {
nxt[++tot] = head[u], head[u] = tot; V[tot] = v;
return;
}
void Gauss(int n) {
for(int i = 1; i <= n; i++) {
int r = i;
for(int j = i + 1; j <= n; j++) if(fabs(a[j][i]) > fabs(a[r][i])) r = j;
if(i != r) swap(a[i], a[r]), swap(b[i], b[r]);
for(int j = i + 1; j <= n; j++) {
ld d = a[j][i] / a[i][i]; b[j] -= d * b[i];
for(int k = i; k <= n; k++) a[j][k] -= d * a[i][k];
}
}
for(int i = n; i >= 1; i--) {
for(int j = i + 1; j <= n; j++) b[i] -= x[j] * a[i][j];
x[i] = b[i] / a[i][i];
}
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {cin >> st[i] >> ed[i]; add(st[i], ed[i]); add(ed[i], st[i]); d[st[i]]++, d[ed[i]]++;}
for(int u = 1; u < n; u++) {
a[u][u] = 1.0;
for(int i = head[u]; i; i = nxt[i]) {
int v = V[i];
if(v != n) a[u][v] = -1.0 / d[v];
}
}
b[1] = 1;
Gauss(n - 1);
for(int i = 1; i <= m; i++) f[i] = x[st[i]] / d[st[i]] + x[ed[i]] / d[ed[i]];
sort(f + 1, f + m + 1);
for(int i = 1; i <= m; i++) ans += f[i] * (m - i + 1);
cout << fixed << setprecision(3) << ans;
return 0;
}
Luogu P10879 对树链剖分的爱
会了,待补. 咕咕咕.
Luogu P2517 订货
朴素 DP 是简单的,设 \(f_{i,j}\) 表示第 \(i\) 个月,上个月库存量为 \(j\) 的最低成本,容易写出转移:
直接 DP \(O(nS^2)\) 过不了,考虑拆式子化简.
把不含 \(k\) 的拿到括号外面去,即
跟 \(k\) 有关的最小值可以一轮前缀找出来,这样就优化掉了一个枚举 \(S\) 的复杂度,变成了 \(O(nS)\).
但是直接拿全局最小值样例会算出来一个离谱的负数,因为实际上 \(k\) 存在一个范围 \([0,\min(U_i+j, S)]\),超出这个范围的一定不会是最优的成本. 所以再单开个数组存一下前缀最小值就好了.
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 60, maxs = 1e4 + 10, inff = 1e9 + 10;
int n, m, s, u[maxn], d[maxn];
ll f[maxn][maxs], g[maxs];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m >> s;
for(int i = 1; i <= n; i++) cin >> u[i];
for(int i = 1; i <= n; i++) cin >> d[i];
for(int i = 0; i <= s; i++) f[1][i] = (u[1] + i) * d[1];
for(int i = 2; i <= n; i++) {
for(int k = 0; k <= s; k++) g[k] = min(k ? g[k - 1] : inff, f[i - 1][k] + k * (m - d[i]));
for(int j = 0; j <= s; j++) f[i][j] = g[min(u[i] + j, s)] + (u[i] + j) * d[i];
}
cout << f[n][0];
return 0;
}

浙公网安备 33010602011771号