CF123E maze 题解
思考暴力:枚举起点和终点,再枚举每一种遍历序列得到答案。复杂度起飞。
根据期望的可加性,我们无需硬着头皮统计每一条序列的贡献,而是把序列的贡献拆成遍历序列包含的边的贡献。换句话说,假如 \(Edge\) 为遍历时经过的边集,\(e\) 为边,则:
以上为第一部分,我们将路径贡献通过期望的性质拆成了更好计算的边的贡献。
考虑边的贡献:在固定起点与终点的情况下,我们把边分为三种:在起点到终点必经之路(最短路径)上的,不在必经之路上的,与不可能走到的(这些边在以起点为根时终点的子树内)。
第一种边贡献为 \(1\)。因为我们必须经过这条边,且因为遍历方式为深度优先搜索,一定会在经过这条边后搜到终点结束,不可能再走第二次。
现在我们思考第二种边的贡献。我们设起点为根节点,并设这条边深度较浅的点为 \(u\)。我们让 \(u\) 往上跳,直到遇到一个必经之路上的点 \(fa\)。我们令这条必经之路上边的下一个点为 \(w\)。
显然,\(u\) 与 \(w\) 分属 \(fa\) 的不同子树。假如我们先进入了 \(u\) 的子树,由于终点不在其中,所以会全遍历一遍后回溯,此时这条边贡献为 \(2\),因为进去和出来按照题目要求要算两次。
假如我们先进入了 \(w\),就不会再出来,而是遍历到终点后结束答案。此时这条边不会被计算到,贡献为 \(0\)。
而 \(u\) 和 \(w\) 先进哪棵子树完全取决于它们在遍历序列中的先后。由于序列随机,所以概率各为 \(50\%\)。
这时我们可以发现,第二种边对期望的贡献同样为 \(1\)。也就是说,固定起点终点,假如第三种边有 \(m\) 条,那么我们要求的期望就是 \(n - 1 - m\)。
以上为第二部分,我们将边的贡献分类讨论,最终简化了要求的问题。
那么我们来思考怎么快速求出最终答案。对于一个点 \(u\),我们思考他作为终点的情况(此时我们设 \(1\) 为根)。对于他的每棵子树,我们求出其内所有节点作为起点的概率和 \(f_v\),那么以这些点作为起点能够走到的边数就是子树大小(因为包括了连向 \(u\) 的边)。所以贡献为 \(u\times f_v\times si_v\)。
当然还有不是他子树作为起点的路径,这些路径的概率和为 \(1-f_u\),边集大小为 \(1-si_u\),按照上面的方式也能简单计算。
当然,钦定 \(u\) 作为起点运算也是可以的。以上为第三部分。
顺便一提这道题是我见过的少有的卡关闭同步流 cin
的题。请注意使用更快的输入方式。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n;
vector<int> e[N];
double x[N], y[N], sumX, sumY;
int si[N];
double f[N];
double ans;
double now;
void dfs(int u, int fa) {
si[u] = 1;
for (auto v : e[u]) {
if (v == fa) continue;
dfs(v, u);
f[u] += f[v];
si[u] += si[v];
}
f[u] += x[u];
}
signed main() {
// freopen("text.in", "r", stdin);
scanf("%d", &n);
int u, v;
for (int i = 1; i < n; ++i) {
scanf("%d%d", &u, &v);
e[u].push_back(v);
e[v].push_back(u);
}
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &x[i], &y[i]);
sumX += x[i], sumY += y[i];
}
for (int i = 1; i <= n; ++i) x[i] /= sumX;
for (int i = 1; i <= n; ++i) y[i] /= sumY;
dfs(1, -1);
double ans=0;
for (int now = 1; now <= n; ++now) {
for (auto v : e[now]) {
if (si[v] > si[now]) continue;
ans += f[v] * y[now] * si[v];
}
ans += (1 - f[now]) * y[now] * (n - si[now]);
}
printf("%.10lf\n", ans);
return 0;
}