洛谷 P1850 [NOIP 2016 提高组] 换教室

题目传送门


前言

终于自己想出概率期望 \(dp\) 的状态了,但是依旧没能相对转移方程。(招笑)


暴力

这题部分分和特殊情况分给的挺多的,所以先拿部分分。

一、思路

  1. 先跑一边 \(Floyd\) 最短路求出两点间最短距离 \(dis_{i, j}\)
  2. 对于 \(m = 0\),答案就是 \(\sum_{i = 1}^{n - 1} dis_{c_{i}, c_{i + 1}}\)
    对于所有 \(n \leq 20\),直接用状态压缩,二进制枚举所有 【申请情况】【同意情况】(注意:这两者不一样,因为申请了不一定同意,所以枚举申请情况之下还要枚举同意情况),直接计算。

二、复杂度

  1. 空间:\(O(v^2 + n)\)
  2. 时间:对于 \(m = 0\)\(O(v^3 + n)\);对于 \(n \leq 20\)\(O(v^3 + n \times 2^n \times 2^m)\)
    (当然后者的时间复杂度远远跑不满,因为对于多数数据 \(m\) 很小,会限制枚举的状态数量)

三、代码

#include <bits/stdc++.h>

using namespace std;

const int maxn = 2e3 + 7;
const int maxv = 3e2 + 7;
const int inf  = 0x3f3f3f3f;

int n, m, v, e;
int c[maxn], d[maxn];
double p[maxn];
int dis[maxv][maxv];
void Floyd() {
	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]);
}
double ans;
int main() {
	scanf("%d%d%d%d", &n, &m, &v, &e);
	for (int i = 1; i <= n; ++i) scanf("%d", c + i);
	for (int i = 1; i <= n; ++i) scanf("%d", d + i);
	for (int i = 1; i <= n; ++i) scanf("%lf", p + i);
	memset(dis, inf, sizeof(dis));
	for (int i = 1; i <= v; ++i) dis[i][i] = dis[0][i] = dis[i][0] = 0;
	for (int i = 1, a, b, w; i <= e; ++i) {
		scanf("%d%d%d", &a, &b, &w);
		dis[a][b] = dis[b][a] = min(w, dis[a][b]);
	}
	Floyd();
	if (m == 0) {
		for (int i = 1; i < n; ++i)
			ans += dis[c[i]][c[i + 1]];
		printf("%.2lf\n", ans);
		return 0;
	}
	ans = 2e9;
	for (int s = 0; s < (1 << n); ++s) {  // 枚举【申请情况】
		if (__builtin_popcount(s) > m) continue;  // 用来计算 s 二进制中有多少个 1 的函数
		double sum = 0;  // 此【申请情况】下的期望
		for (int ss = s; 1; ss = s & (ss - 1)) {  // 枚举【同意情况】
			double tmp = 0;  // 此【同意情况】对【申请情况】的贡献
			for (int i = 2; i <= n; ++i) {
				int lst = ((ss & (1 << (i - 1 - 1))) ? d[i - 1] : c[i - 1]);
				int now = ((ss & (1 << (i - 1))) ? d[i] : c[i]);
				tmp += dis[lst][now];
			}
			for (int i = 1; i <= n; ++i)
				if (ss & (1 << (i - 1))) tmp *= p[i];
				else if (s & (1 << (i - 1))) tmp *= 1 - p[i];
			sum += tmp;
			if (ss == 0) break;
		}
		ans = min(ans, sum);
	}
	printf("%.2lf\n", ans);
	return 0;
} 

期望得分 \(68pts\),实际得分 \(68pts\)
(洛谷测评机跑出来挺迷的,应该是数据水的问题,前面暴力的数据点一部分没过,后面正解的数据点过了一部分)


正解

概率期望的题一般就是拿 \(dp\) 做。

一、思路

状态设计

  • 很明显要有两维的状态 \(i, j\),表示前 \(i\) 个中选了 \(j\) 个。
    但是由于花费还与上一状态有关(上一个课程是在 \(c_i\) 还是 \(d_i\)),所以还要一维表示这个课是否被申请了。
  • \(dp_{i, j, 0/1}\) 表示在前 \(i\) 个课中 申请了(注意不是同意!!)\(j\) 个,且第 \(i\) 个课【没被申请 / 被申请了】时的最小期望。

状态转移

  1. 若当前课程 \(i\) 不申请,且上一个课程也没有申请,那么转移方程为:

