【Codeforces235D_CF235D】Graph Game(概率_基环树)

题目

Codeforces 235D

Codeforces 235D(镜像站)

突然发现了 CF 镜像站这个神奇的东西 ……

翻译

题目名称:图的游戏

描述

在计算机科学中,有一种解决有关树上路径的问题的算法称为「点分治」。我们来用函数的形式描述这个算法:

\(solve(t)\)\(t\) 是一棵树):

  1. 在树 \(t\) 中选择一个结点 \(x\) (通常选择重心)。我们称这一步为「第一步」。
  2. 处理所有经过 \(x\) 的路径。
  3. 从树 \(t\) 中删除结点 \(x\)
  4. 然后 \(t\) 变成了若干个子树。
  5. 在每个子树上执行 \(solve\) 函数。

\(t\) 只有一个结点时,因为删除这个点后就什么也没有了,所以算法结束。

现在,我家妹子不骂人(译者注:这是人名,原文为「WJMZBMR」,听说是某位远古神犇的网名)错误地认为在「第一步」中选任意一个点都是可以的,所以他将随机地选一个点。使这个算法更糟的是,他认为一棵「树」的边数和点数相等!所以这个算法的过程变成了这样:

定义一个变量 \(totalCost\) ,初始化为 \(0\)\(solve(t)\) (现在 \(t\) 是一个图):

  1. \(totalCost=totalCost+(size\ of\ t)\) 。操作符「=」的意思是赋值。\(Size\ of\ t\) 的意思是 \(t\) 的结点数。
  2. 在图 \(t\) 中随机选择一个结点 \(x\)\(t\) 中所有结点等概率)。
  3. 从图 \(t\) 中删除 \(x\)
  4. 然后 \(t\) 变成了若干个连通块。
  5. 在每个连通块上执行 \(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}\)

综上所述:

\[P(x)=\frac{1}{n}+0+\frac{n-s}{n}\cdot \frac{1}{s}=\frac{s+n-s}{ns}=\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();
}
posted @ 2020-03-25 16:38  Inspector_Javert  阅读(159)  评论(0编辑  收藏  举报