【Codeforces235D_CF235D】Graph Game(概率_基环树)
题目
突然发现了 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(3\leq n\leq3000)\) —— 图中的点数和边数。接下来的 \(n\) 行中,每一行包含两个整数 \(a_i,b_i(0\leq a_i,b_i\leq 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}\cdot\frac{1}{a}+\frac{c-b}{c}\cdot\frac{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();
}