国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

高階數(shù)據(jù)結(jié)構(gòu)——圖

這篇具有很好參考價(jià)值的文章主要介紹了高階數(shù)據(jù)結(jié)構(gòu)——圖。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

圖的基本概念

圖的基本概念

圖是由頂點(diǎn)集合和邊的集合組成的一種數(shù)據(jù)結(jié)構(gòu),記作 G = ( V , E ) G=(V, E)G=(V,E) 。

有向圖和無(wú)向圖:

  • 在有向圖中,頂點(diǎn)對(duì) < x , y >是有序的,頂點(diǎn)對(duì) < x , y > 稱(chēng)為頂點(diǎn) x 到頂點(diǎn) y 的一條邊,< x , y > 和 <y,x>是兩條不同的邊。
  • 在無(wú)向圖中,頂點(diǎn)對(duì) ( x , y ) 是無(wú)序的,頂點(diǎn)對(duì) ( x , y ) 稱(chēng)為頂點(diǎn) x 和頂點(diǎn) y 相關(guān)聯(lián)的一條邊,這條邊沒(méi)有特定方向,( x , y ) 和 ( y , x ) 是同一條邊。

高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

完全圖:

  • 在有 n 個(gè)頂點(diǎn)的無(wú)向圖中,若有 n × ( n ? 1 ) ÷ 2 條邊,即任意兩個(gè)頂點(diǎn)之間都有直接相連的邊,則稱(chēng)此圖為無(wú)向完全圖。
  • 在有 n 個(gè)頂點(diǎn)的有向圖中,若有 n × ( n ? 1 )條邊,即任意兩個(gè)頂點(diǎn)之間都有雙向的邊,則稱(chēng)此圖為有向完全圖。

如下圖:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

鄰接頂點(diǎn):

  • 在無(wú)向圖中,若 ( u , v ) 是圖中的一條邊,則稱(chēng) u 和 v 互為鄰接頂點(diǎn),并稱(chēng)邊 ( u , v ) 依附于頂點(diǎn) u 和頂點(diǎn)v 。
  • 在有向圖中,若 < u , v > 是圖中的一條邊,則稱(chēng)頂點(diǎn) u 鄰接到頂點(diǎn) v ,頂點(diǎn) v 鄰接自頂點(diǎn) u ,并稱(chēng)邊 <u , v > 與頂點(diǎn) u 和頂點(diǎn) v 相關(guān)聯(lián)。

頂點(diǎn)的度:

  • 在有向圖中,頂點(diǎn)的度等于該頂點(diǎn)的入度與出度之和,頂點(diǎn)的入度是以該頂點(diǎn)為終點(diǎn)的邊的條數(shù),頂點(diǎn)的出度是以該頂點(diǎn)為起點(diǎn)的邊的條數(shù)。
  • 在無(wú)向圖中,頂點(diǎn)的度等于與該頂點(diǎn)相關(guān)聯(lián)的邊的條數(shù),同時(shí)也等于該頂點(diǎn)的入度和出度。

路徑與路徑長(zhǎng)度:

  • 若從頂點(diǎn) vi 出發(fā)有一組邊使其可到達(dá)頂點(diǎn) vj ,則稱(chēng)頂點(diǎn) vi 到頂點(diǎn) vj 的頂點(diǎn)序列為從頂點(diǎn) vi 到頂點(diǎn) vj 的路徑。
  • 對(duì)于不帶權(quán)的圖,一條路徑的長(zhǎng)度是指該路徑上的邊的條數(shù);對(duì)于帶權(quán)的圖,一條路徑的長(zhǎng)度是指該路徑上各個(gè)邊權(quán)值的總和。

帶權(quán)圖示例:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

簡(jiǎn)單路徑與回路:

  • 若路徑上的各個(gè)頂點(diǎn)均不相同,則稱(chēng)這樣的路徑為簡(jiǎn)單路徑。
  • 若路徑上第一個(gè)頂點(diǎn)與最后一個(gè)頂點(diǎn)相同,則稱(chēng)這樣的路徑為回路或環(huán)。

如下圖:高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

子圖:

  • 設(shè)圖G=(V,E) 和圖G1=(V1,E1),若 V 1 ? V 且 E1?E ,則稱(chēng) G1 是 G 的子圖。

如下圖:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

連通圖和強(qiáng)連通圖:

  • 在無(wú)向圖中,若從頂點(diǎn) v1 到頂點(diǎn) v2 有路徑,則稱(chēng)頂點(diǎn) v1 與頂點(diǎn) v2 是連通的,如果圖中任意一對(duì)頂點(diǎn)都是連通的,則稱(chēng)此圖為連通圖。
  • 在有向圖中,若每一對(duì)頂點(diǎn)vi 和 vj 之間都存在一條從vi到vj 的路,也存在一條從 vj 到 vi 的路,則稱(chēng)此圖是強(qiáng)連通圖。

生成樹(shù)與最小生成樹(shù):

  • 一個(gè)連通圖的最小連通子圖稱(chēng)為該圖的生成樹(shù),有 n 個(gè)頂點(diǎn)的連通圖的生成樹(shù)有 n 個(gè)頂點(diǎn)和 n ? 1 條邊。
  • 最小生成樹(shù)指的是一個(gè)圖的生成樹(shù)中,總權(quán)值最小的生成樹(shù)。

圖的相關(guān)應(yīng)用場(chǎng)景

圖常見(jiàn)的表示場(chǎng)景如下:

  • 交通網(wǎng)絡(luò):圖中的每個(gè)頂點(diǎn)表示一個(gè)地點(diǎn),圖中的邊表示這兩個(gè)地點(diǎn)之間是否有直接相連的公路,邊的權(quán)值可以是這兩個(gè)地點(diǎn)之間的距離、高鐵時(shí)間等。
  • 網(wǎng)絡(luò)設(shè)備拓?fù)洌簣D中的每個(gè)頂點(diǎn)表示網(wǎng)絡(luò)中的一個(gè)設(shè)備,圖中的邊表示這兩個(gè)設(shè)備之間是否可以互傳數(shù)據(jù),邊的權(quán)值可以是這兩個(gè)設(shè)備之間傳輸數(shù)據(jù)所需的時(shí)間、丟包的概率等。
  • 社交網(wǎng)絡(luò):圖中的每個(gè)頂點(diǎn)表示一個(gè)人,圖中的邊表示這兩個(gè)人是否互相認(rèn)識(shí),邊的權(quán)值可以是這兩個(gè)人之間的親密度、共同好友個(gè)數(shù)等。

關(guān)于有向圖和無(wú)向圖:

  • 交通網(wǎng)絡(luò)對(duì)應(yīng)的圖可以是有向圖,也可以是無(wú)向圖,無(wú)向圖對(duì)應(yīng)就是雙向車(chē)道,有向圖對(duì)應(yīng)就是單向車(chē)道。
  • 網(wǎng)絡(luò)設(shè)備拓?fù)鋵?duì)應(yīng)的圖通常是無(wú)向圖,兩個(gè)設(shè)備之間有邊表示這兩個(gè)設(shè)備之間可以互相收發(fā)數(shù)據(jù)。
  • 社交網(wǎng)絡(luò)對(duì)應(yīng)的圖可以是有向圖,也可以是無(wú)向圖,無(wú)向圖通常表示一些強(qiáng)社交關(guān)系,比如QQ、微信等(一定互為好友),有向圖通常表示一些弱社交關(guān)系,比如微博、抖音(不一定互相關(guān)注)。

圖的其他相關(guān)作用:

  • 在交通網(wǎng)絡(luò)中,根據(jù)最短路徑算法計(jì)算兩個(gè)地點(diǎn)之間的最短路徑,根據(jù)最小生成樹(shù)算法得到將各個(gè)地點(diǎn)連通起來(lái)所需的最小成本。
  • 在社交網(wǎng)絡(luò)中,根據(jù)廣度優(yōu)先搜索得到兩個(gè)人之間的共同好友進(jìn)行好友推薦,根據(jù)入邊表和出邊表得知有哪些粉絲以及關(guān)注了哪些博主。

圖與樹(shù)的聯(lián)系與區(qū)別

圖與樹(shù)的主要聯(lián)系與區(qū)別如下:

  • 樹(shù)是一種有向無(wú)環(huán)且連通的圖(空樹(shù)除外),但圖并不一定是樹(shù)。
  • 有 n 個(gè)結(jié)點(diǎn)的樹(shù)必須有 n ? 1 條邊,而圖中邊的數(shù)量不取決于頂點(diǎn)的數(shù)量。
  • 樹(shù)通常用于存儲(chǔ)數(shù)據(jù),并快速查找目標(biāo)數(shù)據(jù),而圖通常用于表示某種場(chǎng)景。

圖的存儲(chǔ)結(jié)構(gòu)

圖由頂點(diǎn)和邊組成,存儲(chǔ)圖本質(zhì)就是將圖中的頂點(diǎn)和邊存儲(chǔ)起來(lái)。

鄰接矩陣

鄰接矩陣

鄰接矩陣存儲(chǔ)圖的方式如下:

  1. 用一個(gè)數(shù)組存儲(chǔ)頂點(diǎn)集合,頂點(diǎn)所在位置的下標(biāo)作為該頂點(diǎn)的編號(hào)(所給頂點(diǎn)可能不是整型)。
  2. 用一個(gè)二維數(shù)組matrix存儲(chǔ)邊的集合,其中matrix[i][j] 表示編號(hào)為 i 和 j 的兩個(gè)頂點(diǎn)之間的關(guān)系。

如下圖:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

說(shuō)明一下:

  • 對(duì)于不帶權(quán)的圖,兩個(gè)頂點(diǎn)之間要么相連,要么不相連,可以用0和1表示,m a t r i x [ i ] [ jmatrix[i][j] 為1表示編號(hào)為 i 和 j 的兩個(gè)頂點(diǎn)相連,為0表示不相連。
  • 對(duì)于帶權(quán)的圖,連接兩個(gè)頂點(diǎn)的邊會(huì)帶有一個(gè)權(quán)值,可以用這個(gè)權(quán)值來(lái)設(shè)置對(duì)應(yīng)matrix[i][j] 的值,如果兩個(gè)頂點(diǎn)不相連,則使用不會(huì)出現(xiàn)的權(quán)值進(jìn)行設(shè)置即可(圖中為無(wú)窮大)。
  • 對(duì)于無(wú)向圖來(lái)說(shuō),頂點(diǎn) i 和頂點(diǎn) j 相連,那么頂點(diǎn) j 就和頂點(diǎn) i 相連,因此無(wú)向圖對(duì)應(yīng)的鄰接矩陣是一個(gè)對(duì)稱(chēng)矩陣,即matrix[i][j] 的值等于 matrix[j][i] 的值。
  • 在鄰接矩陣中,第 i 行元素中有效權(quán)值的個(gè)數(shù)就是編號(hào)為 i 的頂點(diǎn)的出度,第 i ii 列元素中有效元素的個(gè)數(shù)就是編號(hào)為 i 的頂點(diǎn)的入度。

鄰接矩陣的優(yōu)缺點(diǎn)

鄰接矩陣的優(yōu)點(diǎn):

  • 鄰接矩陣適合存儲(chǔ)稠密圖,因?yàn)榇鎯?chǔ)稠密圖和稀疏圖時(shí)所開(kāi)辟的二維數(shù)組大小是相同的,因此圖中的邊越多,鄰接矩陣的優(yōu)勢(shì)就越明顯。
  • 鄰接矩陣能夠 O(1) 的判斷兩個(gè)頂點(diǎn)是否相連,并獲得相連邊的權(quán)值。

