前言
復習一下迪傑斯特拉算法,由於最小生成樹的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;
}