算法每周一题(一)——单源最短路
原题目
题目描述
给定一个 \(n\) 个点,\(m\) 条有向边的带非负权图,请你计算从 \(s\) 出发,到每个点的距离。
数据保证能从 \(s\) 出发到任意点。
输入格式
第一行为三个正整数 \(n, m, s\)。 第二行起 \(m\) 行,每行三个非负整数 \(u_i, v_i, w_i\),表示从 \(u_i\) 到 \(v_i\) 有一条权值为 \(w_i\) 的有向边。
输出格式
输出一行 \(n\) 个空格分隔的非负整数,表示 \(s\) 到每个点的距离。
初次尝试:
由于刚刚学过数据结构,很快就有了思路(采用邻接表存储+\(Dijkstra\)算法),但是由于从没实践过😭,所以写的时候有点费劲,又调试了好久,才得到正确的结果:
- 第一代代码:
//单源最短路传统
#include<bits/stdc++.h>
using namespace std;
int dist[100005],path[100005];
bool S[100005];
typedef struct e{
int key;
int d;
struct e *next;
}edge;
typedef struct{
edge *out=NULL;
}node;
void add(node *n,int v,int w)
{
edge *e=(edge *)malloc(sizeof(edge));
e->key=v;
e->d=w;
e->next=n->out;
n->out=e;
}
void kl(node *n,int k)
{
edge *e=n->out;
while(e!=NULL)
{
if(e->d+dist[k]<dist[e->key])
{
dist[e->key]=e->d+dist[k];
path[e->key]=k;
}
e=e->next;
}
}
int main()
{
memset(dist,100,sizeof(dist));//距离初始化为无穷,这里为一个很大的数
memset(path,-1,sizeof(path));
int n,m,s,u,v,w;
node N[100005];
scanf("%d%d%d",&n,&m,&s);
int nn=n;
dist[s]=0;path[s]=s;dist[0]=2000000005;
for(int i=0;i<m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(N+u,v,w);
}
while(nn!=0)
{
int mini=0;
kl(N+s,s);
S[s]=S[0]=1;
for(int i=1;i<=n;i++)
if(dist[i]<dist[mini]&&S[i]==0)mini=i;//寻找距离最短的结点来考虑
s=mini;
nn--;
}
for(int i=1;i<=n;i++)printf("%d ",dist[i]);
return 0;
}
痛苦地写了很久很久,结果全部超时了,简直太悲伤了!!😭😭
又审视写下的代码,发现while(nn!=0)内存在两个循环,一个在kl()函数中,但遍历所有点是算法要求因此不作优化;另一个“寻找距离最短的结点”时间复杂度为O(n),需要考虑对其进行优化。由于总是取出距离最短的结点,想到可以使用堆来进行维护,时间复杂度为O(1),但此时笔者已经心太累只想快点完成这道题,因此直接使用STL库中定义好的优先队列——堆来进行优化。
-
小资料
-
stl优先队列的使用:
-
//定义一个优先队列 std::priority_queue<Type,Container,Functional> q; //数据操作 q.push()//入队 q.pop()//出队 q.top()//返回顶部元素 q.empty()//返回队列是否为空 /*特别地,stl优先队列使用<进行大小比较,对于自定义的容器,需要在内部重载运算符才能正常使用。*/ //使用范例 #include<bits/stdc++.h> using namespace std; struct node{ int k; int m; bool operator <(const node &x)const{ return k<x.k;//默认为大根堆,相反则为小根堆 }//关键部分,定义逻辑比较符,声明比较的内容 }; int main() { std::priority_queue<node> q; q.push({1,2}); q.push({2,1}); q.push({0,7}); node temp=q.top(); cout<<temp.k<<" "<<temp.m; return 0; }
-
解法优化
优化的关键在于想到把新旧的<距离,结点>组全部放在一个堆中是没有关系的,只要取出以后判断结点是否已经访问过即可。这样就方便多了。其次本题中path[]数组也是没有用的,直接注释掉。
- 优化后代码
//单源最短路堆优化
#include<bits/stdc++.h>
using namespace std;
int dist[100005];//int path[100005];//本题path[]数组好像没什么用
bool vis[100005];
//定义数据结构
struct edge{
int key;
int d;
edge *next;
};
struct node{
edge *out=NULL;
}N[100005];
struct node_dist{
int dist;
int node;
bool operator < (const node_dist &x)const
{
return dist>x.dist;
}//重载运算符使兼容,并定义成小根堆
};
std::priority_queue<node_dist> q;//使用stl库定义一个优先队列
//定义函数
void add(node *n,int v,int w)
{
edge *e=(edge *)malloc(sizeof(edge));
e->key=v;
e->d=w;
e->next=n->out;
n->out=e;
}
void kaolv(node *n,int k)
{
edge *e=n->out;
while(e!=NULL)
{
if(e->d+dist[k]<dist[e->key])
{
dist[e->key]=e->d+dist[k];
// path[e->key]=k;
q.push({dist[e->key],e->key});
}
e=e->next;
}
}
int main()
{
int n,m,s,u,v,w;
memset(dist,100,sizeof(dist));//将dist[]数组所有元素初始化为无穷(很大的值)
// memset(path,-1,sizeof(path));
scanf("%d %d %d",&n,&m,&s);
int nn=n;
//输入
for(int i=0;i<m;i++)
{
scanf("%d %d %d",&u,&v,&w);
add(N+u,v,w);
}
//Dijkstra
dist[s]=0;//path[s]=s;
q.push({0,s});
while(!q.empty())
{
node_dist tmp=q.top();
q.pop();
int n=tmp.node;
if(!vis[n])
{
kaolv(N+n,n);//考虑该点
vis[n]=1;
}
}
//输出
for(int i=1;i<=n;i++)printf("%d ",dist[i]);
return 0;
}
终于过了,记录下成功的喜悦,2021-8-4 凌晨1点24分!!