鄰接矩陣的缺點(diǎn):

  • 鄰接矩陣不適合查找一個(gè)頂點(diǎn)連接出去的所有邊,需要遍歷矩陣中對(duì)應(yīng)的一行,該過(guò)程的時(shí)間復(fù)雜度是O(N),其中 N 表示的是頂點(diǎn)的個(gè)數(shù)。

鄰接矩陣的實(shí)現(xiàn)

鄰接矩陣所需成員變量:

  • 數(shù)組 vertexs :用于存儲(chǔ)頂點(diǎn)集合,頂點(diǎn)所在位置的下標(biāo)作為該頂點(diǎn)的編號(hào)。
  • 映射關(guān)系vIndexMap :用于建立頂點(diǎn)與其下標(biāo)的映射關(guān)系,便于根據(jù)頂點(diǎn)找到其對(duì)應(yīng)的下標(biāo)編號(hào)。
  • 鄰接矩陣matrix :用于存儲(chǔ)邊的集合,matrix[i][j] 表示編號(hào)為 i 和 j 的兩個(gè)頂點(diǎn)之間的關(guān)系。

鄰接矩陣的實(shí)現(xiàn):

  • 為了支持任意類(lèi)型的頂點(diǎn)類(lèi)型以及權(quán)值,可以將圖定義為模板類(lèi),其中V 和W 分別表示頂點(diǎn)和權(quán)值的類(lèi)型,MAX_W 表示兩個(gè)頂點(diǎn)沒(méi)有連接時(shí)鄰接矩陣中存儲(chǔ)的值,將MAX_W 的缺省值設(shè)置為 INT_MAX (權(quán)值一般為整型),Direction 表示圖是否為有向圖,將Direction 的缺省值設(shè)置為false (無(wú)向圖居多)。
  • 在構(gòu)造函數(shù)中完成頂點(diǎn)集合的設(shè)置,并建立各個(gè)頂點(diǎn)與其對(duì)應(yīng)下標(biāo)的映射關(guān)系,同時(shí)為鄰接矩陣開(kāi)辟空間,將矩陣中的值初始化為MAX_W ,表示剛開(kāi)始時(shí)各個(gè)頂點(diǎn)之間均不相連。
  • 提供一個(gè)接口用于添加邊,在添加邊時(shí)先分別獲取源頂點(diǎn)和目標(biāo)頂點(diǎn)對(duì)應(yīng)的下標(biāo)編號(hào),然后再將鄰接矩陣中對(duì)應(yīng)位置設(shè)置為邊的權(quán)值,如果圖為無(wú)向圖,則還需要在鄰接矩陣中添加目標(biāo)頂點(diǎn)到源頂點(diǎn)的邊。
  • 在獲取頂點(diǎn)對(duì)應(yīng)的下標(biāo)時(shí),先在vIndexMap 中進(jìn)行查找,如果找到了對(duì)應(yīng)的頂點(diǎn),則返回該頂點(diǎn)對(duì)應(yīng)的下標(biāo)編號(hào),如果沒(méi)有找到對(duì)應(yīng)的頂點(diǎn),則說(shuō)明所給頂點(diǎn)不存在,此時(shí)可以拋出異常。

代碼如下:

//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//構(gòu)造函數(shù)
		Graph(const V* vertexs, int n)
			:_vertexs(vertexs, vertexs + n) //設(shè)置頂點(diǎn)集合
			,_matrix(n, vector<int>(n, MAX_W)) { //開(kāi)辟二維數(shù)組空間
			//建立頂點(diǎn)與下標(biāo)的映射關(guān)系
			for (int i = 0; i < n; i++) {
				_vIndexMap[vertexs[i]] = i;
			}
		}
		//獲取頂點(diǎn)對(duì)應(yīng)的下標(biāo)
		int getVertexIndex(const V& v) {
			auto iter = _vIndexMap.find(v);
			if (iter != _vIndexMap.end()) { //頂點(diǎn)存在
				return iter->second;
			}
			else { //頂點(diǎn)不存在
				throw invalid_argument("不存在的頂點(diǎn)");
				return -1;
			}
		}
		//添加邊
		void addEdge(const V& src, const V& dst, const W& weight) {
			int srci = getVertexIndex(src), dsti = getVertexIndex(dst); //獲取源頂點(diǎn)和目標(biāo)頂點(diǎn)的下標(biāo)
			_matrix[srci][dsti] = weight; //設(shè)置鄰接矩陣中對(duì)應(yīng)的值
			if (Direction == false) { //無(wú)向圖
				_matrix[dsti][srci] = weight; //添加從目標(biāo)頂點(diǎn)到源頂點(diǎn)的邊
			}
		}
		//打印頂點(diǎn)集合和鄰接矩陣
		void print() {
			int n = _vertexs.size();
			//打印頂點(diǎn)集合
			for (int i = 0; i < n; i++) {
				cout << "[" << i << "]->" << _vertexs[i] << endl;
			}
			cout << endl;

			//打印鄰接矩陣
			//橫下標(biāo)
			cout << "  ";
			for (int i = 0; i < n; i++) {
				//cout << i << " ";
				printf("%4d", i);
			}
			cout << endl;
			for (int i = 0; i < n; i++) {
				cout << i << " "; //豎下標(biāo)
				for (int j = 0; j < n; j++) {
					if (_matrix[i][j] == MAX_W) {
						printf("%4c", '*');
					}
					else {
						printf("%4d", _matrix[i][j]);
					}
				}
				cout << endl;
			}
			cout << endl;
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
} 

說(shuō)明一下:

  • 為了方便觀察,可以在類(lèi)中增加一個(gè) p r i n t printprint 接口,用于打印頂點(diǎn)集合和鄰接矩陣。
  • 后續(xù)圖的相關(guān)算法都會(huì)以鄰接矩陣為例進(jìn)行講解,因?yàn)橐话阒挥斜容^稠密的圖才會(huì)存在最小生成樹(shù)和最短路徑的問(wèn)題。

鄰接表

鄰接表

鄰接表存儲(chǔ)圖的方式如下:

  1. 用一個(gè)數(shù)組存儲(chǔ)頂點(diǎn)集合,頂點(diǎn)所在的位置的下標(biāo)作為該頂點(diǎn)的編號(hào)(所給頂點(diǎn)可能不是整型)。
  2. 用一個(gè)出邊表存儲(chǔ)從各個(gè)頂點(diǎn)連接出去的邊,出邊表中下標(biāo)為 i ii 的位置存儲(chǔ)的是從編號(hào)為 i ii 的頂點(diǎn)連接出去的邊。
  3. 用一個(gè)入邊表存儲(chǔ)連接到各個(gè)頂點(diǎn)的邊,入邊表中下標(biāo)為 i ii 的位置存儲(chǔ)的是連接到編號(hào)為 i ii 的頂點(diǎn)的邊。

如下圖:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

說(shuō)明一下:

  • 出邊表和入邊表類(lèi)似于哈希桶,其中每個(gè)位置存儲(chǔ)的都是一個(gè)鏈表,出邊表中下標(biāo)為 i 的位置的鏈表中存儲(chǔ)的都是從編號(hào)為 i 的頂點(diǎn)連接出去的邊,入邊表中下標(biāo)為 i 的位置的鏈表中存儲(chǔ)的都是連接到編號(hào)為 i 的頂點(diǎn)的邊。
  • 在鄰接表中,出邊表中下標(biāo)為 i ii 的位置的鏈表中元素的個(gè)數(shù)就是編號(hào)為 i 的頂點(diǎn)的出度,入邊表中下標(biāo)為 i 的的位置的鏈表中元素的個(gè)數(shù)就是編號(hào)為 i 的頂點(diǎn)的入度。
  • 在實(shí)現(xiàn)鄰接表時(shí),一般只需要用一個(gè)出邊表來(lái)存儲(chǔ)從各個(gè)頂點(diǎn)連接出去的邊即可,因?yàn)榇蠖鄶?shù)情況下都是需要從一個(gè)頂點(diǎn)出發(fā)找與其相連的其他頂點(diǎn),所以一般不需要存儲(chǔ)入邊表。

鄰接表的優(yōu)缺點(diǎn)

鄰接表的優(yōu)點(diǎn):

  • 鄰接表適合存儲(chǔ)稀疏圖,因?yàn)猷徑颖泶鎯?chǔ)圖時(shí)開(kāi)辟的空間大小取決于邊的數(shù)量,圖中邊的數(shù)量越少,鄰接表存儲(chǔ)邊時(shí)所需的內(nèi)存空間就越少。
  • 鄰接表適合查找一個(gè)頂點(diǎn)連接出去的所有邊,出邊表中下標(biāo)為 i ii 的位置的鏈表中存儲(chǔ)的就是從頂點(diǎn) i ii 連接出去的所有邊。

鄰接表的缺點(diǎn):

  • 鄰接表不適合確定兩個(gè)頂點(diǎn)是否相連,需要遍歷出邊表中源頂點(diǎn)對(duì)應(yīng)位置的鏈表,該過(guò)程的時(shí)間復(fù)雜度是 O(E) ,其中 E 表示從源頂點(diǎn)連接出去的邊的數(shù)量。

鄰接表的實(shí)現(xiàn)

鏈表結(jié)點(diǎn)所需成員變量:

  • 源頂點(diǎn)下標(biāo) srci :表示邊的源頂點(diǎn)。
  • 目標(biāo)頂點(diǎn)下標(biāo) dsti :表示邊的目標(biāo)頂點(diǎn)。
  • 權(quán)值 weight :表示邊的權(quán)值。
  • 指針 next :連接下一個(gè)結(jié)點(diǎn)。

代碼如下:

//鄰接表
namespace LinkTable {
	//鏈表結(jié)點(diǎn)定義
	template<class W>
	struct Edge {
		//int _srci;    //源頂點(diǎn)的下標(biāo)(可選)
		int _dsti;      //目標(biāo)頂點(diǎn)的下標(biāo)
		W _weight;      //邊的權(quán)值
		Edge<W>* _next; //連接指針

		Edge(int dsti, const W& weight)
			:_dsti(dsti)
			,_weight(weight)
			,_next(nullptr)
		{}
	};
} 

說(shuō)明一下:

  • 對(duì)于出邊表來(lái)說(shuō),下標(biāo)為 i 的位置的鏈表中存儲(chǔ)的邊的源頂點(diǎn)都是頂點(diǎn) i ,所以鏈表結(jié)點(diǎn)中的源頂點(diǎn)成員可以不用存儲(chǔ)。
  • 對(duì)于入邊表來(lái)說(shuō),下標(biāo)為 i 的位置的鏈表中存儲(chǔ)的邊的目標(biāo)頂點(diǎn)都是頂點(diǎn) i ,所以鏈表結(jié)點(diǎn)中的目標(biāo)頂點(diǎn)成員可以不用存儲(chǔ)。

