圖論——迪傑斯特拉算法和最小生成樹


前言

復習一下迪傑斯特拉算法,由於最小生成樹的Prim算法與迪傑斯特拉算法極其類似,再順便復習下最小生成樹,順便找兩道水題驗證代碼正確性。

迪傑斯特拉算法

目的

該算法用於單源最短路,求一個圖中,從起點S,到終點E的最短路徑

思路

算法基於貪心思想,簡單來講就是兩步:

  • 找出起點距離其他點的最短距離中的最小的那個
  • 用最小的來更新其他點的最短距離,更新完后舍棄

依我所見,迪傑斯特拉類似於排序,假設從起點到其他點的路徑為邊。

  • 選出最短的邊,通過最短的邊來更新其他的邊。
  • 再通過第二短的邊,更新其他的邊。
  • 整個過程就是從小到大依次找出從起點到其他點的最短邊

題目

牛客網:https://ac.nowcoder.com/acm/problem/17511

普通方法

先遍歷頂點,再遍歷該頂點到其他頂點的邊,時間復雜度:\(O(n^2)\)

#include <bits/stdc++.h>
#define ll long long
#define MAX 1005
using namespace std;
 
int mp[MAX][MAX],ans[MAX],n,m,s,t;
bool used[MAX];
 
void init(){
    scanf("%d%d%d%d",&n,&m,&s,&t);
    memset(mp,0x3f,sizeof(mp));
    memset(ans,0x3f,sizeof(ans));
    memset(used,false,sizeof(used));
    ans[s] = 0;
    for(int i = 1;i <= m;i++){
        int x,y,v;
        scanf("%d%d%d",&x,&y,&v);
        mp[x][y] = mp[y][x] = min(mp[y][x],v);
        if (x == s) ans[y] = mp[x][y];
        else if (y == s) ans[x] = mp[x][y];
    }
}
int dijkstra(int start,int end){
 
    while(true){
        int min_edge = 0;
        for(int i = 1;i <= n;i++)//尋找從起點到其他點的路線中的最短路線
            if (!used[i]&&(!min_edge || ans[i] < ans[min_edge]))
                min_edge = i;
        //當找到終點時,可提前退出
        if (min_edge == end || !min_edge) break;
        used[min_edge] = true;
 
        for(int i = 1;i <= n;i++)
            ans[i] = min(ans[i],ans[min_edge]+mp[min_edge][i]);
    }
 
    return ans[end]==0x3f3f3f3f?-1:ans[end];
}
 
int main(){
    init();
    printf("%d\n",dijkstra(s,t));
    return 0;
}

優先隊列(堆)

m為邊數,n為頂點數
先上結論,時間復雜度\(O(m*logn)\)

for(int i = 1;i <= n;i++)//尋找從起點到其他點的路線中的最短路線
    if (!used[i]&&(!min_edge || ans[i] < ans[min_edge]))
        min_edge = i;

顯然,對於上面代碼,可以使用優先隊列(堆)來使得時間復雜度降為\(O(logn)\),但是!!!下面還有個for循環,所以本身時間復雜度還是\(O(n^2)\)

for(int i = 1;i <= n;i++)
    ans[i] = min(ans[i],ans[min_edge]+mp[min_edge][i]);

那么,可否把這里也改下呢?顯然,我們不需要遍歷所有的頂點,因為點到點不一定有邊,這時候我們只需要遍歷邊即可,也就是把邊存儲起來,即使用鄰接表。

總結一下,整個過程每個頂點可能遍歷了多次,但其中只有一次需要遍歷鄰接的邊,即所有邊也只需要遍歷一次(無向邊就是兩次),由於使用了堆,所以還需要加上用堆的時間復雜度,所以總的時間復雜度為\(O(m*logn)\)

下面是代碼

#include <bits/stdc++.h>
#define ll long long
#define MAX 1005
using namespace std;

int ans[MAX],n,m,s,t;//ans為起點到某一點的最短路線
vector<pair<int,int>> mp[MAX*10];//鄰接表存儲邊,mp下標表示起點,pair第一個值表示長度,第二個值表示終點
void init(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	memset(ans,0x3f,sizeof(ans));
	ans[s] = 0;
	for(int i = 1;i <= m;i++){
		int x,y,v;
		scanf("%d%d%d",&x,&y,&v);
		mp[x].push_back(make_pair(v,y));
		mp[y].push_back(make_pair(v,x));
	}
}
int dijkstra(int start,int end){

	priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>> > p_que;
	p_que.push(make_pair(ans[start],start));//pair第一個值表示長度,第二個值表示頂點

	while(!p_que.empty()){
		pair<int,int> node = p_que.top();
		p_que.pop();
		if (node.first > ans[node.second]) continue;
		for(int i = mp[node.second].size()-1;i >= 0;i--){
			pair<int,int> temp = mp[node.second][i];
			if (ans[temp.second] > ans[node.second]+temp.first){
				ans[temp.second] = ans[node.second]+temp.first;
				p_que.push(make_pair(ans[temp.second],temp.second));
			}
		}
	}

	return ans[end]==0x3f3f3f3f?-1:ans[end];
}

