图论概述和SPFA

图论概述和SPFA

2019-12-28

Powered by Gauss

1.图论——最短路

图论是信息学学习过程中不可或缺的一个部分。图论的应用是非常广泛的,在现实生活中大家处处都能遇到,例如电子地图,机票查询等。

现在的算法竞赛考试的范围是无边无际,但主要的考点也就是图论,DP,数论,字符串等等。图论是一大考点,例如:

题目 来源
最优贸易          NOIP2009提高组第三题
信息传递 NOIP2015提高组第二题
运输计划 NOIP2015提高组第六题
寻找道路 NOIP2014提高组第五题

 

 

 

 

当然,关于图论的算法远远不止这些,上面只列举了一些著名的题目。

【图论的历史】

 

图论起源于一个非常经典的问题——柯尼斯堡(Konigsberg)问题。

 

1738年,瑞典数学家欧拉( Leornhard Euler)解决了柯尼斯堡问题。由此图论诞生。欧拉也成为图论的创始人。

 

1859年,英国数学家汉密尔顿发明了一种游戏:用一个规则的实心十二面体,它的20个顶点标出世界著名的20个城市,要求游戏者找一条沿着各边通过每个顶点刚好一次的闭回路,即“绕行世界”。
 
用图论的语言来说,游戏的目的是在十二面体的图中找出一个生成圈。这个生成圈后来被称为汉密尔顿回路。这个问题后来就叫做汉密尔顿问题。由于运筹学、计算机科学和编码理论中的很多问题都可以化为汉密尔顿问题,从而引起广泛的注意和研究。
 
【图论的分支】
 
如上图所示,图论主要分为最小生成树,最短路和最大流。其中最重要的莫过于最短路。
跟最短路相关的算法有很多,下面的图展示了几种主要算法。
【最短路算法】
 
如上图所示,最短路算法主要分为Floyd-Warshall、Bellman-Ford、SPFA、Dijsktra。
这几种算法各有千秋,下面的表格展示了它们的对比:
  Floyd Dijsktra Bellman-Ford SPFA
空间复杂度 O(N2)            

O(M)

O(M) O(M)
时间复杂度 O(N3) O((M+N)logN) O(NM) O(NM)
适用情况 稠密图 稠密图 稀疏图 稀疏图
负权 Y N Y Y
有负权边 Y N Y Y
判断负权回路 N N Y Y
今天我们主要来研究SPFA算法。
 
 
-------------------------------------------------------------------------- 不怎么华丽的分割线---------------------------------------------------------------------
 
首先,我们用一道题来引入SPFA的思想。
题目传送门:HDU-2544
大家都看过这道题了。
这是一道模板题,用上述的四个算法都能过(逃~)
 
名称 时间
Floyd-Warshall 899MS
Dijsktra 256MS
Bellman-ford 159MS
SPFA 43MS
速度上的差距无法弥补,所以还是要学SPFA。
 
【SPFA】简介
SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下复杂度和朴素 Bellman-Ford 相同,为 O(VE)。(源自百度百科)
SPFA的全称是Shortest Path Faster Algorithm,即求最短路更快的方法,源自Bellman-Ford。
【SPFA】算法思想
SPFA与贝尔曼福特算法最大的区别就是存储方式。
下面给出Bellman的代码和SPFA的代码,以更好的展示区别
void spfa(int s,int n)
{
    int r=0,l=0;
    memset(dl,0,sizeof(dl));
    memset(b,1,sizeof(b));
    memset(dis,0x7f7f7f7f,sizeof(dis));
    dis[s]=0;
    dl[r++]=s;
    while(l<r)
    {
        int x=dl[l];
        b[x]=1;
        for(int i=1;i<=n;i++)
        {
            if(a[x][i]!=0x7f7f7f7f)
            {
                if(dis[i]>dis[x]+a[x][i])
            {
                dis[i]=dis[x]+a[x][i];
                if(b[i])
                {
                    dl[r++]=i;
                    b[i]=0;
                }
            }
            }
        }
        l++;
    }
}
void bellman()
{
    int s=1;
    int d[NUM];
    for(int i=1;i<=n;i++) d[i]=INF;
    d[s]=0;
    for(int k=1;k<=n;k++)
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(d[j]>d[i]+graph[i][j]) d[j]=d[i]+graph[i][j];        
            }
        } 
    }
    printf("%d",d[n]);
}