鄰接表所需成員變量:

  • 數(shù)組vertexs :用于存儲(chǔ)頂點(diǎn)集合,頂點(diǎn)所在位置的下標(biāo)作為該頂點(diǎn)的編號(hào)。
  • 映射關(guān)系vIndexMap :用于建立頂點(diǎn)與其下標(biāo)的映射關(guān)系,便于根據(jù)頂點(diǎn)找到其對(duì)應(yīng)的下標(biāo)編號(hào)。
  • 鄰接表(出邊表)linkTable :用于存儲(chǔ)邊的集合,linkTable[i] 鏈表中存儲(chǔ)的邊的源頂點(diǎn)都是頂點(diǎn) i 。

鄰接表的實(shí)現(xiàn):

  • 為了支持任意類(lèi)型的頂點(diǎn)類(lèi)型以及權(quán)值,可以將圖定義為模板,其中 V 和 W 分別表示頂點(diǎn)和權(quán)值的類(lèi)型, Direction表示圖是否為有向圖,將Direction 的缺省值設(shè)置為false (無(wú)向圖居多)。
  • 在構(gòu)造函數(shù)中完成頂點(diǎn)集合的設(shè)置,并建立各個(gè)頂點(diǎn)與其對(duì)應(yīng)下標(biāo)的映射關(guān)系,同時(shí)為鄰接表開(kāi)辟空間,將鄰接表中的值初始化為空指針,表示剛開(kāi)始時(shí)各個(gè)頂點(diǎn)之間均不相連。
  • 提供一個(gè)接口用于添加邊,在添加邊時(shí)先分別獲取源頂點(diǎn)和目標(biāo)頂點(diǎn)對(duì)應(yīng)的下標(biāo)編號(hào),然后在源頂點(diǎn)對(duì)應(yīng)的鏈表中頭插一個(gè)邊結(jié)點(diǎn),如果圖為無(wú)向圖,則還需要在目標(biāo)頂點(diǎn)對(duì)應(yīng)的鏈表中頭插一個(gè)邊結(jié)點(diǎn)。

代碼如下:

