题目
突然发现了 CF 镜像站这个神奇的东西 ……
翻译
题目名称:图的游戏
描述
在计算机科学中,有一种解决有关树上路径的问题的算法称为「点分治」。我们来用函数的形式描述这个算法:
(solve(t)) ((t) 是一棵树):
- 在树 (t) 中选择一个结点 (x) (通常选择重心)。我们称这一步为「第一步」。
- 处理所有经过 (x) 的路径。
- 从树 (t) 中删除结点 (x) 。
- 然后 (t) 变成了若干个子树。
- 在每个子树上执行 (solve) 函数。
当 (t) 只有一个结点时,因为删除这个点后就什么也没有了,所以算法结束。
现在,我家妹子不骂人(译者注:这是人名,原文为「WJMZBMR」,听说是某位远古神犇的网名)错误地认为在「第一步」中选任意一个点都是可以的,所以他将随机地选一个点。使这个算法更糟的是,他认为一棵「树」的边数和点数相等!所以这个算法的过程变成了这样:
定义一个变量 (totalCost) ,初始化为 (0) 。(solve(t)) (现在 (t) 是一个图):
- (totalCost=totalCost+(size of t)) 。操作符「=」的意思是赋值。(Size of t) 的意思是 (t) 的结点数。
- 在图 (t) 中随机选择一个结点 (x) ( (t) 中所有结点等概率)。
- 从图 (t) 中删除 (x) 。
- 然后 (t) 变成了若干个连通块。
- 在每个连通块上执行 (solve) 函数。
他会在一个 (n) 个结点和 (n) 条边的连通图上执行 (solve) 。他认为这个算法很快,但实际上它很慢。他想知道这个过程中 (totalCost) 的期望。你能帮他吗?
输入
第一行包含一个整数 (n(3leq nleq3000)) —— 图中的点数和边数。接下来的 (n) 行中,每一行包含两个整数 (a_i,b_i(0leq a_i,b_ileq n - 1)) ,表示在 (a_i) 和 (b_i) 之间有一条边。
注意结点编号是从 (0) 到 (n-1) 。保证图中没有自环和重边。保证图连通。
输出
输出一个整数 —— (totalCost) 的期望。如果你的答案和标准答案的绝对或相对误差不超过 (10^{-6}) 则被认定为正确。
分析
一个引理:
对于图 (G) 中的任意一个大小为 (s) 的连通块(不一定是连通分量) (C) ,里面每一个点成为该连通块中第一个被选中的点的概率是 (frac{1}{s}) 。考虑用归纳法证明(我自己口胡的不知道对不对啊)。
如果图 (G) 只有一个点,显然成立。
如果现在已经证明了对于所有点数小于 (n) 的图成立,来证明对于所有点数为 (n) 的图成立。考虑对于图 (G) 中任意一个连通块 (C) (设大小为 (s) ),在其中任意取一点 (x) 计算在 (C) 中首先选中 (x) 的概率 (P(x)) 。分这几种情况。
第一,如果第一步就选中 (x) ,那么 (x) 显然是 (C) 中第一个被选中的点,概率为 (frac{1}{n}) ;
第二,如果第一步选中了 (C) 中除 (x) 以外的点,那么 (x) 显然不可能是 (C) 中第一个被选中的点,概率为 (0) ;
第三,如果第一步选中了 (C) 以外的点(概率为 (frac{n-s}{n}) ),那么 (G) 被分成了一个或若干个点数小于 (n) 的连通分量,其中一定有一个连通分量完整包含了 (C) 。这个连通分量的点数小于 (n) 。由于已经证明了这个引理对所有点数小于 (n) 的图都成立,所以在这个连通分量中 (x) 成为 (C) 中第一个被选中的概率为 (frac{1}{s}) 。
综上所述:
好现在来看这道题。当一个点被选中时,答案的增量是这个点当前所在连通块的点数。换句话说,每一个与它连通的点都会对答案有 1 的贡献。形式化地,对于每一个 有序 点对 ((x,y)) ,如果 (x) 被选中时 (x) 和 (y) 连通,那么就会对答案有 1 的贡献。根据期望的线性性,每对 ((x,y)) 对答案有贡献的概率之和就是答案。
题目中给出的图是基环树。分两种情况讨论。
第一,如果 (x) 和 (y) 在同一棵树中,那么这两个点连通当且仅当它们树上路径上没有删掉任意一个点,也就是说 (x) 必须是这条路径上第一个被选中的点。根据引理,概率为 (frac{1}{p}) ,其中 (p) 是路径上的点数(含端点)。
第二,如果 (x) 和 (y) 不在同一棵树中,那么根据顺时针或是逆时针绕环就有两条路径。设两条路径的长度为 (a) 和 (b) ,两条路径并的长度为 (c) ,则有几下几种情况满足条件:
- (x) 是路径并中第一个被选中的点,概率为 (frac{1}{c}) ;
- 先选中了一条路径上一个点(概率为 (frac{c-a}{c}) 或 (frac{c-b}{c}) ),然后在另一条路径上第一个选中 (x) (概率为 (frac{1}{a}) 或 (frac{1}{b}) )。
综上,这种情况下概率为 (frac{1}{c}+frac{c-a}{c}cdotfrac{1}{a}+frac{c-b}{c}cdotfrac{1}{b}) 。
代码
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
namespace zyt
{
const int N = 3e3 + 10, B = 15;
int n;
vector<int> g[N];
bool vis[N], incir[N];
int cir[N], circnt, pos[N], rot[N], fa[N][B], dep[N];
bool dfs(const int u, const int f)
{
if (vis[u])
{
cir[pos[u] = circnt++] = u;
incir[u] = true;
return true;
}
vis[u] = true;
for (auto v : g[u])
{
if (v == f)
continue;
if (dfs(v, u))
{
if (u == cir[0])
return false;
else
{
cir[pos[u] = circnt++] = u;
incir[u] = true;
return true;
}
}
}
return false;
}
void JMAK(const int u, const int f, const int r)
{
rot[u] = r;
fa[u][0] = f;
for (int i = 1; i < B; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
dep[u] = dep[f] + 1;
for (auto v : g[u])
{
if (incir[v] || v == f)
continue;
JMAK(v, u, r);
}
}
int lca(int a, int b)
{
if (dep[a] < dep[b])
swap(a, b);
for (int i = B - 1; i >= 0; i--)
if (dep[fa[a][i]] >= dep[b])
a = fa[a][i];
if (a == b)
return a;
for (int i = B - 1; i >= 0; i--)
if (fa[a][i] != fa[b][i])
a = fa[a][i], b = fa[b][i];
return fa[a][0];
}
int dis(const int a, const int b)
{
return dep[a] + dep[b] - (dep[lca(a, b)] << 1);
}
int work()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int a, b;
scanf("%d%d", &a, &b);
++a, ++b;
g[a].push_back(b), g[b].push_back(a);
}
dfs(1, 0);
for (int i = 0, o = 0; i < circnt; i++)
JMAK(cir[i], o, cir[i]);
double ans = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (rot[i] == rot[j])
ans += 1.0 / double(dis(i, j) + 1);
else
{
int len1 = dep[i] + dep[j], len2 = abs(pos[rot[i]] - pos[rot[j]]) - 1, len3 = circnt - len2 - 2;
int tot = len1 + len2 + len3;
ans += 1.0 / tot + (double)len2 / tot * (1.0 / (len1 + len3)) + (double)len3 / tot * (1.0 / (len1 + len2));
}
printf("%.9f", ans);
return 0;
}
}
int main()
{
return zyt::work();
}