Floyd演算法學習筆記

Saltyfish6發表於2024-03-19

Floyd演算法學習筆記

前言

如有錯誤,歡迎各位 dalao 批評指出。

前置芝士:

1.鄰接矩陣(Floyd要用鄰接矩陣存圖)

2.動態規劃思想(最好學過,沒學過也沒有太大影響)

1. Floyd 所解決問題的型別

我們可以發現,如 Dijkstra,SPFA,Bellman Ford 一類的最短路演算法都是解決單源點最短路問題,也就是確定了起點或者終點來求最短路的問題。但是,我們發現,這些演算法解決多源點最短路問題,也就是有多個起點和終點的最短路問題 ,的效率太低。假設有 \(n\) 個點,\(m\) 條邊。解決多源最短路時,如果用以上三種演算法來解決,都需要分別做 \(n\) 次,來求解以每個點為起點的單源最短路,時間複雜度最慢分別是 \(O(nmlogn),O(n^2m),O(n^2m)\),在稠密圖 \(m=n*(n-1)/2≈n^2\)其中最快的都需要 \(O(n^3logm)\) ,最慢的甚至是 \(O(n^4)\) ,效率太低。因此,我們今天的主角 Floyd 就因解決多源最短路問題而誕生了!

2. Floyd 的思想和基本做法

Floyd演算法的基本思想是動態規劃。

\(dp_{ij}\) 表示 \(i\)\(j\) 的最短距離。(有 \(n\) 個點)

首先對於這個 \(dp\) 陣列的初始化就是將輸入的邊 \(x-y\) 權值為 \(z\) (無權圖就是 \(1\)),如果圖是無向,則 \(dp_{xy}=dp_{yx}=z\) ,如果圖是有向,則 \(dp_{xy}=z\),最後將所有 \(dp_{ii}=0 (0\le i\le n)\),比較顯然,這裡不做解釋。

接著我們進行狀態轉移。顯然,我們要轉移 \(dp_{ij}\),就需要找一個點 \(k\),來進行轉移,也就是 \(dp_{ij}\gets min(dp_{ij},dp_{ik}+dp_{kj})\),其中 \(dp_{ik}+dp_{kj}\) 就表示 \(i-k\) 的最短路與 \(j-k\) 的最短路之和,其實也就相當於一個鬆弛操作。

這裡特別要注意的是:我們的 \(k\) 那一層迴圈一定要放在 \(i\)\(j\) 兩層迴圈之外,因為如果放在 \(i,j\) 以內的話,你就會發現每兩個點的最短路只會被算到一次,而當你在進行狀態轉移時,你只算到一次的話,你會發現有些點在轉移時,還沒有被更新,就會出現沒有求出最短路的情況,所以,\(k\) 的迴圈要放到 \(i,j\) 的迴圈之外。

Floyd演算法由於 \(i,j,k\) 都要列舉一層迴圈,所以時間複雜度為 \(O(n^3)\),比開頭講的三個演算法要快。

3.Floyd 程式碼實現

//n表示有n個點 
memset(dp,0x3f,sizeof(dp));//賦一個極大值,並且防止在轉移時不溢位。
//接下來進行邊的初始化和dp[i][i]=0
............
//狀態轉移 
for(int k=1;k<=n;++k)//列舉k 
{
	for(int i=1;i<=n;++i)//列舉i 
	{
		if(i==k)//如果i,k個點相等,則不需要更新 
		continue;
		for(int j=1;j<=n;++j)
		{
			if(i==j||k==j)//同第八行 
			continue;
			dp[i][j]=min(dp[i][k]+dp[k][j],dp[i][j]);//狀態轉移。 
		}
	}
}

4.Floyd 演算法的一些其他應用

(1).Floyd 輸出路徑

對於輸出路徑,我們可以定義一個 \(path_{ij}\) 表示 \(dp_{ij}\) 是有 \(dp_{ipath_{ij}}+dp_{path_{ij}j}\) 轉移而來。並且在一開始,我們將所有 \(path\) 陣列的元素賦一個特殊值。(假定為 \(-1\) )

