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}\) 的转移. 如果不换教室,那么上次可能换也可能不换,两者取最小值;如果要换教室同理,就有

\[\begin{aligned} &f_{i,j,0}=\min\Big(f_{i-1,j,0}+w_{0,0}(i-1,i),f_{i-1,j,1}+w_{1,0}(i-1,i)\Big)\\ &f_{i,j,1}=\min\Big(f_{i-1,j-1,0}+w_{0,1}(i-1,i),f_{i-1,j-1,1}+w_{1,1}(i-1,i)\Big) \end{aligned} \]

再考虑 \(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\). 所以就可以列出贡献

\[\begin{aligned} &w_{0,0}(i-1,i)=dis_{c_{i-1},c_{i}}\\ &w_{0,1}(i-1,i)=k_i\times dis_{c_{i-1},d_{i}}+(1-k_i)\times dis_{c_{i-1},c_{i}}\\ &w_{1,0}(i-1,i)=k_{i-1}\times dis_{d_{i-1},c_{i}}+(1-k_{i-1})\times dis_{c_{i-1},c_{i}}\\ &w_{1,1}(i-1,i)=k_{i-1}\times\Big(k_i\times dis_{d_{i-1},d_{i}}+(1-k_i)\times dis_{d_{i-1},c_{i}})\Big)+(1-k_{i-1})\times\Big(k_i\times dis_{c_{i-1},d_{i}}+(1-k_i)\times dis_{c_{i-1},c_{i}})\Big)\\ \end{aligned} \]

初始化 \(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\) 变成了初态而并非之前的末态.

考虑怎么转移. 其实就是上面的推导过程,从后面的状态推到前面. 即

\[f_{i,s}= \begin{cases} f_{i,s}+\max(f_{i+1,s},f_{i+1,s|1<<j-1}+v_j)&s_j\&s=s_j\\ f_{i,s}+f_{i+1,s}&otherwise. \end{cases} \]

时间复杂度是 \(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\) 跳成功的最小时间期望一样. 可以列出转移:

\[f_u=\begin{cases} f_{fa_u}+1&当fa_u 是跳板星球\\ f_{fa_u}+p^{cnt}&otherwise. \end{cases} \]

直接转移,精细实现预处理 \(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\). 可以列出初始转移:

\[f_{i,j}={i-1\over i+j}f_{i-1,j}+{j-1\over i+j}f_{i,j-1}+{1\over2}{1\over i+j}f_{i-1,j}+{1\over2}{1\over i+j}f_{i,j-1} \]

这样 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}\)

\[g_{i,j}=\Big(i-{1\over2}\Big)g_{i-1,j}+\Big(j-{1\over2}\Big)g_{i,j-1} \]

发现这个东西很像带系数的组合数,考虑其组合意义其实就是网格图 \((i,j)\)\((0,0)\) 的方案数乘上固定的系数. 这个系数与上面的阶乘逆元很相似,都是依次 \(-1\) 再乘起来,即

\[g_{i,j}={i+j\choose i}\big(i-{1\over2}\big)^{\underline i}\big(j-{1\over2}\big)^{\underline j} \]

预处理阶乘,阶乘逆元和下降幂,可以做到 \(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_u=\sum_{(u,v)\in E}{g_v\over d_v} \]

\(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\) 的最低成本,容易写出转移:

\[f_{i,j}=\min_k\{f_{i-1,k}+d_i(U_i+j-k)+mk\} \]

直接 DP \(O(nS^2)\) 过不了,考虑拆式子化简.

把不含 \(k\) 的拿到括号外面去,即

\[f_{i,j}=\min_k\{f_{i-1,k}+k(m-d_i)\}+d_i(U_i+j) \]

\(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;
}
posted @ 2025-07-12 09:46  Ydoc770  阅读(19)  评论(0)    收藏  举报