1. 程式人生 > >Dijkstra和Prim演算法 【含數學證明】

Dijkstra和Prim演算法 【含數學證明】

#ACM&演算法

不知是不是因為自己的抽象學習能力還是不夠強,之前在翻閱別人部落格學習這兩個演算法的時候,整個人一直處於懵逼狀態。

分析了一下,發現之前存在的部落格教程,大多趨向於方法講解,學完以後有知其然不知其所以然的感覺。另一部分部落格則偏向數學證明,語言嚴謹但理解起來確實要費一番功夫。所以打算嘗試一下在下文裡以解決一個問題的方式講解這兩個演算法。

在敲演算法之前先闡明一下這兩個演算法所能解決的問題,先從Dijkstra開始。

舉個栗子

我們現在拿到了一張無向圖,如下:
這是一個由7個節點和10條邊組成的無向圖。每條路上的數字代表這條路的長度。 現在任選其中兩個點,我們需要找到一條路把它們連起來,並使這條路儘可能的短。 根據動態規劃的思路,我們把這個問題化解成一個較小的問題去解決。我們將挑選兩個緊挨著(有邊直接相連)的點,A和B。 顯而易見,如果從A到B有一條路能聯通的話,那這條路只有兩種可能: 1,這條路只有一條邊,直接連線A和B。 2,這條路有多條邊,從A出發,經過其他點中轉,到達B。 那我們分別來討論。 這兩種方法的共同點就是,都要從A點出發。而從A點出發的路是有限的。那也就是說,如果從A出發直達B的這條路是從A出發所有路中最短的一條,那麼這條路一定是AB間的最短路
。 證明也很簡單,因為所有路的長度都是正數,所以如果有另一條路從A出發繞過某些點到達B的話,那它一定會在從A出發的所有路的長度的基礎上再加上一些數(至少為0)。也就絕對不可能小於AB直接連線的長度。 如下:
我們先把起點標記為0,意為起點到自己的距離為0。因為這一定是A到A的最短路,所以把所有和A連線的點的值更新為A點的值加上A到該點的路程距離。 接著,根據剛才的論述,我們確定B的值已經更新為A到B的最短路,故,我們對B點也進行上述操作。得到下圖:
在本圖中,已經更新過值,但還未確定為最短路的節點有C/E/F,他們中值最小的是E,那我們可以確定E現在的值就是從A到E的最短路。 原因同上,我們已經把A和B所有能走的路嘗試走過了,如果從C和F還有另一條路能繞過某些點到達E的話,那一定比E現在的值要大,因為C和F的值已經比E大了,加上一個大於等於零的數只會更大。 重複進行上述操作,我們最終會得到如下的圖:

這時候,我們會發現,圖上未被確定為最短路的點裡,值最小的就是目標點D了,那我們就可以得出,從A到D的最短路是現在D的值,即12。 而Prim演算法所解決的問題與上面的略有不同。 我們現在依然是面對這張無向圖:
現在,我們需要從這些路徑中挑選出幾條,使得所有點都可以通過這些路相互連通,並要求這些路的總長度儘量短。 先從思路講起, 既然是求全聯通的最短路,那麼只要是已經連在一起的點,我們都可以把它們看成同一個點,而它們直接連線的所有的邊,都是這個點直接連線的邊。 用例子解釋一下:假如我們已經挑選了兩條路連線了A、B、C三個點:
我們通過紅色的路連線了A、B、C三點,圖中所有標為黃色的路就是這個連通體外部所有直接連線的路。那麼,我們可以確定,為了使E點接入連通體,我們最少的花費一定是通過C-E這條路。 原因同上,如果通過其它路去連線的話,那這條路一定比現有的路還要長一些,所以當前標為黃色的路中最短的一條一定是下一步選擇的最優解。 反覆上述操作,我們能得到下圖:

即為該無向圖的最小生成樹。 附Dijkstra程式碼,題目是HDOJ 1874:
#include<iostream>
#include<algorithm>
#include<string.h>

using namespace std;
#define MAX 1e9;

int city, road;
long vis[205];

struct point{
	long dis=1e9;
	int conect[205][2];
	long i=0;
}node[205];


