最短路问题(含邻接表)

版权声明:个人笔记,仅供复习 https://blog.csdn.net/weixin_42373330/article/details/82934202

DAG,英文全称是 Directed Acyclic Graph(有向无环图)

若有环的话,DAG就不妙了,下面介绍Dijkstra算法

首先考虑所有边权均为正的情况。在这种情况下,最短路是一定存在的,但最长路却不一定存在——如果图中有个环,每走一圈路就会变长(别忘了,所有边权为正)。为了简单起见,先考虑最短路径。

给出单源最短路(Single-source Shortest Paths,SSSP),即从单个源点出发,到所有结点的最短路径。该算法同时适用于有向图和无向图。

清除所有点的标号
设d[0]=0,其他d[i]=INF
循环n次
{
    在所有未标记的结点中,选出d值最小的结点x
    给结点x标记
    对于从x出发的所有边(x,y),更新d[y]=min(d[y],d[x]+w(x,y))
}
memset(v,0,sizeof(v));
for(int i=0;i<kn;i++) d[i]=(i==0?0:INF);
for(int i=0;i<n;i++)
{
    int x,m=INF;
    for(int y=0;y<n;y++) if(!v[y] && d[y]<=m) m=d[x=y];
    v[x]=1;
    for(int y=0;y<n;y++) d[y]=min(d[y],d[x]+w[x][y]);
}

打印:从终点出发,不断顺着d[i]+w[i][j]==d[j]的边(i,j)从节点j“退回”到节点i,直到起点。另外可以用空间换时间,在更新d数组时维护“父亲指针”。具体来说,需要把d[y]=min(d[y],d[x]+w(x,y))改成:

if(d[y]>d[x]+w[x][y])
{
    d[y]=d[x]+w[x][y];
    fa[y]=x;
}

把它称为边(x,y)上的松弛操作(relaxation)


稀疏图的邻接表

上面的程序的时间复杂度为\small O(n^{2})——循环体一共执行了n次,而在每次循环中,“求最小d值”和“更新其他的d值”均是\small O(n)的。下面把它优化到\small O(mlogn).

为什么说是“优化到”呢?在最坏情况下,m和\small n^{2}是同阶的 ,mlogn岂不是要比\small n^{2}大?这话没错,但在很多情况下,图中的边并没有那么多,mlogn比\small n^{2}小得多。我们把m小于\small n^{2}的图称为稀疏图(Sparse Graph),而m相对较大的图称为稠密图(Dense Graph)。

在学习稀疏图时,首先要掌握图的新表示法。既然m远小于\small n^{2},那么邻接矩阵中会有大量的表示“此边不存在”的元素,不仅浪费了空间,而且也减低了时间效率——例如为了遍历所有边,必须检查邻接矩阵中的所有元素。尽管只有那些实际存在的边参与了核心运算,但我们浪费了大量的时间去判断到底哪些边存在。

当然可以使用前面介绍的vector<int> G[maxn]存储稀疏图,但这里想介绍的是另一种流行的表示法——邻接表(Adjacency List)。

在这种表示法中,每个结点i都有一个链表,里面保存着从i出发的所有边。对于无向图来说,每条边会在邻接表中出现两次。和前面一样,这里继续用数组实现链表:首先给每条边编号,然后用first[u]保存结点u的第一条边的编号,next[e]表示编号为e的边的“下一条边”的编号。下面的函数读入有向图的边列表,并建立邻接表:

int n, m;
int first[maxn];
int u[maxn],v[maxn],w[maxn],next[maxn];
void read_graph()
{
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++) first[i]=-1;//初始化表头
    for(int e=0;e<m;e++)
    {
        scanf("%d%d%d",&u[e],&v[e],&w[e]);
        next[e]=first[u[e]]; //**插入链表
        first[u[e]]=e;  //**
    }
}

上述代码的巧妙之处是插入到链表的首部而非尾部,这样就避免了对链表的遍历,不过注意的是,同一个起点的各条边在邻接表中的顺序和读入顺序正好相反。读者如果还记得哈希表,应该会发现这里的链表和哈希表中的链表实现很相似。

