1. 程式人生 > 其它 >演算法專題——Floyd拓展

演算法專題——Floyd拓展

多源最短路演算法Floyd的相關拓展

Floyd演算法概念

用於求解任意兩點之間的最短路的演算法,演算法的原理本質為dp。下面進行簡要的原理說明。

狀態方程:dp[k][i][j]表示只經過前k個點,可以達到的i與j的最短路徑為多少。那麼不難得到轉移方程:dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j])即從不經過k點與經過k點中得到一個最優的解。發現轉移方程中k層的狀態僅與k-1層的狀態有關,因此我們可以使用滾動陣列的方法進行空間上的優化,最終可以得到:dp[i][j] = min(dp[i][j], dp[i][k] + dp[i][k] + dp[k][j])



求最短路

即Floyd最基本的應用,可用於求多源匯最短路,時間複雜度較大為O(n^3), 因此一般只能用於點數較少的情況. 這裡不多贅述, 直接貼出程式碼, 自己體會.

for (int t = 1; t <= n; t++)
for (int i = 1; i <= n; i++) 
for (int j = 1; j <= n; j++)
	if (dist[i][j] > dist[i][t] + dist[t][j])
		dist[i][j] = dist[i][t] + dist[t][j];

例題

Cow Tours

題面:

分析:

連線一條邊,使得兩個聯通塊相連,並讓得到的圖的直徑最小。

很顯然列舉一下連線兩個塊的所有邊,取得一個最小值即可,還要注意連線的邊對於新得到的圖的直徑並沒有貢獻,而是原先兩個圖的直徑的最大值。

給出核心程式碼

double MAX = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
	if (dist[i][j] != INF) 
		d1[i] = max(d1[i], dist[i][j]), MAX = max(MAX, d1[i]);

double ans = 0x3f3f3f3f;
for (int i = 1; i <= n; i++) 
for (int j = 1; j <= n; j++)
	if (dist[i][j] == INF) {
		ans = min(ans, cal(i, j) + d1[i] + d1[j]);

printf("%.6f", max(ans, MAX));

求傳遞閉包

Floyd演算法還可以求解原圖的傳遞閉包, 這是非常顯然的一個事實, 在求最短路的作用中, dist[i][j]的值為有效值時即表明了i含有一條路徑可以到達j, 即可以連一條從ij的邊. 傳遞閉包相比於最短路的應用, 陣列需要維護的數值更加簡單, 只需要維護一個bool變量表明是否含有一條邊可以從i指向j即可. 傳遞閉包的問題, 可以抽象為用於處理所有需要進行節點之間關係傳遞的問題, 如大小關係(A<B, B<C → A<C) .

例題

排序

題面:

分析:

分析給出的字元的大小關係,並依據大小關係將其打印出來。可以發現一個表示式給出時A<B,也表明了A會小於所有大於B的字元,即具有傳遞性,通過這種傳遞性我們可以得到整個序列的關係,當最終形成A < B < C < ... < E26個字元都出現的序列時,我們就表示這個序列是合法的,可以輸出。這種傳遞性我們可以通過傳遞閉包進行實現,即通過確定字元之間的關係是否已經得到滿足可以輸出的情況,而要得到字元之間的關係就要時字元之間之間傳遞大小資訊,即傳遞閉包的特點所在。

見核心程式碼:

for (int i = 1; i <= m; i++) {
	scanf("%s", &line);
    if (!flag) {
        gra[line[0] - 'A'][line[2] - 'A'] = 1;
        cnt ++;
        for (int k = 0; k < n; k++) 
        for (int ii = 0; ii < n; ii++) 
		for (int jj = 0; jj < n; jj++) 
            gra[ii][jj] = gra[ii][k] & gra[k][jj];
        flag = Check();
    }
}

此外還有一種時間複雜度更低的寫法,對於每次新加入的一條邊,可以發現,新增的邊數是O(n)級別的,僅與新加入的邊的兩個端點有關的點才會被更新。得到如下優化程式碼:

for (int i = 1; i <= m; i++) {
	scanf("%s", &line);
    if (!flag) {
        gra[line[0] - 'A'][line[2] - 'A'] = 1;
        cnt ++;
        int u = line[0] - 'A', v = line[2] - 'A';
        for (int ii = 0; ii < n; ii++) {
            if (gra[ii][u]) gra[ii][v] = 1;
            if (gra[v][ii]) gra[u][ii] = 1;
        	for (int jj = 0; jj < n; jj++) 
            	if (gra[ii][u] && gra[v][jj]) gra[ii][jj] = 1;
    	}
        flag = Check();
    }
}

求最小環

可以得到一種樸素的思路。是否可以得到任意一個節點,假設其為環上的一點然後列舉另外兩個節點,以其為環上另外兩個點,而後通過min(g[i][j] + g[i][k] + g[k][j])得到最小環。核心程式碼見下。

for (int k = 1; k <= n; k++) 
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) 
	if (g[i][j] + g[j][k] + g[k][i] < res) {
		res = g[i][j] + g[j][k] + g[k][j];
		getPath(i, j, k);					//求以ijk確定的環經過的路徑
	}

