前言
复习一下迪杰斯特拉算法,由于最小生成树的Prim算法与迪杰斯特拉算法极其类似,再顺便复习下最小生成树,顺便找两道水题验证代码正确性。
迪杰斯特拉算法
目的
该算法用于单源最短路,求一个图中,从起点S,到终点E的最短路径
思路
算法基于贪心思想,简单来讲就是两步:
- 找出起点距离其他点的最短距离中的最小的那个
- 用最小的来更新其他点的最短距离,更新完后舍弃
依我所见,迪杰斯特拉类似于排序,假设从起点到其他点的路径为边。
- 选出最短的边,通过最短的边来更新其他的边。
- 再通过第二短的边,更新其他的边。
- 整个过程就是从小到大依次找出从起点到其他点的最短边
题目
牛客网:https://ac.nowcoder.com/acm/problem/17511
普通方法
先遍历顶点,再遍历该顶点到其他顶点的边,时间复杂度:\(O(n^2)\)。
#include
#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
#define ll long long
#define MAX 1005
using namespace std;
int ans[MAX],n,m,s,t;//ans为起点到某一点的最短路线
vector> 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,vector>,greater> > p_que;
p_que.push(make_pair(ans[start],start));//pair第一个值表示长度,第二个值表示顶点
while(!p_que.empty()){
pair 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 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算法则是以一个集合(有多个点)为起点,求该集合到其他所有点的最小值,并求和,步骤如下:
- 每次循环,找出集合与非集合的点中的最小边,假设这个点为\(x\)
- 集合加入\(x\)点,并遍历\(x\)与其他点的边,假设有一条边\(edge[x][y]\) < 集合到 \(y\) 的距离,则更新集合到\(y\)的距离
- 回到第一步
题目
牛客网:https://ac.nowcoder.com/acm/problem/15108
代码
显然,可以跟迪杰斯特拉一样,使用堆进行优化,由于时间问题,只给出普通代码。
#include
#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;
}