\[dp_{i, j, 0} = min(dp_{i, j}, dp_{i - 1, j, 0} + dis_{c_{i - 1}, c_i}) \]

  1. 若当前课程 \(i\) 不申请,但上一个课程申请了,那么上一个申请可以通过,也可以不通过,那么转移方程为(转移条件为 \(j > 0\)):

\[dp_{i, j, 0} = min( dp_{i, j, 0}, dp_{i - 1, j, 1} + dis_{c_{i - 1}, c_{i}} \times (1 - k_i) + dis_{d_{i - 1}, c_i} \times k_{i}) \]

  1. 若当前课程 \(i\) 申请,那么上一次可以申请,也可以不申请,总共有四种情况,在此就不列举出来了(条件当然也是 \(j > 0\))。

边界条件

  • 只有一个课程时,申请或不申请期望花费都为 \(0\),即:\(dp_{1,0,0} = dp_{1,1,1} = 0\)
  • 而对于 \(dp_{1,0,1},dp_{1,1,0}\) 这种不合法状态,我们可以先把他们设为正无穷,这样就不会从它门转移了。

答案

  • 因为题目说可以不用玩 \(m\) 次申请,所以答案就是 \(min_{i = 0}^{m} \left\{dp_{n, i,0}, dp_{n, i, 1} \right\}\)

复杂度

  1. 空间:\(O(v^2 + n \times m)\)
  2. 时间:\(O(v^3 + n \times m)\)

二、代码

#include <bits/stdc++.h>

using namespace std;

const int maxn = 2e3 + 7;
const int maxv = 3e2 + 7;
const int inf  = 0x3f3f3f3f;

int n, m, v, e;
int c[maxn], d[maxn];
double p[maxn];
int dis[maxv][maxv];
void Floyd() {
	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]);
}

double dp[maxn][maxn][2];
double ans;
int main() {
	scanf("%d%d%d%d", &n, &m, &v, &e);
	for (int i = 1; i <= n; ++i) scanf("%d", c + i);
	for (int i = 1; i <= n; ++i) scanf("%d", d + i);
	for (int i = 1; i <= n; ++i) scanf("%lf", p + i);
	memset(dis, 63, sizeof(dis));
	for (int i = 1; i <= v; ++i) dis[i][i] = dis[i][0] = dis[0][i] = 0;
	for (int i = 1, a, b, w; i <= e; ++i) {
		scanf("%d%d%d", &a, &b, &w);
		dis[a][b] = dis[b][a] = min(w, dis[a][b]);
	}
	Floyd();
	for (int i = 0; i <= n; ++i)
		for (int j = 0; j <= m; ++j)
			dp[i][j][0] = dp[i][j][1] = 2e9;
	dp[1][0][0] = dp[1][1][1] = 0;
	for (int i = 2; i <= n; ++i) {
		dp[i][0][0] = dp[i - 1][0][0] + dis[c[i - 1]][c[i]];
		for (int j = 1; j <= min(i, m); ++j) {
			// 巨丑马蜂
			dp[i][j][0] = min(dp[i - 1][j][0] + dis[c[i - 1]][c[i]],
							  dp[i - 1][j][1] + dis[c[i - 1]][c[i]] * (1 - p[i - 1]) + dis[d[i - 1]][c[i]] * p[i - 1]);
							  
			dp[i][j][1] = min(dp[i][j][1], dp[i - 1][j - 1][0] + 
										   dis[c[i - 1]][c[i]] * (1 - p[i]) + 
										   dis[c[i - 1]][d[i]] * p[i]);
			
			dp[i][j][1] = min(dp[i][j][1], dp[i - 1][j - 1][1] + 
										   dis[c[i - 1]][c[i]] * (1 - p[i - 1]) * (1 - p[i]) + 
										   dis[c[i - 1]][d[i]] * (1 - p[i - 1]) * p[i] + 
										   dis[d[i - 1]][c[i]] * p[i - 1] * (1 - p[i]) + 
										   dis[d[i - 1]][d[i]] * p[i - 1] * p[i]);
		}
	}
	
	ans = 2e9;
	for (int i = 0; i <= m; ++i)
		ans = min(ans, min(dp[n][i][0], dp[n][i][1]));
	printf("%.2lf\n", ans);
	return 0;
} 

期望的分 \(100pts\),实际得分 \(100pts\)

posted @ 2025-05-03 14:50  syzyc  阅读(49)  评论(0)    收藏  举报