int main(){
	init();
	printf("%d\n",dijkstra(s,t));
	return 0;
}

最小生成樹

目的

給定一個無向圖

  • 生成樹:一個子圖,其任意兩點都能互通,且是棵樹
  • 最小生成樹:邊上有值,且所有邊的和最小的生成樹

Prim算法

思路

假設迪傑斯特拉算法是以一個點為起點,求該起點到其他所有點的最小值,那么,最小生成樹的Prim算法則是以一個集合(有多個點)為起點,求該集合到其他所有點的最小值,並求和,步驟如下:

  • 每次循環,找出集合與非集合的點中的最小邊,假設這個點為\(x\)
  • 集合加入\(x\)點,並遍歷\(x\)與其他點的邊,假設有一條邊\(edge[x][y]\) < 集合到 \(y\) 的距離,則更新集合到\(y\)的距離
  • 回到第一步

題目

牛客網:https://ac.nowcoder.com/acm/problem/15108

代碼

顯然,可以跟迪傑斯特拉一樣,使用堆進行優化,由於時間問題,只給出普通代碼。

#include <bits/stdc++.h>
#define ll long long
#define MAX 1005
using namespace std;
 
int mp[MAX][MAX],ans[MAX],c,n,m;
bool used[MAX];

int Prim(int start){//幾乎和迪傑斯特拉算法一模一樣
 	int len = 0;
    while(true){
        int min_edge = 0;
        for(int i = 1;i <= n;i++)//尋找從集合到其他點的路線中的最短路線
            if (!used[i]&&(!min_edge || ans[i] < ans[min_edge]))
                min_edge = i;
        //len >= 0x3f3f3f3f說明無法生成最小生成樹,即圖不連通
        if (!min_edge || len >= 0x3f3f3f3f) break;
        used[min_edge] = true;
 		len += ans[min_edge];//加上最小值
        for(int i = 1;i <= n;i++)
            ans[i] = min(ans[i],mp[min_edge][i]);//注意這里和迪傑斯特拉不同,也幾乎是唯一的不同點
    }
 
    return len;
}
 
void init(int start){
	while(~scanf("%d%d%d",&c,&m,&n)){
	    memset(mp,0x3f,sizeof(mp));
	    memset(ans,0x3f,sizeof(ans));
	    memset(used,false,sizeof(used));
	    ans[start] = 0;
	    for(int i = 1;i <= m;i++){
	        int x,y,v;
	        scanf("%d%d%d",&x,&y,&v);
	        mp[x][y] = mp[y][x] = min(mp[y][x],v);
	    }
	    printf("%s\n", Prim(1) <= c ?"Yes":"No");
	}
}
 
int main(){
    init(1);
    return 0;
}

Kruskal

思路

如果說Prim算法是通過點來生成最小生成樹的,那么Kruskal就是通過邊來生成的

  • 將所有的邊從小到大進行排序,我們只從其中選出n-1條邊
  • 每次拿出最小的邊,如果加入這條邊后沒有生成環路,則加入這條邊(這里可以通過並查集判斷,對於一條邊的兩個點,如果是同一個圈子的,那么這兩個點已經聯通了,此時加入這條邊就會生成環路)

題目

牛客網:https://ac.nowcoder.com/acm/problem/15108

代碼

#include <bits/stdc++.h>
#define ll long long
#define MAX_EDGE 10005
#define MAX_POINT 105
using namespace std;

struct edge{
	int from,to,value;
}e[MAX_EDGE];

int father[MAX_POINT],c,n,m;
void initFather(int n) {
	for (int i = 1;i <= n;i++)
		father[i] = i;
}
int findFather(int x){
	return father[x] = (father[x] == x ? x : findFather(father[x]));
}
bool unio(int x,int y) {
	int fx = findFather(x);
	int fy = findFather(y);
	if (fx != fy) {
		father[fx] = fy;
		return true;
	}
	return false;
}
bool cmp (edge a,edge b) {
	return a.value < b.value;
}
int Kruskal(int n,int m){
 	int len = 0;
 	sort(e,e+m,cmp);
 	for (int i = 0;i < m;i++) {
 		len += unio(e[i].from,e[i].to) ? e[i].value : 0;
 	}
    return len;
}
int main(){
    while(~scanf("%d%d%d",&c,&m,&n)){
		initFather(n);
	    for(int i = 0;i < m;i++){
	        scanf("%d%d%d",&e[i].from,&e[i].to,&e[i].value);
	    }
	    printf("%s\n", Kruskal(n,m) <= c ?"Yes":"No");
	}
    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM