圖論 最短路總結

寫在前面:圖論題的調試真感人
讓咱們進入正題node

最短路是啥

emmm 顧名思義最短路就是求一個點到另一個點的最小距離
通常來講最短路分爲:單源最短路和多源最短路
單源最短路就是求一個源點到另外多個點的最短距離
而多源最短路就是求多個點到其餘點的最短距離
算法通常有:算法

  • floyd(多源 O(\(n^3\)))
  • dijkstra(單源 O(\(n^2\)) 可用堆優化到O(\(n*log_n\)))
  • Bellman-Ford(單源 O(\(nE\)))
  • SPFA(單源 O(nE 可是比BF強))
    具體的優劣以及使用範圍咱們會在下面具體講解

floyd 算法

  • 適用範圍 : 多源最短路 可處理負權 可是不能處理負環 運行一次可求得任意兩點間最短路
  • floyd算法其實很好理解 也很好寫(畢竟 O(\(n^3\)))有的時候能夠將其看成dp理解
  • 先來想這樣一個問題 :
    如今你在老家(B地)自由自在的玩耍着 忽然有人告訴你去A地能給你分對象 (咳咳) 而後你就屁顛屁顛的跑去了A地 可是有好多人都要去A地 你但願能夠最快到達A地(也就是路徑最短)
  • 顯然你能夠直接坐車從B地直接趕往A地 可是這樣必定是最短的嗎?
    然並卵 畢竟路上你要走山路十八彎 而這時C地出如今你的面前 從B直接到A要走1000km(反正很遠很遠) 可是從B到C只須要 1 km,從C到B呢也只須要 1 km(反正很短很短)那你確定會先到C 再到B吧
  • 這就是咱們的核心思路了
    揪黑板!!
    若是咱們已知並記錄了從i到j的最短路徑 而若是將k做爲中轉點可使得咱們的最短路徑更短 那咱們就更新i到j的最短路徑 (其餘算法也會用到這個思想,即下面的鬆弛操做)

核心代碼實現:

for(int k=1;k<=n;++k)//枚舉中轉點
    for(int i=1;i<=n;++i)//枚舉邊的起點
        for(int j=1;j<=n;++j)//枚舉邊的終點
            if(a[i][j]>a[i][k]+a[k][j])//鬆弛操做(即利用第三個點來判斷是否能夠更新目標兩個點的最短距離)
                a[i][j]=a[i][k]+a[k][j];//a[i][j]是從i到j的最小值

關於k爲何要枚舉在第一層循環:
剛纔已經說過floyd相似於dp,而k就是dp的階段(dp的階段顯然要枚舉在第一層的),其實a原本是三維a[k][i][j]表示只通過前k個點從i到j的最短路,而能夠將第一維的k捨去(like揹包) 因此就成了如今的樣子啦函數


dijkstra 算法

  • 適用範圍:單源最短路 不能處理帶有負權邊的圖 須要指定起點s
  • dij是求最短樓最經常使用的方法也是最經典的:
    而後維護一個集合S用於存放已經知道對於源點s的最短距離的點
    另一個集合U用於維護還不知道對於源點s的最短距離的點(可是能夠知道當前不徹底狀態下的最短距離)
  • 初始時 S中只有s本身 距離本身的距離是0 而其餘點距離s的距離都初始化爲正無窮
    而後咱們利用這個點來求出對於其餘點的最短距離
  1. 首先進行一次鬆弛操做 將s能夠直接到達的點的距離dis[i]記錄下來 而後更新i點的距離(若是比當前已知的s到i的最短距離更短的話)
  2. 從U集合中選出一個距離s最短的點 將其加入到S集合中 而後利用這個點再去更新另一些點的距離
  3. 在新出現的點中選出距離s最短的點 加入到S集合中 而後再利用新點再去更新其餘點距離
  4. 重複以上步驟知道目標點距離源點s的距離求出或者沒法再更新
  • 下面是圖例演示

核心代碼實現 :

void dij(int s){
	memset(vis,0,sizeof(vis));
	vis[s] = 1;//將s放入S集合
	for(int i = 1;i <= n;++i){
		if(g[s][i]){dis[i] = g[s][i];}//若是從s到i有路的話 就將s到i的距離設置爲長度
		else dis[i] = 0x3f3f3f3f;//將其餘點設置爲正無窮(即目前沒法到達)
	}
	dis[s] = 0;//源點s到本身的最短距離是0
	for(int i = 1;i < n;++i){//遍歷每個點以求出每個點距離源點的最小距離
		int Min = 0x3f3f3f3f,k = 0;//Min維護這一輪維護後要放入S集合的距離最小值,k維護要放入S集合的點
		for(int j = 1;j <= n;++j)
			if(!vis[j] && Min > dis[j]){//若是點j尚未在S集合中而且s到當前節點的距離更小
				Min = dis[j];k = j;
			}
		vis[k] = 1;//k放入S集合
		for(int j = 1;j <= n;++j){
			if(g[k][j] && dis[j] > dis[k] + g[k][j]){//若是能夠經過k鬆弛
				dis[j] = dis[k] + g[k][j];//更新到j的最小值
			}
		}
	}
}

