最短路

写在前面:我是一只蒟蒻。

今天,我们来学习一下图论中很基础的一个问题“最短路问题”,当然,它也可以很难。。。。。。

在正式的讲解最短路之前,我们来聊一聊存图方式。

对于一张有向图,我们一般由邻接矩阵和邻接表两种方式进行存储。对于无向图,可以把无向边看做两条方向相反的有向边,从而采用与有向图相同的存储方式,所以一下讲解最短路时,就以有向图为基准来进行讲解。

邻接矩阵,这是一个十分简单的存图方式,使用二维数组进行存储,但是空间复杂度O(n2)是十分不优秀的。很容易就“爆”了。

所以,我们就使用邻接表的方法进行存图。

具体如何存,下面是代码:

 1 struct node{
 2     int to,next,w;
 3 }e[2*MAXN];
 4 int cnt=0,head[MAXN];
 5 void add(int u,int v,int w){
 6     cnt++;
 7     e[cnt].next=head[u];
 8     e[cnt].to=v;
 9     e[cnt].w=w;
10     head[u]=cnt;
11 }

使用的时候我们就可以通过head【】来进行

1 for(int i=head[x];i;i=e[i].next){
2     int y=e[i].to;
3     ......//进行一系列的操作
4 }

好的,前面我们就说到这里,下面进入正题

单源最短路

单源最短路问题(SSSP问题)

是说,给定一张有向图G=(V,E),V是点集,E是边集,即有n个点,m条边。节点[1,n]之间的连续整数编号,(x,y,z)表示从x指向y,长度为z的边。

设1号点为起点,求长度为n的数组dis【】,dis【i】表示从1到i的最短路径长度。


Dijkstra 算法

主要流程:

1、初始化dis【1】=0,其余节点的dis值为正无穷。

2、找到一个未标记的dis【x】最小的节点x,然后标记节点x。

3、扫描节点x的所有出边(x,y,z),如果dis【y】>dis【x】+ z,则使用dis【x】+ z来更新dis【y】。

4、重复2,3,直到所有节点都被标记。

代码(非堆优化):

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 int n,m,dis[1005],vis[1005],a[1005][1005];
 4 void dj(){
 5     memset(dis,0x3f3f,sizeof(dis));//dis数组,存储距离 
 6     memset(vis,0,sizeof(vis));//标记是否访问过 
 7     dis[1]=0;
 8     for(int i=1;i<n;i++){//重复n-1次 
 9         int x=0;
10         for(int j=1;j<=n;j++){
11             if(!vis[j]&&(x==0||dis[j]<dis[x])){
12                  x=j;//寻找未标记节点中dis最小的并用全局最小值x更新其他节点 
13             }
14             vis[x]=1;
15         }
16         for(int y=1;y<=n;y++){
17             dis[y]=min(dis[y],dis[x]+a[x][y]);
18         }
19     }
20 }
21 int main(){
22     scanf("%d%d",&n,&m);
23     memset(a,0x3f3f,sizeof(a));
24     for(int i=1;i<=n;i++){
25         for(int j=1;j<=m;j++){
26             int b,c,d;
27             scanf("%d%d%d",&b,&c,&d);
28             a[b][c]=min(a[b][c],d);//矩阵存图 
29         }
30     }
31     dj();
32     for(int i=1;i<=n;i++){
33         printf("%d",dis[i]);
34     }
35     return 0;
36 }

其主要思想是基于贪心的思想,仅适用于无负边权的图,我们不断选择我全局最小值进行标记和扩展更新,最终得到最短路长度。

不难看出,这个算法的时间复杂度为O(n2)十分之高,所以我们尝试进行优化(先留一个坑~~)

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N=100010,M=1000010;
 4 struct edge{
 5     int to,next,w;
 6 }e[N];
 7 int head[N];
 8 bool vis[N];
 9 int dis[N];