void dij(long be, long ed)
{
	if (vis[be]==1)//如果已經踩過了 就不管了 【退出】
	{
		return;
	}
	vis[be] = 1; //如果沒踩過 就標記成 踩過了
	for (size_t i = 0; i < node[be].i; i++) // 對於所有的 聯通 的 點 更新 注意這個min函式
	{
		node[node[be].conect[i][0]].dis = min(node[node[be].conect[i][0]].dis, node[be].dis + node[be].conect[i][1]);
	}
	long minn=1e9;//為 尋找 下一個根節點做準備
	int targ=1e9;
	for (size_t i = 0; i < city; i++)
	{
		if (minn>node[i].dis&&vis[i]==0)//找到 未踩過的  dis值最小的 點 作為根節點
		{
			minn = node[i].dis;
			targ = i;
		}
	}
	if (targ==1e9)//如果沒有 【退出】
	{
		return;
	}
		dij(targ, ed);//否則 對這個根節點 重複上述操作。
}


int main()
{
	
	while (cin >> city >> road)
	{
		memset(vis, 0, sizeof(vis));
		for (size_t i = 0; i < city; i++)
		{
			node[i].dis = 1e9;
			node[i].i = 0;
		}
		while (road--)
		{
			long a, b,c;
			cin >> a >> b>>c;
			node[a].conect[node[a].i][0] = b;
			node[a].conect[node[a].i][1] = c;
			node[a].i++;
			node[b].conect[node[b].i][0] = a;
			node[b].conect[node[b].i][1] = c;
			node[b].i++; 
		}
		long be, ed;
		cin >> be >> ed;
		node[be].dis = 0;
		dij(be, ed);
		if (node[ed].dis==1e9)
		{
			cout << "-1\n";
		}
		else
		{
			cout << node[ed].dis << "\n";
		}
	}
}


Prim程式碼 題目是HDOJ 1863:
/*Author:	Res*/
/*Date: 16/2/16*/

/*使用鄰接矩陣存圖,點數查詢,沒有使用並查集*/

#include<iostream>
#include<algorithm>
#include<cmath>
#include<string.h>

using namespace std;
int con[1005][1005];//鄰接矩陣
int confar[1005];//已連通的圖到未聯通點的距離,若距離為0為無法連通,距離為1e9為已聯通
long long ans,node;//node是已聯通的點的數量


int main()
{
	int be, ed,lenth,n,m,target;
	while (cin >> m >> n)
	{
		memset(con, 0, sizeof(con));//初始化
		memset(confar, 0, sizeof(confar));
		ans = 0;
		node = 1;
		target = 0;//是否有某個點無法連通

		if (m==0)
		{
			return 0;
		}
		for (size_t i = 0; i < m; i++)
		{
			cin >> be >> ed >> lenth;
			if (con[be][ed])//存入鄰接矩陣,因為兩點間有多條路,只存最短的一條。
			{
				con[be][ed] = min(con[be][ed], lenth);
			}
			else
			{
				con[be][ed] = lenth;
			}
			if (con[ed][be])
			{
				con[ed][be] = min(con[ed][be], lenth);
			}
			else
			{
				con[ed][be] = lenth;
			}
		}

		be = 1;
		confar[1] = 1e9;
		while (node != n)
		{
			for (size_t i = 1; i <= n; i++)
			{
				if (con[be][i]&&confar[i]!=1e9)//在該點的鄰接表中搜索更近的路程,存入距離表confar中
				{
					if (confar[i])
					{
						confar[i] = min(confar[i], con[be][i]);
					}
					else
					{
						confar[i] = con[be][i];
					}
				}
			}
			int find = 1e9;
			for (size_t i = 1; i <= n; i++)
			{
				if (find>confar[i]&&confar[i]!=0)//在距離表中搜索最近的未使用的路
				{
					find = confar[i];
					be = i;
				}
			}
			if (find==1e9)
			{
				target = 1;
			}
			ans += find;//累加
			confar[be] = 1e9;
			node++;
		}
		if (target)
		{
			cout << "?\n";
		}
		else
		{
			cout << ans << "\n";
		}
	}
}


1,無向圖:由若干點與邊構成,每條邊都連線著兩個點,且每條邊都可以雙向聯通的圖。
2,上述兩個演算法,當遇到待選取的路距離相同時,可任選一個,對結果沒有影響。