顯然,要輸出 \(i-j\) 的路徑,我們可以透過遞迴來解決。

具體程式碼如下:

//賦特殊值並且做 floyd
-----------
//輸出路徑函式
void print(int i,int j)//print(a,b) 表示輸出a,b的最短路徑
{
    if(path[i][j]==-1)//因為開始初始化為-1,這裡就可以避免相鄰的再次輸出 
        return;
    print(i,path[i][j]);//前半部分
    cout<<path[i][j]<<"-->";//輸出該點 
    print(path[i][j],j);//後半部分
}

(2).Floyd 判斷負環

在說這一個應用之前,我們先來講一下負環的定義。

負環,就是指一個圖中,存在一個環,使得這個環的權值之和為負數,這個環就是負環。

例如下圖:

qwq

可以發現,由圖中三個點組成的一個環的權值之和為負數,這就是一個簡單的負環。

一個圖中一旦存在負環,環裡的兩個點之間的最短路可以被無限更新,因為為了使得路徑長度最短,它可以一直走這個負環,無限迴圈,這個最短路徑就可以無限縮小。也就是說,一個圖一旦存在負環,它的最短路就是 \(-\infty\) ,也就相當於無解。

雖然說 SPFA Bellmanford 兩個最短路演算法可以判斷負環,但是我們還是要講一講 Floyd 演算法判斷負環的方法。

因為負環可以使得兩個點之間的最短路無限變小,所以我們可以發現,我們可以做兩次 Floyd ,第一次來更新所謂的最短路,然後再做第二次的時候,如果兩個點之間的最短路還可以被更新,就可以說明這個圖中存在負環,比較顯然。(自己想一想)。

第二次 Floyd 程式碼實現:

for(int k=1;k<=n;++k)
{
    for(int i=1;i<=n;++i)
    {
        if(i==k)//同第一次
        continue;
        for(int j=1;j<=n;++j)
        {
            if(i==j||j==k)//同上
            continue;
            if(dp[i][j]>dp[i][k]+dp[k][j])//如果可以被更新,則存在負環
            {
                puts("No solution!");
            }
        }
    }
}

(3).Floyd 判斷有向圖的連通性

我們知道,對於無向圖的兩個點是否聯通,我們可以運用並查集來判斷兩個點是否聯通。

但是對於一個有向圖,並查集是不能夠維護的,所以這個時候,我們就再一次請出今天的主角 Floyd 來判斷兩個點 \(i,j\) 是否連通。

我們定義 \(dp{ij}\) 表示 \(i,j\) 是否連通,若聯通則為 \(1\) ,不連通則為 \(0\)

顯然,我們可以根據 Floyd 一一樣的方式進行初始化。一開始除了 \(dp_{ii}\) 全部賦值為 \(0\) ,輸入邊的時候直接初始化即可。

接下來就是狀態轉移。我們可以模仿普通的 Floyd ,再找一個點 \(k\) ,如果 \(i\) 可以到達 \(k\) ,\(k\) 可以到達 \(j\) ,就可以說明 \(i\) 可以到達 \(j\) ,轉化成轉移方程就是 \(dp_{ij}|=dp_{ik}\&dp_{kj}\)

核心程式碼:

//n表示有n個點 
memset(dp,0,sizeof(dp));//賦值。
//接下來進行邊的初始化和dp[i][i]=1
............
//狀態轉移 
for(int k=1;k<=n;++k)//列舉k 
{
	for(int i=1;i<=n;++i)//列舉i 
	{
		if(i==k)//同上
		continue;
		for(int j=1;j<=n;++j)
		{
			if(i==j||k==j)//同上
			continue;
			dp[i][j]|=dp[i][k]&dp[k][j];//狀態轉移。 
		}
	}
}

關於 Floyd 演算法的講解就到這裡了,\(see~ you\)

相關文章