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)
稀疏图的邻接表
上面的程序的时间复杂度为——循环体一共执行了n次,而在每次循环中,“求最小d值”和“更新其他的d值”均是的。下面把它优化到.
为什么说是“优化到”呢?在最坏情况下,m和是同阶的 ,mlogn岂不是要比大?这话没错,但在很多情况下,图中的边并没有那么多,mlogn比小得多。我们把m小于的图称为稀疏图(Sparse Graph),而m相对较大的图称为稠密图(Dense Graph)。
在学习稀疏图时,首先要掌握图的新表示法。既然m远小于,那么邻接矩阵中会有大量的表示“此边不存在”的元素,不仅浪费了空间,而且也减低了时间效率——例如为了遍历所有边,必须检查邻接矩阵中的所有元素。尽管只有那些实际存在的边参与了核心运算,但我们浪费了大量的时间去判断到底哪些边存在。
当然可以使用前面介绍的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算法,不然看出它的时间复杂度为.在实践中,我们常用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算法在最坏情况下需要时间,不过在实践中,往往只需要很短的时间就能求出最短路。
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)