這中思路十分簡單明瞭,但是有一個毛病,即並不能通過i,j,k互通,得到i,j,k在一個環上,即不能通過三個點從而確定一個環。

但是可以發現,如果i,j,k可以確定一個環,那麼該最小環的求法確實是min(g[i][j] + g[i][k] + g[k][j]),所以在此基礎上進行修改,只要讓i,j,k可以確定一個環即可。可以得到,如果ikjk是直接相連的,即g[i][k] / g[j][k] 路徑上僅有一條邊,那麼i,j,k就可以確定一個環。所以我們可以建立兩個陣列,一個儲存原圖——g陣列,一個儲存最短路徑——d陣列。

for (int k = 1; k <= n; k++) 
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) 
	if (d[i][j] + g[j][k] + g[k][i] < res) {
		res = d[i][j] + g[j][k] + g[k][j];
		cnt = 0;
		path[cnt++] = k;
		path[cnt++] = i;
		getPath(i, j);					//求以ij經過的路徑
		path[cht++] = j;
	}

基於此,我們也可以在Floyd演算法上進行修改,減少一些時間複雜度。

要求得圖上的最小環, 首先要不遺漏的遍歷到所有的環, 而為了減少冗餘的重複計算, 可以對要求的情況進行一個分類. 以環中編號最大的節點為依據進行分類,第k類表示以k為最大編號的環,而後得到另外兩個節點i,j,使g[i][k] / g[j][k]路徑僅含一條邊,進而得到最小環為min(g[i][j] + g[i][k] + g[k][j])。我們發現Floyd演算法的性質,在if(g[i][j] > g[i][k] + g[k][j])的鬆弛操作中,i,j的最短路徑中的中間節點總是不大於當前迴圈中的k,即在前k-1個節點的鬆弛操作完成之後,第k個節點的鬆弛操作還未開始時,d[i][j]如果存在有效值,即存在路徑時,該路徑的最大中間節點一定不超過k,又讓i,j小於k,於是就可以達成第k類的分類思想。核心程式碼見下。

memcpy(d, g, size d);
for (int  k = 1; k <= n; k++) {
	for (int i = 1; i < k; i++) 
	for (int j = i + 1; j < k; j++) {
		if (res > g[i][k] + g[k][j] + d[i][j]) {
			res = g[i][k] + g[k][j] + d[i][j];
            cnt = 0;
            path[cnt++] = k;
            path[cnt++] = i;
            getPath(i, j);					//求以ij經過的路徑
            path[cht++] = j;
		}
	}
	for (int i = 1; i <= n; i++)
	for (int j = 1; j <= n; j++) {
		if (d[i][j] > d[i][k] + d[k][j]) {
			d[i][j] = d[i][k] + d[k][j];
			pos[i][j] = k;
		}
	}
}

例題

觀光之旅

題面:

分析:

非常模板的最小環問題,直接求解就行。


動態規劃思想

Floyd演算法是基於dp得到的,其中第k個節點的思想很值得我們的借鑑,至於更多的關於圖論動態規劃的思想,在演算法專題——最短路已經提到過,這裡就不重複提及了,下面直接來看一道題。

例題

Cow Relays

題面:

分析:

還沒寫完,欠著先