最短路
写在前面:我是一只蒟蒻。
今天,我们来学习一下图论中很基础的一个问题“最短路问题”,当然,它也可以很难。。。。。。
在正式的讲解最短路之前,我们来聊一聊存图方式。
对于一张有向图,我们一般由邻接矩阵和邻接表两种方式进行存储。对于无向图,可以把无向边看做两条方向相反的有向边,从而采用与有向图相同的存储方式,所以一下讲解最短路时,就以有向图为基准来进行讲解。
邻接矩阵,这是一个十分简单的存图方式,使用二维数组进行存储,但是空间复杂度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\n",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\n",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个整数,表示排名可以确定的奶牛的数目
输入输出样例
说明
输出说明:
编号为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◕∀◕)ノヾ,不,是没有了!