【題解】金牌導航-高斯消元/Luogu P3232 遊走

linyihdfj發表於2022-04-26

題目描述:


詳細分析:

我們對於編號的分配,很明顯可以發現如下的分配就是期望最小的:對經過的期望次數越大的邊賦予更小的編號。
那麼問題就轉化為了怎麼求一條邊的經過的期望次數,我們發現邊數非常大所以肯定不好弄,所以我們就轉而看很少的點。因為我們會發現如果我們能知道經過每個點的期望次數,那麼這條邊的期望次數很輕鬆就能表達出來。
比如如下的式子:(設 \(ans[i]\) 為經過第 \(i\) 個點的期望次數, \(du[i]\) 為第 \(i\) 個點的度數, \(res[i]\) 為經過第 \(i\) 條邊的期望次數)

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

這個式子應該很好理解,就是說每個點等概率地選擇和他相連的邊,所以選擇這一條邊地期望次數就是經過它的期望次數除以它的度數,也就是與他相連的邊的數量,因為這條邊可以從兩個端點開始走然後經過,所以應該加上兩個端點的值。
上文探討了如果知道經過所有點的期望次數如何求經過這條邊的期望,那麼下文就來看看如何求經過每個點的期望次數。
很明顯可以列出這樣的一個式子:

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

注意 \(to\) 是指所有與 \(i\) 有邊直接相連的點,\(to\) 不包含 \(n\) 號節點,因為這個式子的含義從是 \(to\) 等概率地回到 \(i\) 節點,可是 \(n\) 號節點就停了,也就不存在再走回來的情況了
但是其實還有一種特殊情況,就是對於 \(1\) 號節點,其作為初始節點所以一定在開始時被經過一次,所以其不僅要計算從別的點到來的期望次數,更要算其一開始的這一次

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

我們會發現上文的這個式子會出現迴圈依賴的情況,就是假設 \(A\) 的值需要 \(B\) 的值才能推出來,但是 \(B\) 也同樣需要 \(A\) 才能推出來。而且我們考慮這個式子不含有最大值最小值的操作,所以就考慮使用高斯消元,把這個式子化成一個方程,然後求解.
那麼既然要高斯消元就要考慮我們的未知數是什麼,我們的係數是什麼,常數是什麼,這一切都是根據我上面的式子得出來的.
首先未知數肯定非常容易,就是我們不知道的數嘛,那我們不知道什麼?就是 \(ans\) 陣列啊,所以 \(ans\) 就是我們的未知數, \(ans[i]\) 就代表我們的第 \(i\) 個未知數.
這個明白了之後剩下的就非常簡單的,考慮對上面的式子進行轉化

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

很明顯 \(du\) 陣列我們是知道的,又發現有一個 \(\dfrac{1}{du[to]} \times ans[to]\) 的項,所以 \(du\) 陣列就理所應當的成為了我們的係數
會發現了常數項除了 \(ans[1]\) 的方程含有一個 \(1\) ,其他的都是 \(0\).
注意在程式碼裡我的 \(a\) 陣列開的二維,因為我們的高斯消元需要知道第幾個方程,所以就按照第一個點的順序給方程編了號,所以 \(a\) 陣列的第一維就是編號,第二維才是我們的未知數,這也就與正常的高斯消元一樣了

程式碼詳解:

點選檢視程式碼
#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;
}

我的上文對這個程式碼的解釋應該也比較合理了,如果有任何疑問或者感覺我說的不對的地方歡迎大家留言或者私信我

相關文章