以n=4个点,m=6条边为例,输入

u v w e
1 2 3 0
3 1 4 1
1 4 6 2
2 4 3 3
2 3 2 4
3 4 5 5

可以看出first[节点]=该结点的第几条边,next[节点的第几条边]=该边的前一条边(如果是第一条,就等于-1)


使用优先队列的Dijkstra算法(找出最小的d值)

有了邻接表,“遍历从x出发的所有边(x,y),更新d[y]”,就可以写成“for(int e=first[x];e!=-1;e=next[e]) d[v[e]]<?=d[x]+w[e];”.尽管在最坏的情况下,这个循环仍然会循环n-1次,但从整体上看,每条边恰好被检查过一次(想一想,为什么),因此“d[v[e]]<?=d[x]+w[e]”这条语句执行的次数恰好是m。这样,只需集中精力优化“找出未标记节点中的最小d值”即可。

幸运的是,c++的STL提供了“优先队列”这一容器,可以帮助我们快速完成这一操作。优先队列和普通的FIFO队列
都定义在<queue>中,有push()和pop()过程,分别表示“往队列里加入新元素”和“从队列中删除队首元素”。唯一有区别的是,在优先队列中,元素并不是按照进入队列的先后顺序排列,而是按照优先级的高低顺序排列。
换句话说,pop()删除的是优先级最高的元素,而不一定是最先进入队列的元素。正因如此获取队首元素的方法
不再是front(),而是top(). ///---///
定义优先队列最简单的方法是priority_queue<类型名> q,它利用元素自身的“小于”操作符来定义优先级。例如
在priority_queue<int> q这样的优先队列中,先出队的总是最大的整数。使用自定义比较的方法和set类似:
struct cmp
{
    bool operator() (const int a, const int  b) //a 的优先级比b小时返回true
    {
        return a%10>b%10;
    }
};

priority_queue<int,vector<int>, cmp> q; //“个位数大的优先级反而小”的整数的优先队列key
===================================================================================
在Dijksta算法中,d[i]小的值应该先出队,因此需要使用自定义比较器
//在STL中,可以用greater<int>表示“大于”运算符,因此可以用
priority_queue<int,vector<int>,greater<int> >q(注意最后两个大于号之间一定要有空格,不然会被误认为移位运算符“>>”)来声明一个小整数先出队的优先队列。(从小到大)//这里很容易搞错的,我就经常记错
而less就表示“小于”运算符,是从大到小的,与默认的priority_queue<int> q是一样的。
----
关于这个大于小于的理解,我之钱看到一篇博客,是这样理解的,就是“小于”号表示与默认的相同,“大于”表示与默认的不同,优先队列默认的是从大到小,所以less是从大到小,greater是从小到大

然而,这个方法是有问题的:除了需要最小的d值之外,还要找到这个最小值对应的结点编号。解决方法是:把d值和编号“捆绑”成一个整体放到优先队列中,使得取出最小d值的同时也会取出对应的结点编号。

STL中的pair便是专门把两个类型捆绑倒一起的。为了方便起见,我们用typedf pair<int,int> pii自定义一个pii类型,则priority_queue<pii,vector<pii>,greater<pii> >q就定义了一个由二元数组构成的优先队列。pair定义了它自己的排序规则——先比较第一维,相等时才比较第二维,因此需要按的(d[i],i)而不是(i,d[i])的方式组合。代码如下:

bool done[maxn];
for(int i=0;i<n;i++) d[i]=(i==0?0:INF);
memset(done,0, sizeof(done)); //初始化计算标志,所有结点都没有算出来
q.push(make_pair(d[0],0)); //起点进入优先队列
while(!q.empty())
{
    pii u=1.top();q.pop();
    int x=u.second;
    if(done[x]) continue; //已经算过,忽略
    done[x]=true;
    for(int e=first[x];e!=-1;e=next[e]) if(d[v[e]]>d[x]+w[e])
    {
        d[v[e]]=d[x]+w[e]; //c松弛成功,更新d[v[e]]
        q.push(make_pair(d[v[e]],v[e])); //加入优先队列
    }
}