10 int n,m,tot;
11 priority_queue<pair <int ,int > >q;
12 void add(int x,int y,int z){
13     e[++tot].next=head[x];
14     e[tot].to=y;
15     e[tot].w=z;
16     head[x]=tot; 
17 }
18 void dijkstra(){
19     memset(dis,0x3f,sizeof(dis));
20     memset(vis,0,sizeof(vis));
21     dis[1]=0;
22     q.push(make_pair(0,1));
23     while(q.size()){
24         int x=q.top().second;q.pop();
25         if(vis[x])continue;
26         vis[x]=1;
27         for(int i=head[x];i;i=e[i].next){
28             int y=e[i].to,z=e[i].w;
29             if(dis[y]>dis[x]+z){
30                 dis[y]=dis[x]+z;
31                 q.push(make_pair(-dis[y],y));
32             }
33         }
34     }
35 }
36 int main(){
37     scanf("%d%d",&n,&m);
38     for(int i=1;i<=m;i++){
39         int x,y,z;
40         scanf("%d%d%d",&x,&y,&z);
41         add(x,y,z);
42     }
43     dijkstra();
44     for(int i=1;i<=n;i++){
45         printf("%d
",dis[i]);
46     }
47     return 0;
48 }

OK,dj结束~^_^~


Bellman-Ford&&SPFA算法

给定一张有向图,若对于图中的某一条边(x,y,z),有dis【y】<=dis【x】+z成立,则称改该边满足三角形不等式。若所有边都满足三角形不等式,则dis数组就是所求最短路。

首先,我们先介绍一下基于迭代思想的算法bellman-ford。(尽管我不会,也不懂什么是迭代)

流程如下:(时间复杂度O(nm))

1、扫描所有边(x,y,z),若dis[y]>dis[x]+z,则进行更新。

2、一直重复,直到没有更新发生

SPFA算法,在国际上通称为“队列优化的bellman-ford算法”。

流程:

1、建立一个队列,最初的队列中只含有起点1。

2、取出队头节点x,扫描它的所有出边(x,y,z)(链式前向星)如果dis[y]>dis[x]+z,则更新dis[y]。同时判断y是否在队列中,如果不在,则将y加入队列。

3、重复操作,直到队列为空。

代码:

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 const int N=100010,M=1000010;
 4 struct edge{
 5     int to,next,w;
 6 }e[N];
 7 int head[N];
 8 bool vis[N];
 9 int dis[N];
10 int n,m,tot;
11 queue<int >q;
12 void add(int x,int y,int z){
13     e[++tot].next=head[x];
14     e[tot].to=y;
15     e[tot].w=z;
16     head[x]=tot; 
17 }
18 void spfa(){
19     memset(dis,0x3f,sizeof(dis));
20     memset(vis,0,sizeof(vis));
21     dis[1]=0,vis[1]=1;
22     q.push(1);
23     while(!q.empty()){
24         int x=q.front();q.pop();
25         vis[x]=1;
26         for(int i=head[x];i;i=e[i].next){
27             int y=e[i].to,z=e[i].w;
28             if(dis[y]>dis[x]+z){
29                 dis[y]=dis[x]+z;
30                 if(!vis[y])q.push(y),vis[y]=1;
31             }
32         }
33     }
34 }
35 int main(){
36     scanf("%d%d",&n,&m);
37     for(int i=1;i<=m;i++){
38         int x,y,z;
39         scanf("%d%d%d",&x,&y,&z);
40         add(x,y,z);
41     }
42     spfa();
43     for(int i=1;i<=n;i++){
44         printf("%d
",dis[i]);
45     }
46     return 0;
47 }

相对于dj来说,spfa可以解决负边权的问题,其时间复杂度也是比较玄学的O(km),有时对于特殊的数据就会变为O(nm),所以spfa有风险,使用需谨慎。

now,The end of spfa。


Floyd 算法

floyd算法的核心思想就是动态规划,最外层的k表示的是阶段,i,j为附加状态,所以应置于内层循环,然后状态转移方程就是d[i][j]=min(d[i][j],d[i][k]+d[k][j])

最终d[i][j]保存了i到j的最短路

 代码:

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 int d[310][310],n,m;
 4 int main(){
 5 scanf("%d%d",&n,&m);
 6 memset(d,0x3f3f,sizeof(d));
 7 for(int i=1;i<=n;i++)d[i][i]=0;
 8 for(int i=1;i<=m;i++){
 9     int x,y,z;
10     scanf("%d%d%d",&x,&y,&z);
11     d[x][y]=min(d[x][y],z);
12 }
13 for(int k=1;k<=n;k++){
14     for(int i=1;i<=n;i++){
15         for(int j=1;j<=n;j++){
16             d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
17         }
18     }
19 }
20 for(int i=1;i<=n;i++){
21     for(int j=1;j<=n;j++){
22         printf("%d ",d[i][j]);
23     }
24     printf(" ");
25 }    
26     return 0;
27 }

下面floyd的传递闭包问题:

定义:

传递闭包、即在数学中,在集合X上的二元关系R的传递闭包是包含R的X上的最小的传递关系。例如,如果X是(生或死)人的集合而R是关系“为父子”,则 R 的传递闭包是关系“x 是 y 的祖先”。再比如,如果X是空港的集合而关系 xRy 为“从空港 x 到空港 y 有直航”,则R的传递闭包是“可能经一次或多次航行从x飞到 y”。(摘自百度百科)。虽然我也不是太懂这个是什么意思qwq

下面是另一组定义:

在交际网络中,给定若干个元素和若干对二元关系,且关系具有传递性(设O是定义在集合s上的二元关系,若对于任意a,b,c属于s,只要有aOb且bOc,就必然有aOc,则称O具有传递性)。通过传递性推导出尽量多的元素之间的关系的问题被称为传递闭包。

通俗一点来说,就是通过传递性,尽可能多的得出元素之间的关系。

所以我们建立邻接矩阵d,其中d[i][j]=1表示有关系,d[i][j]=0则表示没有关系。特别的d[i][i]始终为一

所以我们就可以使用floyd算法来解决传递闭包问题

代码:

 1 #include<bits/stdc++.h>
 2 using namespace std;
 3 bool d[600][600];
 4 int n,m;
 5 int main(){
 6     scanf("%d%d",&n,&m);
 7     for(int i=1;i<=n;i++)d[i][i]=1;
 8     for(int i=1;i<=m;i++){
 9         int x,y;
10         scanf("%d%d",&x,&y);
11         d[x][y]=d[y][x]=1;
12     }
13     for(int k=1;k<=n;k++){
14         for(int i=1;i<=n;i++){
15             for(int j=1;j<=n;j++){
16                 d[i][j]|=d[i][k]&d[k][j];//状态传递 
17             }
18         }
19     }
20 }

这样我们就可以很好的解决传递闭包的问题。


下面是一道传递闭包的十分简单的例题:

P2419 [USACO08JAN]牛大赛Cow Contest

题目描述

N (1 ≤ N ≤ 100) cows, conveniently numbered 1..N, are participating in a programming contest. As we all know, some cows code better than others. Each cow has a certain constant skill rating that is unique among the competitors.

The contest is conducted in several head-to-head rounds, each between two cows. If cow A has a greater skill level than cow B (1 ≤ A ≤ N; 1 ≤ B ≤ N; A ≠ B), then cow A will always beat cow B.

Farmer John is trying to rank the cows by skill level. Given a list the results of M (1 ≤ M ≤ 4,500) two-cow rounds, determine the number of cows whose ranks can be precisely determined from the results. It is guaranteed that the results of the rounds will not be contradictory.

FJ的N(1 <= N <= 100)头奶牛们最近参加了场程序设计竞赛:)。在赛场上,奶牛们按1..N依次编号。每头奶牛的编程能力不尽相同,并且没有哪两头奶牛的水平不相上下,也就是说,奶牛们的编程能力有明确的排名。 整个比赛被分成了若干轮,每一轮是两头指定编号的奶牛的对决。如果编号为A的奶牛的编程能力强于编号为B的奶牛(1 <= A <= N; 1 <= B <= N; A != B) ,那么她们的对决中,编号为A的奶牛总是能胜出。 FJ想知道奶牛们编程能力的具体排名,于是他找来了奶牛们所有 M(1 <= M <= 4,500)轮比赛的结果,希望你能根据这些信息,推断出尽可能多的奶牛的编程能力排名。比赛结果保证不会自相矛盾。

输入输出格式

输入格式:

第1行: 2个用空格隔开的整数:N 和 M

第2..M+1行: 每行为2个用空格隔开的整数A、B,描述了参加某一轮比赛的奶 牛的编号,以及结果(编号为A,即为每行的第一个数的奶牛为 胜者)

输出格式:

第1行: 输出1个整数,表示排名可以确定的奶牛的数目

输入输出样例

输入样例#1: 
5 5
4 3
4 2
3 2
1 2
2 5
输出样例#1:
2

说明

输出说明:

编号为2的奶牛输给了编号为1、3、4的奶牛,也就是说她的水平比这3头奶

牛都差。而编号为5的奶牛又输在了她的手下,也就是说,她的水平比编号为5的

奶牛强一些。于是,编号为2的奶牛的排名必然为第4,编号为5的奶牛的水平必

然最差。其他3头奶牛的排名仍无法确定。

题目我们看完后,不难发现,这是一道非常简单的传递闭包问题,从题目中可以得知,如果A能打败B,B能打败C,则A就能打败C,所以我们就可以采用floyd算法进行解决

AC代码:

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<queue>
 5 #include<cmath>
 6 #include<algorithm>
 7 using namespace std;
 8 int c[105][105];
 9 int n,m;
10 int main(){
11     scanf("%d%d",&n,&m);
12     for(int i=1;i<=m;i++){
13         int x,y;
14         scanf("%d%d",&x,&y);
15         c[x][y]=1;
16     }
17     for(int k=1;k<=n;k++){
18         for(int i=1;i<=n;i++){
19             for(int j=1;j<=n;j++){
20                 if(i!=j&&c[i][k]&&c[k][j]){
21                     c[i][j]=1;//状态转移
22                 }
23             }
24         }
25     }
26     int sum=0;
27     for(int i=1;i<=n;i++){
28         int r=0;
29         for(int j=1;j<=n;j++){
30             if(c[i][j]||c[j][i]){
31                 r++;//统计个数
32             }
33         }
34         if(r==n-1){
35             sum++;
36         }
37     }
38     printf("%d",sum);
39 } 

好的那么floyd的拓展我们也已经讲完了。

基础算法部分我们已经结束。


下面就是一些例题ヾ(o◕∀◕)ノヾ,不,是没有了!

原文地址:https://www.cnblogs.com/xishirujin/p/10414898.html