//鄰接表
namespace LinkTable {
	template<class V, class W, bool Direction = false>
	class Graph {
		typedef Edge<W> Edge;
	public:
		//構(gòu)造函數(shù)
		Graph(const V* vertexs, int n)
			:_vertexs(vertexs, vertexs + n) //設(shè)置頂點(diǎn)集合
			, _linkTable(n, nullptr) { //開(kāi)辟鄰接表的空間
			//建立頂點(diǎn)與下標(biāo)的映射關(guān)系
			for (int i = 0; i < n; i++) {
				_vIndexMap[vertexs[i]] = i;
			}
		}
		//獲取頂點(diǎn)對(duì)應(yīng)的下標(biāo)
		int getVertexIndex(const V& v) {
			auto iter = _vIndexMap.find(v);
			if (iter != _vIndexMap.end()) { //頂點(diǎn)存在
				return iter->second;
			}
			else { //頂點(diǎn)不存在
				throw invalid_argument("不存在的頂點(diǎn)");
				return -1;
			}
		}
		//添加邊
		void addEdge(const V& src, const V& dst, const W& weight) {
			int srci = getVertexIndex(src), dsti = getVertexIndex(dst); //獲取源頂點(diǎn)和目標(biāo)頂點(diǎn)的下標(biāo)

			//添加從源頂點(diǎn)到目標(biāo)頂點(diǎn)的邊
			Edge* sdEdge = new Edge(dsti, weight);
			sdEdge->_next = _linkTable[srci];
			_linkTable[srci] = sdEdge;

			if (Direction == false) { //無(wú)向圖
				//添加從目標(biāo)頂點(diǎn)到源頂點(diǎn)的邊
				Edge* dsEdge = new Edge(srci, weight);
				dsEdge->_next = _linkTable[dsti];
				_linkTable[dsti] = dsEdge;
			}
		}
		//打印頂點(diǎn)集合和鄰接表
		void print() {
			int n = _vertexs.size();
			//打印頂點(diǎn)集合
			for (int i = 0; i < n; i++) {
				cout << "[" << i << "]->" << _vertexs[i] << " ";
			}
			cout << endl << endl;

			//打印鄰接表
			for (int i = 0; i < n; i++) {
				Edge* cur = _linkTable[i];
				cout << "[" << i << ":" << _vertexs[i] << "]->";
				while (cur) {
					cout << "[" << cur->_dsti << ":" << _vertexs[cur->_dsti] << ":" << cur->_weight << "]->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<Edge*> _linkTable;         //鄰接表(出邊表)
	};
} 

說(shuō)明一下:
為了方便觀察,可以在類(lèi)中增加一個(gè)print 接口,用于打印頂點(diǎn)集合和鄰接表。

圖的遍歷

圖的遍歷指的是遍歷圖中的頂點(diǎn),主要有廣度優(yōu)先遍歷和深度優(yōu)先遍歷兩種方式。

廣度優(yōu)先遍歷

廣度優(yōu)先遍歷

廣度優(yōu)先遍歷又稱(chēng)BFS,其遍歷過(guò)程類(lèi)似于二叉樹(shù)的層序遍歷,從起始頂點(diǎn)開(kāi)始一層一層向外進(jìn)行遍歷。
如下圖:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

廣度優(yōu)先遍歷的實(shí)現(xiàn):

  • 廣度優(yōu)先遍歷需要借助一個(gè)隊(duì)列和一個(gè)標(biāo)記數(shù)組,利用隊(duì)列先進(jìn)先出的特點(diǎn)實(shí)現(xiàn)一層一層向外遍歷,利用標(biāo)記數(shù)組來(lái)記錄各個(gè)頂點(diǎn)是否被訪問(wèn)過(guò)。
  • 剛開(kāi)始時(shí)將起始頂點(diǎn)入隊(duì)列,并將起始頂點(diǎn)標(biāo)記為訪問(wèn)過(guò),然后不斷從隊(duì)列中取出頂點(diǎn)進(jìn)行訪問(wèn),并判斷該頂點(diǎn)是否有鄰接頂點(diǎn),如果有鄰接頂點(diǎn)并且該鄰接頂點(diǎn)沒(méi)有被訪問(wèn)過(guò),則將該鄰接頂點(diǎn)入隊(duì)列,并在入隊(duì)列后立即將該鄰接頂點(diǎn)標(biāo)記為訪問(wèn)過(guò)。

代碼如下:

//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//廣度優(yōu)先遍歷
		void bfs(const V& src) {
			int srci = getVertexIndex(src); //起始頂點(diǎn)的下標(biāo)
			queue<int> q; //隊(duì)列
			vector<bool> visited(_vertexs.size(), false); //標(biāo)記數(shù)組
			q.push(srci); //起始頂點(diǎn)入隊(duì)列
			visited[srci] = true; //起始頂點(diǎn)標(biāo)記為訪問(wèn)過(guò)
			
			while (!q.empty()) {
				int front = q.front();
				q.pop();
				cout << _vertexs[front] << " ";
				for (int i = 0; i < _vertexs.size(); i++) { //找出從front連接出去的頂點(diǎn)
					if (_matrix[front][i] != MAX_W && visited[i] == false) { //是鄰接頂點(diǎn),并且沒(méi)有被訪問(wèn)過(guò)
						q.push(i); //入隊(duì)列
						visited[i] = true; //標(biāo)記為訪問(wèn)過(guò)
					}
				}
			}
			cout << endl;
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
} 

說(shuō)明一下:

  • 為了防止頂點(diǎn)被重復(fù)加入隊(duì)列導(dǎo)致死循環(huán),因此需要一個(gè)標(biāo)記數(shù)組,當(dāng)一個(gè)頂點(diǎn)被訪問(wèn)過(guò)后就不應(yīng)該再將其加入隊(duì)列了。
  • 如果當(dāng)一個(gè)頂點(diǎn)從隊(duì)列中取出訪問(wèn)時(shí)才再將其標(biāo)記為訪問(wèn)過(guò),也可能會(huì)存在頂點(diǎn)被重復(fù)加入隊(duì)列的情況,比如當(dāng)圖中的頂點(diǎn)B出隊(duì)列時(shí),頂點(diǎn)C作為頂點(diǎn)B的鄰接頂點(diǎn)并且還沒(méi)有被訪問(wèn)過(guò)(頂點(diǎn)C還在隊(duì)列中),此時(shí)頂點(diǎn)C就會(huì)再次被加入隊(duì)列,因此最好在一個(gè)頂點(diǎn)被入隊(duì)列時(shí)就將其標(biāo)記為訪問(wèn)過(guò)。
  • 如果所給圖不是一個(gè)連通圖,那么從一個(gè)頂點(diǎn)開(kāi)始進(jìn)行廣度優(yōu)先遍歷,無(wú)法遍歷完圖中的所有頂點(diǎn),這時(shí)可以遍歷標(biāo)記數(shù)組,查看哪些頂點(diǎn)還沒(méi)有被訪問(wèn)過(guò),對(duì)于沒(méi)有被訪問(wèn)過(guò)的頂點(diǎn),則從該頂點(diǎn)處繼續(xù)進(jìn)行廣度優(yōu)先遍歷,直到圖中所有的頂點(diǎn)都被訪問(wèn)過(guò)。

深度優(yōu)先遍歷

深度優(yōu)先遍歷

深度優(yōu)先遍歷又稱(chēng)DFS,其遍歷過(guò)程類(lèi)似于二叉樹(shù)的先序遍歷,從起始頂點(diǎn)開(kāi)始不斷對(duì)頂點(diǎn)進(jìn)行深入遍歷。
如下圖:

深度優(yōu)先遍歷的實(shí)現(xiàn):

  • 深度優(yōu)先遍歷可以通過(guò)遞歸實(shí)現(xiàn),同時(shí)也需要借助一個(gè)標(biāo)記數(shù)組來(lái)記錄各個(gè)頂點(diǎn)是否被訪問(wèn)過(guò)。
  • 從起始頂點(diǎn)處開(kāi)始進(jìn)行遞歸遍歷,在遍歷過(guò)程中先對(duì)當(dāng)前頂點(diǎn)進(jìn)行訪問(wèn),并將其標(biāo)記為訪問(wèn)過(guò),然后判斷該頂點(diǎn)是否有鄰接頂點(diǎn),如果有鄰接頂點(diǎn)并且該鄰接頂點(diǎn)沒(méi)有被訪問(wèn)過(guò),則遞歸遍歷該鄰接頂點(diǎn)。

代碼如下:

//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//深度優(yōu)先遍歷(子函數(shù))
		void _dfs(int srci, vector<bool>& visited) {
			cout << _vertexs[srci] << " "; //訪問(wèn)
			visited[srci] = true; //標(biāo)記為訪問(wèn)過(guò)
			for (int i = 0; i < _vertexs.size(); i++) { //找從srci連接出去的頂點(diǎn)
				if (_matrix[srci][i] != MAX_W && visited[i] == false) { //是鄰接頂點(diǎn),并且沒(méi)有被訪問(wèn)過(guò)
					_dfs(i, visited); //遞歸遍歷
				}
			}
		}
		//深度優(yōu)先遍歷
		void dfs(const V& src) {
			int srci = getVertexIndex(src); //起始頂點(diǎn)的下標(biāo)
			vector<bool> visited(_vertexs.size(), false); //標(biāo)記數(shù)組
			_dfs(srci, visited); //遞歸遍歷
			cout << endl;
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
} 

說(shuō)明一下:

  • 如果所給圖不是一個(gè)連通圖,那么從一個(gè)頂點(diǎn)開(kāi)始進(jìn)行深度優(yōu)先遍歷,無(wú)法遍歷完圖中的所有頂點(diǎn),這時(shí)可以遍歷標(biāo)記數(shù)組,查看哪些頂點(diǎn)還沒(méi)有被訪問(wèn)過(guò),對(duì)于沒(méi)有被訪問(wèn)過(guò)的頂點(diǎn),則從該頂點(diǎn)處繼續(xù)進(jìn)行深度優(yōu)先遍歷,直到圖中所有的頂點(diǎn)都被訪問(wèn)過(guò)。

最小生成樹(shù)

最小生成樹(shù)

關(guān)于最小生成樹(shù):

  • 一個(gè)連通圖的最小連通子圖稱(chēng)為該圖的生成樹(shù),若連通圖由 n 個(gè)頂點(diǎn)組成,則其生成樹(shù)必含 n 個(gè)頂點(diǎn)和 n?1 條邊,最小生成樹(shù)指的是一個(gè)圖的生成樹(shù)中,總權(quán)值最小的生成樹(shù)。
  • 連通圖中的每一棵生成樹(shù)都是原圖的一個(gè)極大無(wú)環(huán)子圖,從其中刪去任何一條邊,生成樹(shù)就不再連通,在其中引入任何一條新邊,都會(huì)形成一條回路。

說(shuō)明一下:

  • 對(duì)于各個(gè)頂點(diǎn)來(lái)說(shuō),除了第一個(gè)頂點(diǎn)之外,其他每個(gè)頂點(diǎn)想要連接到圖中,至少需要一條邊使其連接進(jìn)來(lái),所以由 n 個(gè)頂點(diǎn)的連通圖的生成樹(shù)有 n個(gè)頂點(diǎn)和 n ? 1 條邊。
  • 對(duì)于生成樹(shù)來(lái)說(shuō),圖中的每個(gè)頂點(diǎn)已經(jīng)連通了,如果再引入一條新邊,那么必然會(huì)使得被新邊相連的兩個(gè)頂點(diǎn)之間存在一條直接路徑和一條間接路徑,即形成回路。
  • 最小生成樹(shù)是圖的生成樹(shù)中總權(quán)值最小的生成樹(shù),生成樹(shù)是圖的最小連通子圖,而連通圖是無(wú)向圖的概念,有向圖對(duì)應(yīng)的是強(qiáng)連通圖,所以最小生成樹(shù)算法的處理對(duì)象都是無(wú)向圖。

構(gòu)成最小生成樹(shù)的準(zhǔn)則

構(gòu)造最小生成樹(shù)的準(zhǔn)則如下:

  • 只能使用圖中的邊來(lái)構(gòu)造最小生成樹(shù)。
  • 只能使用恰好n?1 條邊來(lái)連接圖中的n 個(gè)頂點(diǎn)。
  • 選用的n?1 條邊不能構(gòu)成回路。
  • 構(gòu)造最小生成樹(shù)的算法有Kruskal算法和Prim算法,這兩個(gè)算法都采用了逐步求解的貪心策略。

Kruskal算法

Kruskal算法(克魯斯卡爾算法)

Kruskal算法的基本思想如下:

  • 構(gòu)造一個(gè)含 n 個(gè)頂點(diǎn)、不含任何邊的圖作為最小生成樹(shù),對(duì)原圖中的各個(gè)邊按權(quán)值進(jìn)行排序。
  • 每次從原圖中選出一條最小權(quán)值的邊,將其加入到最小生成樹(shù)中,如果加入這條邊會(huì)使得最小生成樹(shù)中構(gòu)成回路,則重新選擇一條邊。
  • 按照上述規(guī)則不斷選邊,當(dāng)選出n?1 條合法的邊時(shí),則說(shuō)明最小生成樹(shù)構(gòu)造完畢,如果無(wú)法選出n?1 條合法的邊,則說(shuō)明原圖不存在最小生成樹(shù)。

動(dòng)圖演示:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

Kruskal算法的實(shí)現(xiàn):

  • 根據(jù)原圖設(shè)置最小生成樹(shù)的頂點(diǎn)集合,以及頂點(diǎn)與下標(biāo)的映射關(guān)系,開(kāi)辟最小生成樹(shù)的鄰接矩陣空間,并將矩陣中的值初始化為 MAX_W ,表示剛開(kāi)始時(shí)最小生成樹(shù)中不含任何邊。
  • 遍歷原圖的鄰接矩陣,按權(quán)值將原圖中的所有邊添加到優(yōu)先級(jí)隊(duì)列(小堆)中,為了避免重復(fù)添加相同的邊,在遍歷原圖的鄰接矩陣時(shí)只應(yīng)該遍歷矩陣的一半。
  • 使用一個(gè)并查集來(lái)輔助判環(huán)操作,剛開(kāi)始時(shí)圖中的頂點(diǎn)各自為一個(gè)集合,當(dāng)兩個(gè)頂點(diǎn)相連時(shí)將這兩個(gè)頂點(diǎn)對(duì)應(yīng)的集合進(jìn)行合并,使得連通的頂點(diǎn)在同一個(gè)集合,這樣通過(guò)并查集就能判斷所選的邊是否會(huì)使得最小生成樹(shù)中構(gòu)成回路,如果所選邊連接的兩個(gè)頂點(diǎn)本就在同一個(gè)集合,那么加入這條邊就會(huì)構(gòu)成回路。
  • 使用count 和totalWeight 分別記錄所選邊的數(shù)量和最小生成樹(shù)的總權(quán)值,當(dāng) count 的值等于n?1 時(shí)則停止選邊,此時(shí)可以將最小生成樹(shù)的總權(quán)值作為返回值進(jìn)行返回。
  • 每次選邊時(shí)從優(yōu)先級(jí)隊(duì)列中獲取一個(gè)權(quán)值最小的邊,并通過(guò)并查集判斷這條邊連接的兩個(gè)頂點(diǎn)是否在同一個(gè)集合,如果在則重新選邊,如果不在則將這條邊添加到最小生成樹(shù)中,并將這條邊連接的兩個(gè)頂點(diǎn)對(duì)應(yīng)的集合進(jìn)行合并,同時(shí)更新count 和 totalWeight 的值。
  • 當(dāng)選邊結(jié)束時(shí),如果 count 的值等于 n?1 ,則說(shuō)明最小生成樹(shù)構(gòu)造成功,否則說(shuō)明原圖無(wú)法構(gòu)造出最小生成樹(shù)。

代碼如下:

//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//強(qiáng)制生成默認(rèn)構(gòu)造
		Graph() = default;
		
		void _addEdge(int srci, int dsti, const W& weight) {
			_matrix[srci][dsti] = weight; //設(shè)置鄰接矩陣中對(duì)應(yīng)的值
			if (Direction == false) { //無(wú)向圖
				_matrix[dsti][srci] = weight; //添加從目標(biāo)頂點(diǎn)到源頂點(diǎn)的邊
			}
		}
		//添加邊
		void addEdge(const V& src, const V& dst, const W& weight) {
			int srci = getVertexIndex(src), dsti = getVertexIndex(dst); //獲取源頂點(diǎn)和目標(biāo)頂點(diǎn)的下標(biāo)
			_addEdge(srci, dsti, weight);
		}
		//邊
		struct Edge {
			int _srci; //源頂點(diǎn)的下標(biāo)
			int _dsti; //目標(biāo)頂點(diǎn)的下標(biāo)
			W _weight; //邊的權(quán)值
			Edge(int srci, int dsti, const W& weight)
				:_srci(srci)
				, _dsti(dsti)
				, _weight(weight)
			{}
			bool operator>(const Edge& edge) const{
				return _weight > edge._weight;
			}
		};
		//獲取當(dāng)前圖的最小生成樹(shù)(Kruskal算法)
		W Kruskal(Graph<V, W, MAX_W, Direction>& minTree) {
			int n = _vertexs.size();
			//設(shè)置最小生成樹(shù)的各個(gè)成員變量
			minTree._vertexs = _vertexs; //設(shè)置最小生成樹(shù)的頂點(diǎn)集合
			minTree._vIndexMap = _vIndexMap; //設(shè)置最小生成樹(shù)頂點(diǎn)與下標(biāo)的映射
			minTree._matrix.resize(n, vector<W>(n, MAX_W)); //開(kāi)辟最小生成樹(shù)的二維數(shù)組空間

			priority_queue<Edge, vector<Edge>, greater<Edge>> minHeap; //優(yōu)先級(jí)隊(duì)列(小堆)
			//將所有邊添加到優(yōu)先級(jí)隊(duì)列
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < i; j++) { //只遍歷矩陣的一半,避免重復(fù)添加相同的邊
					if (_matrix[i][j] != MAX_W)
						minHeap.push(Edge(i, j, _matrix[i][j]));
				}
			}
			UnionFindSet ufs(n); //n個(gè)頂點(diǎn)的并查集
			int count = 0; //已選邊的數(shù)量
			W totalWeight = W(); //最小生成樹(shù)的總權(quán)值
			while (!minHeap.empty() && count < n - 1) {
				//從優(yōu)先級(jí)隊(duì)列中獲取一個(gè)權(quán)值最小的邊
				Edge minEdge = minHeap.top();
				minHeap.pop();
				int srci = minEdge._srci, dsti = minEdge._dsti;
				W weight = minEdge._weight;

				if (!ufs.inSameSet(srci, dsti)) { //邊的源頂點(diǎn)和目標(biāo)頂點(diǎn)不在同一個(gè)集合
					minTree._addEdge(srci, dsti, weight); //在最小生成樹(shù)中添加邊
					ufs.unionSet(srci, dsti); //合并源頂點(diǎn)和目標(biāo)頂點(diǎn)對(duì)應(yīng)的集合
					count++;
					totalWeight += weight;
					cout << "選邊: " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl;
				}
				else { //邊的源頂點(diǎn)和目標(biāo)頂點(diǎn)在同一個(gè)集合,加入這條邊會(huì)構(gòu)成環(huán)
					cout << "成環(huán): " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl;
				}
			}
			if (count == n - 1) {
				cout << "構(gòu)建最小生成樹(shù)成功" << endl;
				return totalWeight;
			}
			else {
				cout << "無(wú)法構(gòu)成最小生成樹(shù)" << endl;
				return W();
			}
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
}

說(shuō)明一下:

