[luogu P5129] 不可思议的迷宫 题解

题目链接:P5129 不可思议的迷宫

题面

题目描述

给定一棵基环树,等概率在基环树上选取一个起点,再等概率选取终点(起点和终点可以重合),确定起点和终点后,如果会等概率选取起点到终点的一条简单路径,即不重复经过任何一条边。

求路径长度的期望,对 \(19260817\) 取模。

输入格式

第一行一个整数,表示基环树点数 \(n\)

接下来 \(n\) 行,每行 \(3\) 个整数 \(u,v,w\) ,表示基环树的一条边连接 \(u\)\(v\) ,长度是 \(w\)

输出格式

一行一个整数,表示期望在模 \(19260817\) 意义下的值。

数据规模

\(1\le n\le300000\)

题解

这道题难度不大,但是坑点很多,下面我们一一清点 (拉清单)

首先,最不坑的坑点,\(i\to j\)\(j\to i\) 分开计算,\(i\to i\) 只算一次。

然后,只要路径上的节点与环有交集,路径就可以从环的两侧走,这一点在样例里有部分体现。

最坑的点,若起点或终点在环上,则可以在从起点出发之前(到达终点之后)绕环一周,同样的,如果起点和终点重合在环上,则可以经过 \(0\) 的长度直接到达,也可以绕环一周后到达,你的路径中只要有一个点在环上,路径就可以从环上绕一圈,比如下面这张图中 \(1\to5\) 的路径有\(\color{red}{红色}\)\(\color{blue}{蓝色}\)标出来的两条 (画图技术有限)

知道了这些坑点,其实这道题难度不大,下面开始分析做法。

我们发现如果枚举每个起点和终点,时间就炸了,所以我们可以枚举每条边,考虑每条边被经过的次数。

另外,直接计算期望不方便,不妨先计算路径和,再除以总方案数。

1.非环边

如果一条边是非环边,这条边考虑起来就相对简单,如果起点和终点在它同一侧,则不可能经过它,因为没法回去,如果起点和终点在它异侧,则肯定会经过它,所以直接将左右两边点数乘起来,再乘 \(2\)(将起点和终点对调)就是答案了。

2.环边

如果一条边在环上,那么只要一条路径与环有交集,就肯定有 \(\dfrac{1}{2}\) 的概率经过这条边,所以问答题转化为有多少条路径会经过环。

我们分情况讨论。

首先,约定总点数为 \(n\) ,环上的点数为 \(c\)不在环上但和环上的点有直接边相连的第 \(i\) 个点的子树大小为 \(v_i\)

2.1 两个端点都在环上

如果两个端点都在环上,则这条路径肯定只能沿着环的某个方向走,一定会经过环,环上的每个点都可以作为起点,也可以作为终点,方案数是 \(c^2\)

2.2 只有一个端点在环上

这个也很简单,显然,根据上面的坑点,这种情况也肯定会存在路径经过环,如果有一个端点在环上,则一个端点取在环上,方案数是 \(c\) ,另一个端点取在环外,方案数是 \(n-c\) ,答案就是 \(2c(n-c)\) ,乘 \(2\) 的原因同上。

2.3 两个端点在两个不同的子树内

这里的不同不仅仅是根节点在环上的编号不同,还有同一个环上节点的不同儿子的子树,这个原因上面坑点已经给出解释,这个其实也比较好算,你其实就是选取一个子树,在其中任取一点作为起点,再选取另一个子树,任选一点作为终点,方案数如下:

\[\sum_i\sum_jv_iv_j[i\neq j] \]

这个式子是 \(n^2\) 级别的,肯定不行,我们要进行优化,如何优化呢?我们可以通过构造两个线性可求的式子进行相减,有初中水平知识的可以注意到完全平方公式,没错,我们只要用完全平方公式减去平方和就好了,原因如下:

完全平方式展开后就变成了下面。

\[(\sum_iv_i)^2=\sum_i\sum_jv_iv_j \]

观察发现只少了一个 \([i\neq j]\) ,我们可以专门把这部分取出来,即 \(i=j\) 的情况:

\[\sum_iv_i^2 \]

所以方案数就是:

\[(\sum_iv_i)^2-\sum_iv_i^2 \]

这样我们就分情况讨论完毕了,接下来就是代码实现。

代码实现

先把基环树上的环找到,我这里用的是一种冷门到极点的写法(也许只有我在用),你们可以使用自己的写法,或者查看别的题解,找到环以后就直接统计上面约定的 \(c\)\(v\) ,再统计,最后别忘了乘 \(n^2\) 的逆元。

c++ 代码

#include<bits/stdc++.h>

using namespace std;

#define Reimu inline void // 灵梦赛高
#define Marisa inline int // 魔理沙赛高

