Graph 图论
1、最短路(Dijkstra)
不足:当有向图中出现负权时,则Dijkstra算法失效。
1、Dijkstra单源最短路(二维数组)
首先我们需要这样几个数组:
vis[ i ]:编号为 i 的节点是否被访问,被访问过了置为1,没有就置为0,源点的vis值置为1,因为从他本身开始走,本身肯定是被访问过;
dist[ i ]:从源点开始到编号为 i 的点的最短路径,源点的dist值置为0,因为从他自己开始到他自己本身的距离就是0;
map[ i ][ j ]:从 i 到 j 的道路的长度,如果从 i 到 j 没有道路,道路长度置为INF;
我们的准备工作做好了我们就一起来看一下dijkstra的大致的实现步骤:
①从dist最小的值的点node开始遍历他所能到达的其他点,如果从node点到其他点 i 的距离能够被更新,那我们就更新对应的dist值(源点 -> ……-> i 的距离大于源点 -> …… -> node -> i 的距离,也就是经过node点再到 i 点的距离要更近一些,我们就把dist[ i ]的值更新)
②:定义一个Min 来记录还没有被访问过且从源点到该点的最小值,定义一个mini来记录最小值的点对应的编号,初始值为:Min = INF,mni = -1。
为什么我们要找最小的那个点呢,因为假设我们已知的当前路径最短的点是 p 点,那么 p 点对应的dist值不会被更新( 因为源点到别的点的距离大于等于到p点的距离,其他点到p点的距离大于等于0,所以由这两个大于等于的关系我们可以得出:
源点 -> …… -> p 的距离 >= 源点 -> …… -> 其他点 -> …… -> p 的距离 );
所以我们下一步的寻找就从 p 点递归寻找,如果没有被更新,所以Min的值和mini的值没有被改变,即mini的值是-1,所以递归终止的条件:如果 mini == -1,递归终止,返回到主函数;
void dijkstra(int node){//从node点开始更新寻找
for(int i=1;i<=n;i++){
if(vis[i]==0 && dist[i] > dist[node]+map[node][i]){//可以被更新的条件
dist[i] = dist[node] + map[node][i];//更新
}
}
int Min = 0x7f7f7f7f;
int mini = -1;
for(int i=1;i<=n;i++){
if(vis[i]==0 && dist[i]<Min){//寻找当前最短的点,此处可以优化
Min = dist[i];
mini = i;
}
}
if(mini == -1) return;
vis[mini] = 1;
dijkstra(mini);
}
接下来就是我们的主函数了,主函数需要注意的几个地方就是对于数组的初始化与读入,注意建立边的时候是建立有向边还是无向边
①:建立有向边,那么就只建立一个方向,
for(int i=0;i<m;i++){//建立无向边
int a,b;
int c;
cin >> a >> b >> c;
if(!map[a][b]) map[a][b] = c;
else{
map[a][b] = min(map[a][b],c);
}
}
②:建立无向边,那么我们就要同时更新两个方向
for(int i=0;i<m;i++){//建立有向边
int a,b;
int c;
cin >> a >> b >> c;
if(!map[a][b]) map[a][b] = map[a][b] = c;
else{
map[a][b] = map[b][a] = min(map[a][b],c);
}
}
完整代码如下(以建立有向边为例)
#include<iostream>
#include<cstring>
using namespace std;
int map[10005][10005];
long long dist[20001];
int vis[20001];
int n,m;
int begin,end;
const long long inf = 2147483647;
void dijkstra(int node){
for(int i=1;i<=n;i++){
if(vis[i]==0 && dist[i]>dist[node]+map[node][i]){
dist[i] = dist[node] + map[node][i];
}
}
int Min = 0x7f7f7f7f;
int mini = -1;
for(int i=1;i<=n;i++){
if(vis[i]==0 && dist[i]<Min){
Min = dist[i];
mini = i;
}
}
if(mini == -1) return;
vis[mini] = 1;
dijkstra(mini);
}
int main(){
cin >> n >> m;
cin >> begin;
memset(map,0x7f7f7f7f,sizeof(map));
memset(vis,0,sizeof(vis));
memset(dist,0x7f7f7f7f,sizeof(dist));
for(int i=0;i<m;i++){
int a,b;
int c;
cin >> a >> b >> c;
if(!map[a][b]) map[a][b] = c;
else{
map[a][b] = min(map[a][b],c);
}
}
dist[begin] = 0;
vis[begin] = 1;
dijkstra(begin);
for(int i=1;i<=n;i++){
if(vis[i]==0) cout << inf << " ";
else cout << dist[i] << ' ';
}
return 0;
}
我们很容易注意到用二维数组进行建边时很多都是没有用到的,浪费了大量的内存,并且我们在更新边的时候也是把所有节点都遍历的一遍,这样也浪费了时间,因此我们可以使用邻接表来节省时间和空间:
Dijkstra(邻接表法)
思路跟二维数组的想法一致,就是存储的方式不一样,我们用vector来实现邻接表,因为节点为 i 的点后面要存储的信息有两部分构成,一个是连接的下一个节点的编号,一个是路径长度,因此我们用一个结构体来存储这两部分信息:
struct infor{
int b;//相邻接的节点编号
int cost;//路径长度
};
vector<infor>map[20001];
其余操作跟上述方法一致,遍历的时候也不再是遍历n个点,而是只需要遍历与他相邻的节点就可以了
完整代码如下:
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
struct infor{
int b;
int cost;
};
vector<infor>map[20001];
int vis[20001];
int dist[20001];
int n,m;
const long long inf = 2147483647;
void dijkstra(int node){
for(int i=0;i<map[node].size();i++){
infor a = map[node][i];//把邻接节点的信息取出来进行操作,方法跟上述方法一致
if(dist[a.b] > dist[node] + a.cost){
dist[a.b] = dist[node] + a.cost;
}
}
int Min = 0x7f7f7f7f;
int mini = -1;
for(int i=1;i<=n;i++){
if(vis[i]==0 && dist[i]<Min){
Min = dist[i];
mini = i;
}
}
if(mini == -1) return ;
vis[mini] = 1;
dijkstra(mini);
}
int main(){
int begin;
cin >> n >> m >> begin;
memset(vis,0,sizeof(vis));
memset(dist,0x7f7f7f7f,sizeof(dist));
for(int i=0;i<10009;i++) map[i].clear();
for(int i=0;i<m;i++){
int a,b,c;
cin >> a >> b >> c;
infor node;//存储邻接节点信息
node.b = b;
node.cost = c;
map[a].push_back(node);//建立邻接表
}
vis[begin] = 1;
dist[begin] = 0;
dijkstra(begin);
for(int i=1;i<=n;i++){
if(dist[i]==0x7f7f7f7f) cout << inf << " ";
else cout << dist[i] << " ";
}
return 0;
}
Dijkstra + 邻接矩阵 + 堆优化
我们发现在寻找Min和mini的时候我们同样也是把n个点循环遍历了一遍,这样会让程序的时间复杂度变成n2,在这里我们同样可以使用一个叫做堆的东西(后续会补上这方面的知识),在这里我们偷懒的用STL库中的优先队列(priority_queue),我们在出队的时候会自动的按照我们想要排序的大小进行,这一步把时间复杂度整体的降为了O(mlogn),整体思路还是不变,就是在遍历dist数组的时候稍加改动了,需要注意的地方就是我们每一次出队的时候起始点与终点有可能都被访问过,所以这样的节点是我们不需要的,就让他出队,直到我们找到只有一方被访问过的节点或者队列为空为止。
#include<iostream>
#include<vector>
#include<cstring>
#include<queue>
using namespace std;
struct infor{
int b;
int cost;
};
struct edge{
int a,b;
int cost;
friend bool operator < (edge x,edge y){
return x.cost > y.cost;
}
};
vector<infor>map[20001];
priority_queue<edge>q;
int vis[20001];
int dist[20001];
int n,m;
const long long inf = 2147483647;
void dijkstra(int node){
for(int i=0;i<map[node].size();i++){
infor a = map[node][i];
if(dist[a.b] > dist[node] + a.cost){
dist[a.b] = dist[node] + a.cost;
edge p;
p.a = node;
p.b = a.b;
p.cost = dist[a.b];
q.push(p);
}
}
int Min = 0x7f7f7f7f;
int mini = -1;
while(!q.empty()){
edge p = q.top();
q.pop();
if(vis[p.a]+vis[p.b] == 1){
if(vis[p.a]==0){
mini = p.a;
Min = p.cost;
}
else{
mini = p.b;
Min = p.cost;
}
break;
}
}
if(mini == -1) return ;
vis[mini] = 1;
dijkstra(mini);
}
int main(){
int begin;
cin >> n >> m >> begin;
memset(vis,0,sizeof(vis));
memset(dist,0x7f7f7f7f,sizeof(dist));
for(int i=0;i<10009;i++) map[i].clear();
for(int i=0;i<m;i++){
int a,b,c;
cin >> a >> b >> c;
infor node;
node.b = b;
node.cost = c;
map[a].push_back(node);
}
vis[begin] = 1;
dist[begin] = 0;
dijkstra(begin);
for(int i=1;i<=n;i++){
if(vis[i]==0) cout << inf << " ";
else cout << dist[i] << " ";
}
return 0;
}