  • 在獲取圖的最小生成樹(shù)時(shí),會(huì)以無(wú)參的方式定義一個(gè)最小生成樹(shù)對(duì)象,然后用原圖對(duì)象調(diào)用上述Kruskal函數(shù),通過(guò)輸出型參數(shù)的方式獲取原圖的最小生成樹(shù),由于我們定義了一個(gè)帶參的構(gòu)造函數(shù),使得編譯器不再生成默認(rèn)構(gòu)造函數(shù),因此需要通過(guò)default關(guān)鍵字強(qiáng)制生成Graph類(lèi)的默認(rèn)構(gòu)造函數(shù)。
  • 一條邊包含兩個(gè)頂點(diǎn)和邊的權(quán)值,可以定義一個(gè)Edge結(jié)構(gòu)體來(lái)描述一條邊,結(jié)構(gòu)體內(nèi)包含邊的源頂點(diǎn)和目標(biāo)頂點(diǎn)的下標(biāo)以及邊的權(quán)值,在使用優(yōu)先級(jí)隊(duì)列構(gòu)造小堆結(jié)構(gòu)時(shí),需要存儲(chǔ)的對(duì)象之間能夠支持 > 運(yùn)算符操作,因此需要對(duì)Edge結(jié)構(gòu)體的 > 運(yùn)算符進(jìn)行重載,將其重載為邊的權(quán)值的比較。
  • 當(dāng)選出的邊不會(huì)構(gòu)成回路時(shí),需要將這條邊插入到最小生成樹(shù)對(duì)應(yīng)的圖中,此時(shí)已經(jīng)知道了這條邊的源頂點(diǎn)和目標(biāo)頂點(diǎn)對(duì)應(yīng)的下標(biāo),可以在Graph類(lèi)中新增一個(gè)_addEdge子函數(shù),該函數(shù)支持通過(guò)源頂點(diǎn)和目標(biāo)頂點(diǎn)的下標(biāo)向圖中插入邊,而Graph類(lèi)中原有的addEdge函數(shù)可以復(fù)用這個(gè)_addEdge子函數(shù)。
  • 最小生成樹(shù)不一定是唯一的,特別是當(dāng)原圖中存在很多權(quán)值相等的邊的時(shí)候,比如對(duì)于動(dòng)圖中的圖來(lái)說(shuō),將最小生成樹(shù)中的bc 邊換成 ah 邊也是一棵最小生成樹(shù)。
  • 上述代碼中通過(guò)優(yōu)先級(jí)隊(duì)列構(gòu)造小堆來(lái)依次獲取權(quán)值最小的邊,你也可以通過(guò)其他排序算法按權(quán)值對(duì)邊進(jìn)行排序,然后按權(quán)值從小到大依次遍歷各個(gè)邊進(jìn)行選邊操作。

Prim算法

Prim算法(普里姆算法)

Prim算法的基本思想如下:

  • 構(gòu)造一個(gè)含 n 個(gè)頂點(diǎn)、不含任何邊的圖作為最小生成樹(shù),將圖中的頂點(diǎn)分為兩個(gè)集合,forest 集合中的頂點(diǎn)是已經(jīng)連接到最小生成樹(shù)中的頂點(diǎn),remain 集合中的頂點(diǎn)是還沒(méi)有連接到最小生成樹(shù)中的頂點(diǎn),剛開(kāi)始時(shí) forest 集合中只包含給定的起始頂點(diǎn)。
  • 每次從連接forest 集合與 remain 集合的所有邊中選出一條權(quán)值最小的邊,將其加入到最小生成樹(shù)中,由于選出來(lái)的邊對(duì)應(yīng)的兩個(gè)頂點(diǎn)一個(gè)屬于forest 集合,另一個(gè)屬于 remain 集合,因此是不會(huì)構(gòu)成回路的。
  • 按照上述規(guī)則不斷選邊,當(dāng)選出 n?1 條邊時(shí),所有的頂點(diǎn)都已經(jīng)加入到了 forest 集合,此時(shí)最小生成樹(shù)構(gòu)造完畢,如果無(wú)法選出 n?1 條邊,則說(shuō)明原圖不存在最小生成樹(shù)。

高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

最短路徑

關(guān)于最短路徑:

  • 最短路徑問(wèn)題:從帶權(quán)有向圖中的某一頂點(diǎn)出發(fā),找出一條通往另一頂點(diǎn)的最短路徑,最短指的是路徑各邊的權(quán)值總和達(dá)到最小,最短路徑可分為單源最短路徑和多源最短路徑。
  • 單源最短路徑指的是從圖中某一頂點(diǎn)出發(fā),找出通往其他所有頂點(diǎn)的最短路徑,而多源最短路徑指的是,找出圖中任意兩個(gè)頂點(diǎn)之間的最短路徑。

Prim算法的實(shí)現(xiàn):

  • 根據(jù)原圖設(shè)置最小生成樹(shù)的頂點(diǎn)集合,以及頂點(diǎn)與下標(biāo)的映射關(guān)系,開(kāi)辟最小生成樹(shù)的鄰接矩陣空間,并將矩陣中的值初始化為 MAX_W ,表示剛開(kāi)始時(shí)最小生成樹(shù)中不含任何邊。
  • 使用一個(gè)forest 數(shù)組來(lái)表示各個(gè)頂點(diǎn)是否在 forest 集合中,剛開(kāi)始時(shí)只有起始頂點(diǎn)在 forest 集合中,并將所有從起始頂點(diǎn)連接出去的邊加入優(yōu)先級(jí)隊(duì)列(小堆),這些邊就是剛開(kāi)始時(shí)連接 forest 集合與remain 集合的邊。
  • 使用 count 和 totalWeight 分別記錄所選邊的數(shù)量和最小生成樹(shù)的總權(quán)值,當(dāng)count 的值等于 n?1 時(shí)則停止選邊,此時(shí)將最小生成樹(shù)的總權(quán)值作為返回值進(jìn)行返回。
  • 每次選邊時(shí)從優(yōu)先級(jí)隊(duì)列中獲取一個(gè)權(quán)值最小的邊,將這條邊添加到最小生成樹(shù)中,并將這條邊的目標(biāo)頂點(diǎn)加入 forest 集合中,同時(shí)更新 count 和 totalWeight 的值。此外,還需要將從這條邊的目標(biāo)頂點(diǎn)連接出去的邊加入優(yōu)先級(jí)隊(duì)列,但是需要保證加入的邊的目標(biāo)頂點(diǎn)不能在 forest 集合,否則后續(xù)選出源頂點(diǎn)和目標(biāo)頂點(diǎn)都在forest 集合的邊就會(huì)構(gòu)成回路。
  • 需要注意的是,每次從優(yōu)先級(jí)隊(duì)列中選出一個(gè)權(quán)值最小的邊時(shí),還需要保證選出的這條邊的目標(biāo)頂點(diǎn)不在forest 集合中,避免構(gòu)成回路。雖然向優(yōu)先級(jí)隊(duì)列中加入邊時(shí)保證了加入的邊的目標(biāo)頂點(diǎn)不在forest 集合中,但經(jīng)過(guò)后續(xù)不斷的選邊,可能會(huì)導(dǎo)致之前加入優(yōu)先級(jí)隊(duì)列中的某些邊的目標(biāo)頂點(diǎn)也被加入到了forest 集合中。
  • 當(dāng)選邊結(jié)束時(shí),如果 count 的值等于 n?1 ,則說(shuō)明最小生成樹(shù)構(gòu)造成功,否則說(shuō)明原圖無(wú)法構(gòu)造出最小生成樹(shù)。

代碼如下:

//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//邊
		struct Edge {
			int _srci; //源頂點(diǎn)的下標(biāo)
			int _dsti; //目標(biāo)頂點(diǎn)的下標(biāo)
			W _weight; //邊的權(quán)值
			Edge(int srci, int dsti, const W& weight)
				:_srci(srci)
				, _dsti(dsti)
				, _weight(weight)
			{}
			bool operator>(const Edge& edge) const{
				return _weight > edge._weight;
			}
		};
		//獲取當(dāng)前圖的最小生成樹(shù)(Prim算法)
		W Prim(Graph<V, W, MAX_W, Direction>& minTree, const V& start) {
			int n = _vertexs.size();
			//設(shè)置最小生成樹(shù)的各個(gè)成員變量
			minTree._vertexs = _vertexs; //設(shè)置最小生成樹(shù)的頂點(diǎn)集合
			minTree._vIndexMap = _vIndexMap; //設(shè)置最小生成樹(shù)頂點(diǎn)與下標(biāo)的映射
			minTree._matrix.resize(n, vector<W>(n, MAX_W)); //開(kāi)辟最小生成樹(shù)的二維數(shù)組空間

			int starti = getVertexIndex(start); //獲取起始頂點(diǎn)的下標(biāo)
			vector<bool> forest(n, false);
			forest[starti] = true;
			priority_queue<Edge, vector<Edge>, greater<Edge>> minHeap; //優(yōu)先級(jí)隊(duì)列(小堆)
			
			//將從起始頂點(diǎn)連接出去的邊加入優(yōu)先級(jí)隊(duì)列
			for (int i = 0; i < n; i++) {
				if (_matrix[starti][i] != MAX_W)
					minHeap.push(Edge(starti, i, _matrix[starti][i]));
			}

			int count = 0; //已選邊的數(shù)量
			W totalWeight = W(); //最小生成樹(shù)的總權(quán)值
			while (!minHeap.empty() && count < n - 1) {
				//從優(yōu)先級(jí)隊(duì)列中獲取一個(gè)權(quán)值最小的邊
				Edge minEdge = minHeap.top();
				minHeap.pop();
				int srci = minEdge._srci, dsti = minEdge._dsti;
				W weight = minEdge._weight;

				if (forest[dsti] == false) { //邊的目標(biāo)頂點(diǎn)還沒(méi)有被加入到forest集合中
					//將從目標(biāo)頂點(diǎn)連接出去的邊加入優(yōu)先級(jí)隊(duì)列
					for (int i = 0; i < n; i++) {
						if (_matrix[dsti][i] != MAX_W && forest[i] == false) //加入的邊的目標(biāo)頂點(diǎn)不能在forest集合中
							minHeap.push(Edge(dsti, i, _matrix[dsti][i]));
					}
					minTree._addEdge(srci, dsti, weight); //在最小生成樹(shù)中添加邊
					forest[dsti] = true; //將邊的目標(biāo)頂點(diǎn)加入forest集合
					count++;
					totalWeight += weight;
					cout << "選邊: " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl;
				}
				else { //邊的目標(biāo)頂點(diǎn)已經(jīng)在forest集合中,加入這條邊會(huì)構(gòu)成環(huán)
					cout << "成環(huán): " << _vertexs[srci] << "->" << _vertexs[dsti] << ":" << weight << endl;
				}
			}
			if (count == n - 1) {
				cout << "構(gòu)建最小生成樹(shù)成功" << endl;
				return totalWeight;
			}
			else {
				cout << "無(wú)法構(gòu)成最小生成樹(shù)" << endl;
				return W();
			}
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
}

說(shuō)明一下:

