zoukankan      html  css  js  c++  java
  • 最短路

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

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

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

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

    邻接矩阵,这是一个十分简单的存图方式,使用二维数组进行存储,但是空间复杂度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◕∀◕)ノヾ,不,是没有了!

  • 相关阅读:
    ztree学习---将默认勾选的展开
    CentOS之RPM
    CentOS之文档的压缩与打包
    CentOS之Vim
    CentOS用户和用户组管理
    Linux CentOS更改文件的权限
    CentOS的文件属性:命令 ls -l
    CentOS命令
    Java解析excel
    easyUI的combotree的树的懒加载。
  • 原文地址:https://www.cnblogs.com/xishirujin/p/10414898.html
Copyright © 2011-2022 走看看