namespace FastInput { //快读
	template<typename Ty>
	inline Ty read() {
		Ty x = 0;
		int f = 0;
		char c = getchar();
		for (; !isdigit(c); c = getchar()) f |= c == '-';
		for (; isdigit(c); c = getchar()) x = (x << 1) + (x << 3) + (c & 15);
		return f ? -x : x;
	}

	template<>
	inline double read() {
		double x = 0;
		int f = 0;
		char c = getchar();
		for (; !isdigit(c); c = getchar()) f |= c == '-';
		for (; isdigit(c); c = getchar()) x = x * 10 + (c & 15);
		if (c == '.') {
			double e = 1;
			for (c = getchar(); isdigit(c); c = getchar()) x += (c & 15) * (e *= .1);
		}
		return f ? -x : x;
	}

	template<>
	inline char read() {
		char c = getchar();
		while (!isgraph(c)) c = getchar();
		return c;
	}

	template<>
	inline string read() {
		string s = "";
		char c = getchar();
		for (; !isgraph(c); c = getchar());
		for (; isgraph(c); c = getchar()) s += c;
		return s;
	}

	template<typename Ty>
	Reimu read(Ty &x) {
		x = read<Ty>();
	}

	template<typename Ty, typename...Arr>
	Reimu read(Ty &x, Arr &...arr) {
		read(x);
		read(arr...);
	}
}
using namespace FastInput;

namespace Mod { // 取模模板
    const int mod = 19260817; // 注意本题的模数和其他题有所不同,比较特别
    Marisa add(int x, int y) {
        return (x += y) < mod ? x < 0 ? x + mod : x : x - mod;
    }
    Marisa Add(int&x, int y) {
        return (x += y) < mod ? x < 0 ? (x += mod) : x : (x -= mod);
    }
    Marisa mul(int x, int y) {
        return 1LL * x * y % mod;
    }
    Marisa Mul(int&x, int y) {
        return x = 1LL * x * y % mod;
    }
    Marisa qpow(int x, int y) {
        int res = 1;
        for (; y; y >>= 1) {
            if (y & 1) Mul(res, x);
            Mul(x, x);
        }
        return res;
    }
    Marisa inv(int x) {
        return qpow(x, mod - 2);
    }
}
using namespace Mod;

typedef pair<int, int> Pii;
typedef pair<int, Pii> Piii;
#define fi first
#define se second
#define mp make_pair

const int N = 300010;

int n, sum1, sum2; // sum1是子树和的平方,sum2是子树的平方和
int inc[N], sz[N]; // inc标记这个点在不在环里,sz即子树大小
Piii e[N]; // 用三元组把边存下来
vector<int> g[N]; // vector建图方便找环

Reimu getCir(int x, int fa) { // 冷门找环方法,可以跳过,如果要学请自学 c++ try/catch/throw
	inc[x] = 1;
	try {
		for (auto y: g[x]) {
			if (y == fa) continue;
			if (inc[y]) throw y;
			getCir(y, x);
		}
	} catch(int v) {
		if (!v) throw inc[x] = 0;
		if (v == x) throw 0;
		throw v;
	}
	inc[x] = 0;
}

Reimu dfs(int x, int fa) { // 遍历求子树大小并统计
	sz[x] = 1;
	for (auto y: g[x]) {
		if (y == fa || inc[y]) continue;
		dfs(y, x);
		sz[x] += sz[y];
	}
	if (inc[fa]) {
		Add(sum1, sz[x]);
		Add(sum2, mul(sz[x], sz[x]));
	}
}

int main() {
	read(n);
	for (int i = 1; i <= n; ++i) {
		int x = e[i].se.fi = read<int>(), y = e[i].se.se = read<int>();
		read(e[i].fi);
		g[x].emplace_back(y);
		g[y].emplace_back(x);
	}
	try {
		getCir(1, 0); // 找环
	} catch(int) {}
	int c = 0;
	for (int i = 1; i <= n; ++i) {
		if (inc[i]) {
			dfs(i, 0);
			++c;
		}
	}
	Mul(sum1, sum1);
	int t = add(add(sum1, -sum2), add(mul(mul(add(n, -c), c), 2), mul(c, c))), inv2 = inv(2), ans = 0; // t是能经过环的路径个数
	for (int i = 1; i <= n; ++i) {
		int v = e[i].fi, x = e[i].se.fi, y = e[i].se.se;
		if (inc[x] && inc[y]) Add(ans, mul(mul(v, t), inv2)); // 乘inv2是因为每条边经过的概率为1/2
		else {
			int l = min(sz[x], sz[y]), r = n - l;
			Add(ans, mul(mul(v, mul(l, r)), 2));
		}
	}
	printf("%d", mul(ans, inv(mul(n, n)))); //别忘了要除以n^2
	return 0;
}
posted @ 2021-10-16 08:20  老莽莽穿一切  阅读(155)  评论(1编辑  收藏  举报