金牌导航-高斯消元/Luogu P3232 游走

这应该是我们做的除模板外的第一道高斯消元吧(我是的)

题目描述:


详细分析:

我们对于编号的分配,很明显可以发现如下的分配就是期望最小的:对经过的期望次数越大的边赋予更小的编号。
那么问题就转化为了怎么求一条边的经过的期望次数,我们发现边数非常大所以肯定不好弄,所以我们就转而看很少的点。因为我们会发现如果我们能知道经过每个点的期望次数,那么这条边的期望次数很轻松就能表达出来。
比如如下的式子:(设 ans[i]ans[i] 为经过第 ii 个点的期望次数, du[i]du[i] 为第 ii 个点的度数, res[i]res[i] 为经过第 ii 条边的期望次数)

res[i]=ans[from]du[from]+ans[to]du[to]res[i] = \dfrac{ans[from]}{du[from]} + \dfrac{ans[to]}{du[to]}

这个式子应该很好理解,就是说每个点等概率地选择和他相连的边,所以选择这一条边地期望次数就是经过它的期望次数除以它的度数,也就是与他相连的边的数量,因为这条边可以从两个端点开始走然后经过,所以应该加上两个端点的值。
上文探讨了如果知道经过所有点的期望次数如何求经过这条边的期望,那么下文就来看看如何求经过每个点的期望次数。
很明显可以列出这样的一个式子:

ans[i]=ans[to]du[to]ans[i] = \sum \dfrac{ans[to]}{du[to]}

注意 toto 是指所有与 ii 有边直接相连的点,toto 不包含 nn 号节点,因为这个式子的含义从是 toto 等概率地回到 ii 节点,可是 nn 号节点就停了,也就不存在再走回来的情况了
但是其实还有一种特殊情况,就是对于 11 号节点,其作为初始节点所以一定在开始时被经过一次,所以其不仅要计算从别的点到来的期望次数,更要算其一开始的这一次

ans[1]=1+ans[to]du[to]ans[1] = 1 + \sum \dfrac{ans[to]}{du[to]}

我们会发现上文的这个式子会出现循环依赖的情况,就是假设 AA 的值需要 BB 的值才能推出来,但是 BB 也同样需要 AA 才能推出来。而且我们考虑这个式子不含有最大值最小值的操作,所以就考虑使用高斯消元,把这个式子化成一个方程,然后求解.
那么既然要高斯消元就要考虑我们的未知数是什么,我们的系数是什么,常数是什么,这一切都是根据我上面的式子得出来的.
首先未知数肯定非常容易,就是我们不知道的数嘛,那我们不知道什么?就是 ansans 数组啊,所以 ansans 就是我们的未知数, ans[i]ans[i] 就代表我们的第 ii 个未知数.
这个明白了之后剩下的就非常简单的,考虑对上面的式子进行转化

ans[i]1du[to]×ans[to]=0ans[i] - \sum \dfrac{1}{du[to]} \times ans[to] = 0

很明显 dudu 数组我们是知道的,又发现有一个 1du[to]×ans[to]\dfrac{1}{du[to]} \times ans[to] 的项,所以 dudu 数组就理所应当的成为了我们的系数
会发现了常数项除了 ans[1]ans[1] 的方程含有一个 11 ,其他的都是 00.
注意在代码里我的 aa 数组开的二维,因为我们的高斯消元需要知道第几个方程,所以就按照第一个点的顺序给方程编了号,所以 aa 数组的第一维就是编号,第二维才是我们的未知数,这也就与正常的高斯消元一样了

代码详解:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include<bits/stdc++.h>
using namespace std;
const double eps = 1e-6;
const int MAXN = 505;
const int MAXM = 130000;
struct edge{
int nxt,to;
edge(int _nxt = 0,int _to = 0){
nxt = _nxt,to = _to;
}
}e[2 * MAXM];
double a[MAXN][MAXN];
double ans[MAXN],res[MAXM];
int from[MAXM],to[MAXM],head[MAXN],du[MAXN];
int n,m,cnt;
void Gauss(int n){ //推荐里面写一个 n ,然后直接按模板敲就好了
for(int i=1; i<=n; i++){
int mx = i;
for(int j=i + 1; j<=n; j++){
if(fabs(a[j][i]) > fabs(a[mx][i])){
mx = j;
}
}
if(mx != i) {
for(int j=1; j<=n+1; j++){
swap(a[i][j],a[mx][j]);
}
}
for(int j=1; j<=n; j++){
if(j != i){
double tmp = a[j][i] / a[i][i];
for(int k=i; k<=n+1; k++){
a[j][k] -= tmp * a[i][k];
}
}
}
}
for(int i=1; i<=n; i++){ //ans[i] 即我们最后解出来的解
ans[i] = a[i][n+1] / a[i][i];
}
}
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
int main(){
cin>>n>>m;
for(int i=1; i<=m; i++){
cin>>from[i]>>to[i];
add_edge(from[i],to[i]);
add_edge(to[i],from[i]);
du[from[i]]++;du[to[i]]++;
}
for(int i=1; i<n; i++){ //注意一点,因为到 n 就停了,所以 n 的期望等就不用算了
a[i][i] = 1; //根据我们的式子可以化出来
for(int j=head[i]; j; j = e[j].nxt){
int to = e[j].to;
if(to != n){ //注意 to != n
a[i][to] = -1.0/du[to]; //根据我们的式子可以化出来
}
}
}
a[1][n] = 1; //根据式子可以化出来
Gauss(n-1);
for(int i=1; i<=m; i++){
if(from[i] != n){
res[i] += ans[from[i]] / du[from[i]]; //到达这个点的期望除以度数,就是从这个点到当前点的期望
}
if(to[i] != n){
res[i] += ans[to[i]] / du[to[i]];
}
}
sort(res+1,res+m+1); //从小到大排边
//一定要相信 STL,如果自己写一个 cmp(例如我),就会造成神奇的精度问题
double h = 0;
for(int i=1; i<=m; i++){
//越大的期望的边给他越小的编号
h += res[i] * (m - i + 1);
}
printf("%.3f",h);
return 0;
}

我的上文对这个代码的解释应该也比较合理了,如果有任何疑问或者感觉我说的不对的地方欢迎大家留言或者私信我

作者

linyihdfj

发布于

2022-04-26

更新于

2023-09-14

许可协议