由此可以看出,bellman和SPFA的代码最大的区别就是“松弛”方式。

松弛的步骤如下所示:

        

from to to to to
1 2 3 4 5
INF 5 2 5 40


∵ 2+3+1<40

from to to to to
1 2 3 4 5
INF 5 2 5 6

仔细观察两张表格的区别,得出松弛操作的表达式:

if(d[j]>d[i]+graph[i][j]) d[j]=d[i]+graph[i][j];

其中d[ j ]表示从1号点到 j 号点距离。

【SPFA】的计算过程

SPFA的思想很像BFS:

使用队列来实现:

1.起点s入队,计算s所有邻居到s得距离。把s出队,状态有更新的邻居入队,没更新的出队;

2.现在的队头就是s的一个邻居p,弹出p,重复步骤1、2;

PS:这时某个点可能因为多次修改而对此入队,这时将这个点u入队就行了。

下面给出SPFA的HDU2544的完整代码:

#include<bits/stdc++.h>
using namespace std;
const int INF=1e6;
const int NUM=105;
struct edge
{
    int from,to,w;
    edge(int a,int b,int c)
    {
        from=a;
        to=b;
        w=c;
    }
};
vector<edge>e[NUM];
int n,m;
int pre[NUM];
int spfa(int s)
{
    int dis[NUM];
    bool inq[NUM];
    int Neg[NUM];
    memset(Neg,0,sizeof(Neg));
    Neg[s]=1;
    for(int i=1;i<=n;i++) 
    {
        dis[i]=INF;
        inq[i]=false;
    }
    dis[s]=0;
    queue<int>Q;
    Q.push(s);
    inq[s]=true;
    while(!Q.empty())
    {
        int u=Q.front();
        Q.pop();
        inq[u]=false;
        for(int i=0;i<e[u].size();i++)
        {
            int v=e[u][i].to,w=e[u][i].w;
            if(dis[u]+w<dis[v])
            {
                dis[v]=dis[u]+w;//松弛 
                pre[v]=u;//记录路径 
                if(!inq[v])//更新队列 
                {
                    inq[v]=true;//更新队列的元素
                    Q.push(v);//加入队列
                    Neg[v]++;//判断入队次数
                    if(Neg[v]>=n) return 1;//判断负环 
                }
            }
        }
    }
    printf("%d\n",dis[n]);
    return 0;
}
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        if(n==0 && m==0) return 0;
        for(int i=1;i<=n;i++) e[i].clear();
        while(m--)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            e[a].push_back(edge(a,b,c));
            e[b].push_back(edge(b,a,c));
        }
        spfa(1);
    }
    return 0;
} 

我们来详细解读一下上面的代码;

首先,因为题目的要求是无向图,所以要双向存储:

e[a].push_back(edge(a,b,c));
e[b].push_back(edge(b,a,c));

然后,我们从一号点开始进行SPFA计算

spfa(1);

紧接着,

int dis[NUM];
bool inq[NUM];
int Neg[NUM];
memset(Neg,0,sizeof(Neg));
Neg[s]=1;

其中dis用来存储距离,inq用来存储这个点是否在队列里,Neg用来存储负环。

Neg[s]=1;
for(int i=1;i<=n;i++) 
{
    dis[i]=INF;
    inq[i]=false;
}

这一段代码是初始化,将两个数组进行初始化,非常重要。

dis[s]=0;
queue<int>Q;
Q.push(s);
inq[s]=true;

这里就是SPFA的精华部分,我们使用队列存储。

while(!Q.empty())

只要队列不为空,就说明有点需要松弛

int u=Q.front();
Q.pop();
inq[u]=false;
for(int i=0;i<e[u].size();i++)
{
    int v=e[u][i].to,w=e[u][i].w;
    if(dis[u]+w<dis[v])
    {
        dis[v]=dis[u]+w;//松弛 
        pre[v]=u;//记录路径 
        if(!inq[v])//更新队列 
        {
            inq[v]=true;//更新队列的元素
            Q.push(v);//加入队列
            Neg[v]++;//判断入队次数
            if(Neg[v]>=n) return 1;//判断负环 
        }
    }
}

这里就是图论算法求最短路的精华,松弛;

今天就讲到这里,再见!!!

 

猜你喜欢

转载自www.cnblogs.com/Warframe-Gauss/p/12111611.html
今日推荐