[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 两个端点在两个不同的子树内
这里的不同不仅仅是根节点在环上的编号不同,还有同一个环上节点的不同儿子的子树,这个原因上面坑点已经给出解释,这个其实也比较好算,你其实就是选取一个子树,在其中任取一点作为起点,再选取另一个子树,任选一点作为终点,方案数如下:
这个式子是 \(n^2\) 级别的,肯定不行,我们要进行优化,如何优化呢?我们可以通过构造两个线性可求的式子进行相减,有初中水平知识的可以注意到完全平方公式,没错,我们只要用完全平方公式减去平方和就好了,原因如下:
完全平方式展开后就变成了下面。
观察发现只少了一个 \([i\neq j]\) ,我们可以专门把这部分取出来,即 \(i=j\) 的情况:
所以方案数就是:
这样我们就分情况讨论完毕了,接下来就是代码实现。
代码实现
先把基环树上的环找到,我这里用的是一种冷门到极点的写法(也许只有我在用),你们可以使用自己的写法,或者查看别的题解,找到环以后就直接统计上面约定的 \(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;
}