  • Prim算法構(gòu)造最小生成樹(shù)的思想在選邊時(shí)是不需要判環(huán),但上述利用優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn)的過(guò)程中仍需判環(huán),如果在每次選邊的時(shí)候能夠通過(guò)某種方式,從連接forest 集合和remain 集合的所有邊中選出權(quán)值最小的邊,那么就無(wú)需判環(huán),但這兩個(gè)集合中的頂點(diǎn)是不斷在變化的,每次選邊時(shí)都遍歷連接兩個(gè)集合的所有邊,該過(guò)程的時(shí)間復(fù)雜度較高。
  • Kruskal算法本質(zhì)是一種全局的貪心,每次選邊時(shí)都是在所有邊中選出權(quán)值最小的邊,而Prim算法本質(zhì)是一種局部的貪心,每次選邊時(shí)是從連接 forest 集合和 remain 集合的所有邊中選出權(quán)值最小的邊。

最短路徑

最短路徑

關(guān)于最短路徑:

  • 最短路徑問(wèn)題:從帶權(quán)有向圖中的某一頂點(diǎn)出發(fā),找出一條通往另一頂點(diǎn)的最短路徑,最短指的是路徑各邊的權(quán)值總和達(dá)到最小,最短路徑可分為單源最短路徑和多源最短路徑。
  • 單源最短路徑指的是從圖中某一頂點(diǎn)出發(fā),找出通往其他所有頂點(diǎn)的最短路徑,而多源最短路徑指的是,找出圖中任意兩個(gè)頂點(diǎn)之間的最短路徑。

單源最短路徑-Dijkstra算法

Dijkstra算法(迪杰斯特拉算法)

使用前提:圖中所有邊的權(quán)值非負(fù)。

Dijkstra算法的基本思想如下:

  • 將圖中的頂點(diǎn)分為兩個(gè)集合,集合 S 中的頂點(diǎn)是已經(jīng)確定從源頂點(diǎn)到該頂點(diǎn)的最短路徑的頂點(diǎn),集合 Q 中的頂點(diǎn)是尚未確定從源頂點(diǎn)到該頂點(diǎn)的最短路徑的頂點(diǎn)。
  • 每個(gè)頂點(diǎn)都有一個(gè)估計(jì)值,表示從源頂點(diǎn)到該頂點(diǎn)的可能最短路徑長(zhǎng)度,每次從集合Q 中選出一個(gè)估計(jì)值最小的頂點(diǎn),將其加入到集合 S 中,并對(duì)該頂點(diǎn)連接出去的頂點(diǎn)的估計(jì)值和前驅(qū)頂點(diǎn)進(jìn)行松弛更新。
  • 按照上述步驟不斷從集合 Q 中選取估計(jì)值最小的頂點(diǎn)到集合 S 中,直到所有的頂點(diǎn)都被加入到集合 S 中,此時(shí)通過(guò)各個(gè)頂點(diǎn)的估計(jì)值就可以得知源頂點(diǎn)到該頂點(diǎn)的最短路徑長(zhǎng)度,通過(guò)各個(gè)頂點(diǎn)的前驅(qū)頂點(diǎn)就可以得知最短路徑的走向。

動(dòng)圖演示:
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

Dijkstra算法的實(shí)現(xiàn):

  • 使用一個(gè)dist 數(shù)組來(lái)記錄從源頂點(diǎn)到各個(gè)頂點(diǎn)的最短路徑長(zhǎng)度估計(jì)值,初始時(shí)將源頂點(diǎn)的估計(jì)值設(shè)置為權(quán)值的缺省值(比如int就是0),表示從源頂點(diǎn)到源頂點(diǎn)的路徑長(zhǎng)度為0,將其余頂點(diǎn)的估計(jì)值設(shè)置為MAX_W,表示從源頂點(diǎn)暫時(shí)無(wú)法到達(dá)其他頂點(diǎn)。
  • 使用一個(gè)PathparentPath 數(shù)組來(lái)記錄到達(dá)各個(gè)頂點(diǎn)路徑的前驅(qū)頂點(diǎn),初始時(shí)將各個(gè)頂點(diǎn)的前驅(qū)頂點(diǎn)初始化為-1,表示各個(gè)頂點(diǎn)暫時(shí)只能自己到達(dá)自己,沒(méi)有前驅(qū)頂點(diǎn)。
  • 使用一個(gè)bool 數(shù)組來(lái)記錄各個(gè)頂點(diǎn)是否在 S 集合中,初始時(shí)所有頂點(diǎn)均不在S 集合,表示各個(gè)頂點(diǎn)都還沒(méi)有確定最短路徑。
  • 每次從 Q 集合中選出一個(gè)估計(jì)值最小的頂點(diǎn) u,將其加入到 S 集合,并對(duì)頂點(diǎn)u 連接出去的各個(gè)頂點(diǎn)v 進(jìn)行松弛更新,如果能夠?qū)㈨旤c(diǎn) v 更新出更小的估計(jì)值,則更新其估計(jì)值,并將被更新的頂點(diǎn) v 的前驅(qū)頂點(diǎn)改為頂點(diǎn) u ,因?yàn)閺捻旤c(diǎn) u 到頂點(diǎn) v 能夠得到更小的估計(jì)值,所以在當(dāng)前看來(lái)(后續(xù)可能還會(huì)更新)到達(dá)頂點(diǎn) v 的最短路徑的前驅(qū)頂點(diǎn)就應(yīng)該是頂點(diǎn) u ,如果不能將頂點(diǎn) v 更新出更小的估計(jì)值,則維持原樣。
  • 當(dāng)所有的頂點(diǎn)都加入集合 S 后,dist 數(shù)組中存儲(chǔ)的就是從源頂點(diǎn)到各個(gè)頂點(diǎn)的最短路徑長(zhǎng)度,parentPath 數(shù)組中存儲(chǔ)的就是從源頂點(diǎn)到各個(gè)頂點(diǎn)的最短路徑的前驅(qū)頂點(diǎn),通過(guò)不斷查找各個(gè)頂點(diǎn)的前驅(qū)頂點(diǎn),最終就能得到從源頂點(diǎn)到各個(gè)頂點(diǎn)的最短路徑。

代碼如下:

//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//獲取單源最短路徑(Dijkstra算法)
		void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath) {
			int n = _vertexs.size();
			int srci = getVertexIndex(src); //獲取源頂點(diǎn)的下標(biāo)
			dist.resize(n, MAX_W); //各個(gè)頂點(diǎn)的估計(jì)值初始化為MAX_W
			parentPath.resize(n, -1); //各個(gè)頂點(diǎn)的前驅(qū)頂點(diǎn)初始化為-1

			dist[srci] = W(); //源頂點(diǎn)的估計(jì)值設(shè)置為權(quán)值的缺省值
			vector<bool> S(n, false); //已經(jīng)確定最短路徑的頂點(diǎn)集合
			for (int i = 0; i < n; i++) { //將Q集合中的n個(gè)頂點(diǎn)全部加入到S集合
				//從集合Q中選出一個(gè)估計(jì)值最小的頂點(diǎn)
				W minW = MAX_W; //最小估計(jì)值
				int u = -1;     //估計(jì)值最小的頂點(diǎn)
				for (int j = 0; j < n; j++) {
					if (S[j] == false && dist[j] < minW) {
						minW = dist[j];
						u = j;
					}
				}
				S[u] = true; //將選出的頂點(diǎn)加入到S集合
				//對(duì)u連接出去的頂點(diǎn)進(jìn)行松弛更新
				for (int v = 0; v < n; v++) {
					if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]) { //松弛的頂點(diǎn)不能在S集合
						dist[v] = dist[u] + _matrix[u][v]; //松弛更新出更小的估計(jì)值
						parentPath[v] = u; //更新路徑的前驅(qū)頂點(diǎn)
					}
				}
			}
		}
		//打印最短路徑及路徑權(quán)值
		void printShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath) {
			int n = _vertexs.size();
			int srci = getVertexIndex(src); //獲取源頂點(diǎn)的下標(biāo)
			for (int i = 0; i < n; i++) {
				vector<int> path;
				int cur = i;
				while (cur != -1) { //源頂點(diǎn)的前驅(qū)頂點(diǎn)為-1
					path.push_back(cur);
					cur = parentPath[cur];
				}
				reverse(path.begin(), path.end()); //逆置
				for (int j = 0; j < path.size(); j++) {
					cout << _vertexs[path[j]] << "->";
				}
				cout << "路徑權(quán)值: " << dist[i] << "" << endl;
			}
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
} 

說(shuō)明一下:

  • 為了方便觀察,可以在類(lèi)中增加一個(gè) printShortPath 接口,用于根據(jù) dist 和 parentPath 數(shù)組來(lái)打印最短路徑及路徑權(quán)值。
  • 對(duì)于從源頂點(diǎn) s 到目標(biāo)頂點(diǎn) j 的最短路徑來(lái)說(shuō),如果最短路徑經(jīng)過(guò)了頂點(diǎn) i ,那么最短路徑中從源頂點(diǎn) s 到頂點(diǎn) i 的這條子路徑一定是源頂點(diǎn) s 到頂點(diǎn) i 的最短路徑,因此可以通過(guò)存儲(chǔ)前驅(qū)頂點(diǎn)的方式來(lái)表示從源頂點(diǎn)到各個(gè)頂點(diǎn)的最短路徑。
  • Dijkstra算法每次需要選出一個(gè)頂點(diǎn),并對(duì)其連接出去的頂點(diǎn)進(jìn)行松弛更新,因此其時(shí)間復(fù)雜度是 O (N2) 空間復(fù)雜度是O(N) 。

Dijkstra算法的原理

  • Dijkstra算法每次從集合 Q 中選出一個(gè)估計(jì)值最小的頂點(diǎn) u ,將該頂點(diǎn)加入到集合 S 中,表示確定了從源頂點(diǎn)到頂點(diǎn) u 的最短路徑。
  • 因?yàn)閳D中所有邊的權(quán)值非負(fù)(使用Dijkstra算法的前提),所以對(duì)于估計(jì)值最小的頂點(diǎn) u 來(lái)說(shuō),其估計(jì)值不可能再被其他比它估計(jì)值更大的頂點(diǎn)松弛更新得更小,因此頂點(diǎn) u 的最短路徑就是當(dāng)前的估計(jì)值。
  • 而對(duì)于集合 Q 中的其他頂點(diǎn)來(lái)說(shuō),這些頂點(diǎn)的估計(jì)值比頂點(diǎn) u 的估計(jì)值大,因此頂點(diǎn) u 可能將它們的估計(jì)值松弛更新得更小,所以頂點(diǎn) u 在加入集合 S 后還需要嘗試對(duì)其連接出去的頂點(diǎn)進(jìn)行松弛更新。

單源最短路徑-Bellman-Ford算法

Bellman-Ford算法(貝爾曼福特算法)