在松弛成功后,需要修改结点v[e]的优先级,但STL中的优先级不提供“修改优先级”的操作。因此,只能将新的d[i]重新插入优先队列。这样做并不会影响结果的正确性,因为d值小的结点会自然出列。为了防止结点的重复扩展,如果发现新取出来的结点曾经被取出来过(done[x]),应该直接把它扔掉。顺便说一句:避免重复的另一个方法是把if(done[x])改成if(u.first!=d[x]),可以省掉一个done数组。

即使是稠密图,使用priority_queue实现的Dijkstra算法也常常比基于邻接矩阵的的Dijkstra算法的运算速度快。理由很简单:执行push操作的前提是d[v[e]]>d[x]+w[e],如果这个式子不成立,push操作会少很多。


Bellman-Ford算法

当负权存在时,连最短路都不一定存在了。尽管如此,还是有办法在最短路存在的情况下把它求出来。在介绍算法前,确认个事实:如果最短路存在,一定存在一个不含环的最短路。

理由如下:在边权可正可负的图中,环有零环,正环和负环3种。如果包含零环或正环,去掉以后路径不会变长;如果包含负环,则意味着最短路不存在。

既然不含环,最短路最多只经过(起点不算)n-1个点,可以通过n-1“轮”松弛操作得到,像这样(起点仍然是0):

for(int i=0;i<n;i++) d[i]=INF;
d[0]=0;
for(int k=0;k<n-1;k++) //迭代k次
    for(int i=0;i<m;i++)//检查每条边
    {
        int x=u[i],y=v[i];
        if(d[x]<INF) d[y]=min(d[y],d[x]+w[i]) //松弛
    }

上述算法称为Bell-Ford算法,不然看出它的时间复杂度为O(nm).在实践中,我们常用FIFO队列来代替上面的循环检查,像这样:

queue<int> q;
bool inq[maxn];
for(int i=0;i<n;i++) d[i]=(i==0?0:INF);
memset(inq,0,sizeof(inq));//在队列中的标志
q.push(0);
while(!q.empty())
{
    int x=q.front();q.pop();
    inq[x]=false; //清除“在队列中”标志
    for(int e=first[x];e!=-1;e=next[e]) if(d[v[e]]>d[x]+w[e])
    {
        d[v[e]]=d[x]+w[e];
        if(!inq[v[e]]) //如果已在队列中,就不要重复加了
        {
            inq[v[e]]=true;
            q.push(v[e]);
        }
    }
}

可以证明,采用FIFO队列的Bell-Ford算法在最坏情况下需要\small O(nm)时间,不过在实践中,往往只需要很短的时间就能求出最短路。


Floyd算法

如果你需要求出每两个点之间的最短路,不必调用n次Dijkstra(边权均为正)或者Bell-ford(有负权)。有一个更简单的方法可以满足你的需求——Floyd-Warshall算法(请记住下面的代码!):

for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
        d[i][j]<?=d[i][k]+d[k][j];

在调用它之前只需做一些简单的初始化:d[i][i]=0,其他d值为“正无穷”INF。注意这里有一个潜在的问题:如果INF定义太大(如2e9),加法d[i][k]+d[k][j]可能会溢出!但如果INF太小,可能会是的长度为INF的边真的变成最短路的一部分。为了谨慎起见,最后估计一下实际最短路长度的上限,并把INF设置成“只比它大一点点”的值。

如果坚持认为不应该允许INF和其他值相加,更不应该得到一个INF的数,请把上述代码改成:

for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
        if(d[i][j]<INF&& d[k][j]<INF) d[i][j]<?=d[i][k]+d[k][j];

在有向图中,有时不必关系路径的长度,而只关心每两点间是否有通路,则可以用1和0分别表示“连通”和“不连通”。这样,除了预处理需做少许调整外,主算法中只需把“d[i][j]<?=d[i][k]+d[k][j]”改成“d[i][j]=d[i][j]||(d[i][k]&&d[k][j])”.这样的结果称为有向图的传递闭包(Transitive Closure)

猜你喜欢

转载自blog.csdn.net/weixin_42373330/article/details/82934202