關於優化:優化

  • 上一個只是樸素的最短路算法 有的時候並不能知足咱們的要求(和出題人喪心病狂的卡空間時間
  • 咱們能夠用鄰接表去存邊 後面遍歷的時候就能夠用鄰接表了 這樣的時間複雜度大約是常數 遠低於樸素算法的O(n)
  • 求集合外的點到源點的最小值咱們能夠建一個小根堆,這樣咱們的時間複雜度就是進堆的時間消耗 , 爲O\((Elog_E\)),(E爲邊數),這裏用優先隊列進行操做

核心代碼實現:

void dij(int x){
	priority_queue<node> q;
	memset(vis,0,sizeof(vis));
	memset(dis,0x3f,sizeof(dis));
	dis[x] = 0;
	q.push(node(x,0));
	while(!q.empty()){
		node t = q.top();q.pop();
		int k = t.num;
		if(vis[k])continue;
		vis[k] = 1;
		for(int i = head[k];i;i = a[i].next){
			int v = a[i].to;
			if(dis[v] > dis[k] + a[i].dis){
				dis[v] = dis[k] + a[i].dis;q.push(node(v,dis[k] + a[i].dis));
			}
		}
	}
}

Bellman-ford算法

  • 適用範圍 : 基本啥也能用 (前提是不考慮時間複雜度狀況下)
  • 算法思想:和dij很像 可是這裏是沿着邊進行鬆弛操做
  • 對於有向帶權圖, 從源點s開始,利用Bellman-ford,依次求解各頂點的最短距離,
    算法概況:
for(int i = 0;i < n;++i)//枚舉頂點
      for each(i,j)//對於每一條邊
            song_chi(i,j)//鬆弛操做
  • BF算法對每一條邊作鬆弛操做 , 而且重複了n次,所以算法的時間複雜度爲O(n*E)

核心代碼實現:

void BF(int u){
	memset(d,0x3f,sizeof(d));
	d[u] = 0;
	for(int i = 1;i < n;++i){
		for(int j = 1;j <= cnt;++j){//cnt存的是圖中共有幾個邊
			int x = a[j].from,y = a[j].to,z = a[j].dis;
			d[y] = min(d[y],d[x] + z);//鬆弛操做
		}
	}
}
  • 買一送一 BF算法更加實惠 噹噹噹噹噹
    咳咳 既然dij和BF感受實現方法差很少 可是BF有一個dij不能企及的地方:判負環
    若是咱們經過BF算法求得了各個點到源點s的最短路 而後再進行一次鬆弛呢?
    若是有負環的話是否是咱們會再從新跑一遍負環而後讓各個點的值更小? 因此利用這個性質咱們就能夠來判斷是否有負環啦!
bool check(){
	for(int i = 1;i <= cnt;++i){
		int x = a[i].from,y = a[i].to,z = a[i].dis;
		if(d[y] < d[x] + z)return 1;
	}
	return 0;
}
//主函數中:
if(check()){
	printf("NO\n");return 0;
}

SPFA算法

  • 適用範圍:反正BF能用的它都能用(文章開頭說過它能夠當作是BF的優化)
  • BF每次都經過全部的邊來鬆弛出一個新點的最短距離 可是這樣太浪費了
  • 只有那些已經鬆弛過的點纔可能去鬆弛別的點,因此咱們能夠用一個隊列來記錄鬆弛成功了的點,以此用這些點來鬆弛鄰接點(顯然優化不小吧 能寫這個就寫這個 嘿嘿 若是你剛剛看過BF而且苦思冥想請不要怪罪博主 還能提升代碼能力的

核心代碼實現

struct node{
	int to,dis,next;
}a[maxn];

void add(int x,int y,int z){
	a[++cnt].to = y;a[cnt].next = head[x];a[cnt].dis = z;head[x] = cnt;
}

bool spfa(int s){
	memset(dis,0x3f,sizeof(dis));dis[s] = 0;//dis存到源點的最短距離
	queue<int>q;
	q.push(s);flag[s] = 1;//s入隊
	while(!q.empty()){
		int u = q.front();q.pop();flag[u] = 0;//由於一個節點u可能屢次進隊
		for(int i = head[u];i;i = a[i].next){//鄰接表存邊
			int v = a[i].to;
			if(dis[v] > dis[u] + a[i].dis){//鬆弛操做:沒錯,仍是我!!!
				dis[v] = dis[u] + a[i].dis;
				if(!flag[v]){//優化
					if(++num[v] >= n)return 0;若是同一個點被屢次鬆弛 那麼確定有負環(這個判斷也比剛纔的少女口阿  把前輩666扣在公屏上)
					q.push(v);flag[v] = 1;//v進隊,標記
				}
			}
		}
	}
	return 1;
}
//主函數中:
if(!spfa(源點))輸出NO
else 輸出距離

好了 本文閱讀到此結束了spa


碼字不易 推薦走起調試


若是您有不懂的地方 或者 您發現代碼有問題能夠在下方評論或者給博主留言code

感謝觀看>_<對象

相關文章
相關標籤/搜索