Bellman-Ford算法的基本思想如下:

  • Bellman-Ford算法本質(zhì)是暴力求解,對(duì)于從源頂點(diǎn) s 到目標(biāo)頂點(diǎn) j 的路徑來(lái)說(shuō),如果存在從源頂點(diǎn)s 到頂點(diǎn) i 的路徑,還存在一條從頂點(diǎn) i 到頂點(diǎn) j 的邊,并且其權(quán)值之和小于當(dāng)前從源頂點(diǎn) s 到目標(biāo)頂點(diǎn) j 的路徑長(zhǎng)度,則可以對(duì)頂點(diǎn) j 的估計(jì)值和前驅(qū)頂點(diǎn)進(jìn)行松弛更新。
  • Bellman-Ford算法根據(jù)路徑的終邊來(lái)進(jìn)行松弛更新,但是僅對(duì)圖中的邊進(jìn)行一次遍歷可能并不能正確更新出最短路徑,最壞的情況下需要對(duì)圖中的邊進(jìn)行 n?1 輪遍歷(n 表示圖中的頂點(diǎn)個(gè)數(shù))。

Bellman-Ford算法的實(shí)現(xiàn):

  • 使用一個(gè)dist 數(shù)組來(lái)記錄從源頂點(diǎn)到各個(gè)頂點(diǎn)的最短路徑長(zhǎng)度估計(jì)值,初始時(shí)將源頂點(diǎn)的估計(jì)值設(shè)置為權(quán)值的缺省值(比如int就是0),表示從源頂點(diǎn)到源頂點(diǎn)的路徑長(zhǎng)度為0,將其余頂點(diǎn)的估計(jì)值設(shè)置為MAX_W,表示從源頂點(diǎn)暫時(shí)無(wú)法到達(dá)其他頂點(diǎn)。
  • 使用一個(gè) parentPath 數(shù)組來(lái)記錄到達(dá)各個(gè)頂點(diǎn)路徑的前驅(qū)頂點(diǎn),初始時(shí)將各個(gè)頂點(diǎn)的前驅(qū)頂點(diǎn)初始化為-1,表示各個(gè)頂點(diǎn)暫時(shí)只能自己到達(dá)自己,沒(méi)有前驅(qū)頂點(diǎn)。
  • 對(duì)圖中的邊進(jìn)行 n?1 輪遍歷,對(duì)于i?>j 的邊來(lái)說(shuō),如果存在s?>i 的路徑,并且s?>i 的路徑權(quán)值與邊 i?>j 的權(quán)值之和小于當(dāng)前 s?>j 的路徑長(zhǎng)度,則將頂點(diǎn) j 的估計(jì)值進(jìn)行更新,并將頂點(diǎn)j 的前驅(qū)頂點(diǎn)改為頂點(diǎn) i ,因?yàn)?i?>j 是圖中的一條直接相連的邊,在這條路徑中頂點(diǎn) j 的上一個(gè)頂點(diǎn)就是頂點(diǎn) i 。
  • 再對(duì)圖中的邊進(jìn)行一次遍歷,嘗試進(jìn)行松弛更新,如果還能更新則說(shuō)明圖中帶有負(fù)權(quán)回路,無(wú)法找到最短路徑
//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//獲取單源最短路徑(BellmanFord算法)
		bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath) {
			int n = _vertexs.size();
			int srci = getVertexIndex(src); //獲取源頂點(diǎn)的下標(biāo)
			dist.resize(n, MAX_W); //各個(gè)頂點(diǎn)的估計(jì)值初始化為MAX_W
			parentPath.resize(n, -1); //各個(gè)頂點(diǎn)的前驅(qū)頂點(diǎn)初始化為-1

			dist[srci] = W(); //源頂點(diǎn)的估計(jì)值設(shè)置為權(quán)值的缺省值
			for (int k = 0; k < n - 1; k++) { //最多更新n-1輪
				bool update = false; //記錄本輪是否更新過(guò)
				for (int i = 0; i < n; i++) {
					for (int j = 0; j < n; j++) {
						if (_matrix[i][j] != MAX_W && dist[i] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) {
							dist[j] = dist[i] + _matrix[i][j]; //松弛更新出更小的路徑權(quán)值
							parentPath[j] = i; //更新路徑的前驅(qū)頂點(diǎn)
							update = true;
						}
					}
				}
				if (update == false) { //本輪沒(méi)有更新過(guò),不必進(jìn)行后續(xù)輪次的更新
					break;
				}
			}
			//更新n-1輪后如果還能更新,則說(shuō)明帶有負(fù)權(quán)回路
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < n; j++) {
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) {
						return false; //帶有負(fù)權(quán)回路的圖無(wú)法求出最短路徑
					}
				}
			}
			return true;
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
} 

說(shuō)明一下:

  • Bellman-Ford算法是暴力求解,可以解決帶有負(fù)權(quán)邊的單源最短路徑問(wèn)題。
  • 負(fù)權(quán)回路指的是在圖中形成回路的各個(gè)邊的權(quán)值之和為負(fù)數(shù),路徑每繞一圈回路其權(quán)值都會(huì)減少,導(dǎo)致無(wú)法找到最短路徑,由于最多需要進(jìn)行n?1 輪松弛更新,因此可以在 n?1 輪松弛更新后再進(jìn)行一輪松弛更新,如果還能進(jìn)行更新則說(shuō)明帶有負(fù)權(quán)回路。
  • Bellman-Ford算法需要對(duì)圖中的邊進(jìn)行 n 輪遍歷,因此其時(shí)間復(fù)雜度是 O(N×E),由于這里是用鄰接矩陣實(shí)現(xiàn)的,遍歷圖中的所有邊的時(shí)間復(fù)雜度是O(N2)所以上述代碼的時(shí)間復(fù)雜度是O(N3)空間復(fù)雜度是O(N)。

為什么最多進(jìn)行n?1 輪松弛更新?

從一個(gè)頂點(diǎn)到另一個(gè)頂點(diǎn)的最短路徑中不能包含回路:

  • 如果形成回路的各個(gè)邊的權(quán)值之和為負(fù)數(shù),則該回路為負(fù)權(quán)回路,找不到最短路徑。
  • 如果形成回路的各個(gè)邊的權(quán)值之和為非負(fù)數(shù),則多走這個(gè)回路是“徒勞”的,可能會(huì)使得路徑長(zhǎng)度變長(zhǎng)。

在每一輪松弛過(guò)程中,后面路徑的更新可能會(huì)影響到前面已經(jīng)更新過(guò)的路徑,比如使得前面已經(jīng)更新過(guò)的路徑的長(zhǎng)度可以變得更短,或者使得某些源頂點(diǎn)之前不可達(dá)的頂點(diǎn)變得可達(dá),但每一輪松弛至少能確定最短路徑中的一條邊,如果圖中有n 個(gè)頂點(diǎn),那么兩個(gè)頂點(diǎn)之間的最短路徑最多有n?1 條邊,因此最多需要進(jìn)行n?1 次松弛更新。

例如下圖中,頂點(diǎn)A、B、C、D、E 的下標(biāo)分別是0、1、2、3、4,現(xiàn)在要計(jì)算以頂點(diǎn) E 為源頂點(diǎn)的單源最短路徑。
高階數(shù)據(jù)結(jié)構(gòu)——圖,數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)

對(duì)于上述圖來(lái)說(shuō),Bellman-Ford算法在第一輪松弛的時(shí)候只能更新出E?>D 這條邊,在第二輪的時(shí)候只能更新出 D?>C ,以此類(lèi)推,最終就會(huì)進(jìn)行4輪松弛更新(建議通過(guò)代碼調(diào)試觀察)。

說(shuō)明一下:

  • 由于只有當(dāng)前輪次進(jìn)行過(guò)更新,才有可能會(huì)影響其他路徑,因此在代碼中使用update 標(biāo)記每輪松弛算法是否進(jìn)行過(guò)更新,如果沒(méi)有進(jìn)行過(guò)更新,則無(wú)需進(jìn)行后面輪次的更新。
  • Bellman-Ford算法還有一個(gè)優(yōu)化方案叫做SPFA(Shortest Path Faster Algorithm),其用一個(gè)隊(duì)列來(lái)維護(hù)可能需要松弛更新的頂點(diǎn),避免了不必要的冗余計(jì)算,大家可以自行了解。

多源最短路徑-Floyd-Warshall算法

Floyd-Warshall算法(弗洛伊德算法)

Floyd-Warshall算法的基本思想如下:

  • Floyd-Warshall算法解決的是任意兩點(diǎn)間的最短路徑的算法,其考慮的是路徑的中間頂點(diǎn),對(duì)于從頂點(diǎn) i ii 到頂點(diǎn) j jj 的路徑來(lái)說(shuō),如果存在從頂點(diǎn) i 到頂點(diǎn) k 的路徑,還存在從頂點(diǎn) k 到頂點(diǎn) j 的路徑,并且這兩條路徑的權(quán)值之和小于當(dāng)前從頂點(diǎn) i 到頂點(diǎn) j 的路徑長(zhǎng)度,則可以對(duì)頂點(diǎn) j 的估計(jì)值和前驅(qū)頂點(diǎn)進(jìn)行松弛更新。
  • Floyd-Warshall算法本質(zhì)是一個(gè)簡(jiǎn)單的動(dòng)態(tài)規(guī)劃,就是判斷從頂點(diǎn) i 到頂點(diǎn) j 的這條路徑是否經(jīng)過(guò)頂點(diǎn) k ,如果經(jīng)過(guò)頂點(diǎn) k 可以讓這條路徑的權(quán)值變得更小,則經(jīng)過(guò),否則則不經(jīng)過(guò)。

Floyd-Warshall算法的實(shí)現(xiàn):

  • 使用一個(gè) vvDist 二維數(shù)組來(lái)記錄從各個(gè)源頂點(diǎn)到各個(gè)頂點(diǎn)的最短路徑長(zhǎng)度的估計(jì)值,vvDist[i][j] 表示從頂點(diǎn) i ii 到頂點(diǎn) j 的最短路徑長(zhǎng)度的估計(jì)值,初始時(shí)將二維數(shù)組中的值全部初始化為MAX_W,表示各個(gè)頂點(diǎn)之間暫時(shí)無(wú)法互通。
  • 使用一個(gè)vvParentPath 二維數(shù)組來(lái)記錄從各個(gè)源頂點(diǎn)到達(dá)各個(gè)頂點(diǎn)路徑的前驅(qū)頂點(diǎn),初始時(shí)將二維數(shù)組中的值全部初始化為-1,表示各個(gè)頂點(diǎn)暫時(shí)只能自己到自己,沒(méi)有前驅(qū)頂點(diǎn)。
  • 根據(jù)鄰接矩陣對(duì)vvDist 和vvParentPath 進(jìn)行初始化,如果從頂點(diǎn) i 到頂點(diǎn) j 有直接相連的邊,則將 vvDist[i][j] 初始化為這條邊的權(quán)值,并將 vvParentPath[i][j] 初始化為 i ,表示在i?>j 這條路徑中頂點(diǎn) j 前驅(qū)頂點(diǎn)是 i ,將 vvDist[i][i] 的值設(shè)置為權(quán)值的缺省值(比如int就是0),表示自己到自己的路徑長(zhǎng)度為0。
  • 依次取各個(gè)頂點(diǎn) k 作為 i?>j 路徑的中間頂點(diǎn),如果同時(shí)存在i?>k 的路徑和k?>j 的路徑,并且這兩條路徑的權(quán)值之和小于當(dāng)前i?>j 路徑的權(quán)值,則更新vvDist[i][j] 的值,并將vvParentPath[i][j] 的值更新為vvParentPath[k][j] 的值。

