图算法这一专题中,最大的感觉,还是算法模板的使用,像并查集、最短路径中的prim算法、最短路径中的dijkstra算法等等,这些问题的求解方法应该说来,都是比较固定的。
算法模板固定,在问题的求解过程中,则是需要合理的灵活变化,符合题意,多去练习十分重要。
简单总结一下图的一些算法。并查集主要操作是,合并两个集合,查找某元素属于哪个集合。很多问题在使用并查集后,会变得很简单。
最小生成树的求解,prim算法:任取一个顶点加入生成树;
在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树。
重复上一步骤,直到所有的顶点都进入了生成树为止。
Kruskal算法:
将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。
最终得到的结果就是最小生成树。并查集。
最短路径问题,Dijkstra算法:设置一个集合S存放已经找到最短路径的顶点,S的初始状态只包含源点v,对vi∈V-S,假设从源点v到vi的有向边为最短路径。以后每求得一条最短路径v, …, vk,就将vk加入集合S中,并将路径v, …, vk , vi与原来的假设相比较,取路径长度较小者为最短路径。重复上述过程,直到集合V中全部顶点加入到集合S中。
这里还有一个比较重要的技术,松弛技术。
bellman-ford,spfa,dijkstra算法都使用了松弛技术,对每个顶点v∈V,都设置一个属性d[v],用来描述从源点s到v的最短路径上权值的上界。
伪代码:
初始化:
Init(G,s)
{
for each vertex v∈V[G]
do d[v]=∞;
d[s]=0;
}
松弛:
Relax(u,v,w)
if(d[v]>d[u]+w(u,v))
d[v]=d[u]+w(u,v)
Dijkstra的缺陷在于它不能处理负权回路:Dijkstra对于标记过的点就不再进行更新了,所以即使有负权导致最短距离的改变也不会重新计算已经计算过的结果,Bellman-Ford算法很好的解决了这个问题。
Bellman-Ford算法思想构造一个最短路径长度数组序列dist 1[u], dist 2 [u], …, dist n-1 [u]。其中:
dist 1 [u]为从源点v到终点u的只经过一条边的最短路径长度,并有dist 1 [u] =Edge[v][u];
dist 2 [u]为从源点v最多经过两条边到达终点u的最短路径长度;
dist 3 [u]为从源点v出发最多经过不构成负权值回路的三条边到达终点u的最短路径长度;
……
dist n-1 [u]为从源点v出发最多经过不构成负权值回路的n-1条边到达终点u的最短路径长度;
算法的最终目的是计算出dist n-1 [u],为源点v到顶点u的最短路径长度。
distk[u]的计算
采用递推方式计算 dist k [u]。
设已经求出 dist k-1 [u] , u =0, 1, …, n-1,此即从源点v最多经过不构成负权值回路的k-1条边到达终点u的最短路径的长度。
从图的邻接矩阵可以找到各个顶点j到达顶点u的距离Edge[j][u],计算min{dist k-1 [j] + Edge[j][u] } ,可得从源点v绕过各个顶点,最多经过不构成负权值回路的k条边到达终点u的最短路径的长度。
比较dist k-1 [u]和min{ dist k-1 [j] + Edge[j][u] } ,取较小者作为distk [u]的值。
递推公式(求顶点u到源点v的最短路径):
dist 1 [u] = Edge[v][u]
dist k [u] = min{ dist k-1[u], min{ dist k-1 [j] + Edge[j][u] } }, j=0,1,…,n-1,j≠u
spfa算法(Shortest path faster algorithm)
SPFA 其实就是Bellman-Ford的一种队列实现,减少了冗余,即松驰的边至少不会以一个d为∞的点为起点。
算法实现:
1.队列Q={s}
2.取出队头u,枚举所有的u的临边 .若d(v)>d(u)+w(u,v)则改进,pre(v)=u,由于d(v)减少了,v可能在以后改进其他的点,所以若v不在Q中,则将v入队。
3.一直迭代2,直到队列Q为空(正常结束),或有的点的入队次数>=n(含有负圈)。
一般用于找负圈(效率高于Bellman-Ford),稀疏图的最短路
每对顶点的最短路径floyd-washall算法的基本思想:
递推产生矩阵序列f(0), f(1), …, f(n)。
其中f(0)[i,j]=map[i,j]
f(k)[i,j]的值表示从vi到vj,中间结点编号不超过k的最短路径长度。
f(n)[i,j]的值就是从vi到vj的最短路径长度。
递推过程----动态规划
如果f(k)[i,j]中间节点编号经过k,则
f(k)[i,j]=f(k-1)[i,k]+f(k-1)[k,j]。如果f(k)[i,j]中间节点不经过k,则f(k)[i,j]=f(k-1)[i,j]。
如果f(k-1)[i,j]<=f(k-1)[i,k]+f(k-1)[k,j],
那么就令f(k)[i,j]= f(k-1)[i,j]。
如果f(k-1)[i,j]>f(k-1)[i,k]+f(k-1)[k,j],
那么就令f(k)[i,j]=f(k-1)[i,k]+f(k-1)[k,j]。
实现:
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);