代碼如下:

//鄰接矩陣
namespace Matrix {
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph {
	public:
		//獲取多源最短路徑(FloydWarshall算法)
		void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvParentPath) {
			int n = _vertexs.size();
			vvDist.resize(n, vector<W>(n, MAX_W)); //任意兩個(gè)頂點(diǎn)直接的路徑權(quán)值初始化為MAX_W
			vvParentPath.resize(n, vector<int>(n, -1)); //各個(gè)頂點(diǎn)的前驅(qū)頂點(diǎn)初始化為-1

			//根據(jù)鄰接矩陣初始化直接相連的頂點(diǎn)
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < n; j++) {
					if (_matrix[i][j] != MAX_W) { //i->j有邊
						vvDist[i][j] = _matrix[i][j]; //i->j的路徑權(quán)值
						vvParentPath[i][j] = i; //i->j路徑的前驅(qū)頂點(diǎn)為i
					}
					if (i == j) { //i->i
						vvDist[i][j] = W(); //i->i的路徑權(quán)值設(shè)置為權(quán)值的缺省值
					}
				}
			}
			for (int k = 0; k < n; k++) { //依次取各個(gè)頂點(diǎn)作為i->j路徑的中間頂點(diǎn)
				for (int i = 0; i < n; i++) {
					for (int j = 0; j < n; j++) {
						if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W &&
							vvDist[i][k] + vvDist[k][j] < vvDist[i][j]) { //存在i->k和k->j的路徑,并且這兩條路徑的權(quán)值之和小于當(dāng)前i->j路徑的權(quán)值
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j]; //松弛更新出更小的路徑權(quán)值
							vvParentPath[i][j] = vvParentPath[k][j]; //更小路徑的前驅(qū)頂點(diǎn)
						}
					}
				}
			}
		}
	private:
		vector<V> _vertexs;               //頂點(diǎn)集合
		unordered_map<V, int> _vIndexMap; //頂點(diǎn)映射下標(biāo)
		vector<vector<W>> _matrix;        //鄰接矩陣
	};
}

說(shuō)明一下:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-731303.html

  • Bellman-Ford算法是根據(jù)路徑的終邊來(lái)進(jìn)行松弛更新的,而Floyd-Warshall算法是根據(jù)路徑經(jīng)過(guò)的中間頂點(diǎn)來(lái)進(jìn)行松弛更新的,因?yàn)楦鶕?jù)Bellman-Ford算法中的dist 只能得知從指定源頂點(diǎn)到某一頂點(diǎn)的路徑權(quán)值,而根據(jù)Floyd-Warshall算法中的vvDist 可以得知任意兩個(gè)頂點(diǎn)之間的路徑權(quán)值。
  • Floyd-Warshall算法的時(shí)間復(fù)雜度是O(N3)空間復(fù)雜度是O(N2)雖然求解多源最短路徑也可以以圖中不同的頂點(diǎn)作為源頂點(diǎn),去調(diào)用Dijkstra算法或Bellman-Ford算法,但Dijkstra算法不能解決帶負(fù)權(quán)的圖,Bellman-Ford算法調(diào)用 N 次的時(shí)間復(fù)雜度又太高。

到了這里,關(guān)于高階數(shù)據(jù)結(jié)構(gòu)——圖的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 【高階數(shù)據(jù)結(jié)構(gòu)】B+樹(shù)

    【高階數(shù)據(jù)結(jié)構(gòu)】B+樹(shù)

    B+樹(shù)是B樹(shù)的變形,是在B樹(shù)基礎(chǔ)上優(yōu)化的多路平衡搜索樹(shù),B+樹(shù)的規(guī)則跟B樹(shù)基本類(lèi)似,但是又在B樹(shù)的基礎(chǔ)上做了一些改進(jìn)優(yōu)化。 一棵m階的B+樹(shù)需滿足下列條件: 每個(gè)分支結(jié)點(diǎn)最多有m棵子樹(shù)(孩子結(jié)點(diǎn))。 非葉根結(jié)點(diǎn)至少有兩棵子樹(shù),其他每個(gè)分支結(jié)點(diǎn)至少有「m/2]棵子樹(shù)。 (前

    2024年02月21日
    瀏覽(19)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】B樹(shù)

    【高階數(shù)據(jù)結(jié)構(gòu)】B樹(shù)

    種類(lèi) 數(shù)據(jù)格式 時(shí)間復(fù)雜度 順序查找 無(wú)要求 O(N) 二分查找 有序 O(log 2 N ) 二叉搜索樹(shù) 無(wú)要求 O(N) 二叉平衡樹(shù)(紅黑樹(shù)和AVL樹(shù)) 無(wú)要求 O(log 2 N ) 哈希 無(wú)要求 O(1) 以上結(jié)構(gòu)適合用于數(shù)據(jù)量相對(duì)不是很大,能夠一次性存放在內(nèi)存中,進(jìn)行數(shù)據(jù)查找的場(chǎng)景 。如果數(shù)據(jù)量很大,比如有

    2024年02月16日
    瀏覽(20)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】哈希表詳解

    【高階數(shù)據(jù)結(jié)構(gòu)】哈希表詳解

    上一篇文章我們學(xué)習(xí)了STL中unordered系列容器的使用,并且提到,unordered系列容器的效率之所以比較高(尤其是查找),是因?yàn)樗讓邮褂昧斯=Y(jié)構(gòu),即哈希表。 那這篇文章,我們就來(lái)學(xué)習(xí)一下哈希表 順序結(jié)構(gòu)以及平衡樹(shù)中,元素關(guān)鍵碼與其存儲(chǔ)位置之間沒(méi)有對(duì)應(yīng)的關(guān)系,

    2024年02月11日
    瀏覽(21)
  • 高階數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí) —— 圖(3)

    高階數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí) —— 圖(3)

    先看一下連通圖和生成樹(shù)的概念 連通圖。在無(wú)向圖中,若從頂點(diǎn)v1到頂點(diǎn)v2有路徑,則稱(chēng)頂點(diǎn)v1與頂點(diǎn)v2是連通的。如果圖中任意一對(duì)頂點(diǎn)都是連通的,則稱(chēng)此圖為連通圖。 生成樹(shù):一個(gè)連通圖的最小連通子圖稱(chēng)作該圖的生成樹(shù)。有n個(gè)頂點(diǎn)的連通圖的生成樹(shù)有n個(gè)頂點(diǎn)和n-1條邊

    2024年02月06日
    瀏覽(18)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】紅黑樹(shù)詳解

    【高階數(shù)據(jù)結(jié)構(gòu)】紅黑樹(shù)詳解

    這篇文章我們?cè)賮?lái)學(xué)習(xí)一種平衡搜索二叉樹(shù)—— 紅黑樹(shù) 紅黑樹(shù)和AVL樹(shù)都是常見(jiàn)的自平衡二叉搜索樹(shù),它們都可以用于高效地支持插入、刪除和查找等操作。雖然它們都能夠保持樹(shù)的平衡性,但在不同的應(yīng)用場(chǎng)景下,紅黑樹(shù)和AVL樹(shù)有各自的優(yōu)勢(shì)和適用性。 紅黑樹(shù)(Red-Black Tr

    2024年02月12日
    瀏覽(29)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】AVL樹(shù)詳解

    【高階數(shù)據(jù)結(jié)構(gòu)】AVL樹(shù)詳解

    前面對(duì)map/multimap/set/multiset進(jìn)行了簡(jiǎn)單的介紹,在其文檔介紹中發(fā)現(xiàn)。 這幾個(gè)容器有個(gè)共同點(diǎn)是: 其底層都是按照二叉搜索樹(shù)來(lái)實(shí)現(xiàn)的,但是二叉搜索樹(shù)有其自身的缺陷,假如往樹(shù)中插入的元素有序或者接近有序,二叉搜索樹(shù)就會(huì)退化成單支樹(shù),時(shí)間復(fù)雜度會(huì)退化成O(N),因此

    2024年02月12日
    瀏覽(33)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】AVL樹(shù)詳解(圖解+代碼)

    【高階數(shù)據(jù)結(jié)構(gòu)】AVL樹(shù)詳解(圖解+代碼)

    前面對(duì)map/multimap/set/multiset進(jìn)行了簡(jiǎn)單的介紹,在其文檔介紹中發(fā)現(xiàn)。 這幾個(gè)容器有個(gè)共同點(diǎn)是: 其底層都是按照二叉搜索樹(shù)來(lái)實(shí)現(xiàn)的,但是二叉搜索樹(shù)有其自身的缺陷,假如往樹(shù)中插入的元素有序或者接近有序,二叉搜索樹(shù)就會(huì)退化成單支樹(shù),時(shí)間復(fù)雜度會(huì)退化成O(N),因此

    2024年02月13日
    瀏覽(25)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】Map 和 Set(詳解)

    【高階數(shù)據(jù)結(jié)構(gòu)】Map 和 Set(詳解)

    (???(??? )??,我是 Scort 目前狀態(tài):大三非科班啃C++中 ??博客主頁(yè):張小姐的貓~江湖背景 快上車(chē)??,握好方向盤(pán)跟我有一起打天下嘞! 送給自己的一句雞湯??: ??真正的大師永遠(yuǎn)懷著一顆學(xué)徒的心 作者水平很有限,如果發(fā)現(xiàn)錯(cuò)誤,可在評(píng)論區(qū)指正,感謝?? ????

    2024年01月23日
    瀏覽(59)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】封裝Map和Set

    【高階數(shù)據(jù)結(jié)構(gòu)】封裝Map和Set

    (???(??? )??,我是 Scort 目前狀態(tài):大三非科班啃C++中 ??博客主頁(yè):張小姐的貓~江湖背景 快上車(chē)??,握好方向盤(pán)跟我有一起打天下嘞! 送給自己的一句雞湯??: ??真正的大師永遠(yuǎn)懷著一顆學(xué)徒的心 作者水平很有限,如果發(fā)現(xiàn)錯(cuò)誤,可在評(píng)論區(qū)指正,感謝?? ????

    2024年01月20日
    瀏覽(28)
  • 【高階數(shù)據(jù)結(jié)構(gòu)】手撕紅黑樹(shù)(超詳細(xì)版本)

    【高階數(shù)據(jù)結(jié)構(gòu)】手撕紅黑樹(shù)(超詳細(xì)版本)

    (???(??? )??,我是 Scort 目前狀態(tài):大三非科班啃C++中 ??博客主頁(yè):張小姐的貓~江湖背景 快上車(chē)??,握好方向盤(pán)跟我有一起打天下嘞! 送給自己的一句雞湯??: ??真正的大師永遠(yuǎn)懷著一顆學(xué)徒的心 作者水平很有限,如果發(fā)現(xiàn)錯(cuò)誤,可在評(píng)論區(qū)指正,感謝?? ????

    2024年01月17日
    瀏覽(28)

覺(jué)得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包