最小生成树
连通图的生成树是包含图中全部顶点的极小连通子图。
注意区分极大连通子图和极小连通子图:极大连通子图是无向图的连通分量,它包含无向图的所有的边;极小连通子图是连通无向图的生成树,既要保持图连通,又要使得边数最小
最短路径
求解最短路径的算法通常都依赖于一种性质,也就是两点之间的最短路径也包含了路径上其他顶点的最短路径。这种最优子结构性质是动态规划和贪心算法是否适用的一个标记。
带权有向图G的最短路径问题,一般可分为两类:一是单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可通过经典的Dijkstra算法求解,此算法也是基于贪心算法的策略;二是求每一对顶点间的最短路径,可通过Floyd-Warshall算法来求解,此算法是基于动态规划的思想。
1.Dijkstra算法求单元最短路径问题
求带权有向图中某个源点到其余各顶点的最短路径,最常用的是Dijkstra算法。该算法设置一个集合S,记录已求得的最短路径的顶点,初始时把源点v0放入S中。此外,在构造过程中还设置了两个辅助数组:
dist[]:记录了从源点v0到其他各顶点当前的最短路径长度,dist[i]初值为arcs[0][i].
path[]:path[i]表示从源点到顶点i之间的最短路径的前驱节点,在算法结束时,可根据其值追溯得到源点v0到顶点vi的最短路径。
假设从顶点0出发,即v0=0,集合S最初只包含顶点0,邻接矩阵arcs表示带权有向图,arcs[i][j]表示有向边< i,j>的权值,若不存在有向边< i,j>,则arcs[i][j]为∞。Dijkstra算法步骤如下:
1)初始化:集合S初始化为{0},dist[]的初始值dist[i]=arcs[0][i], i=1, 2, …, n-1。
2)从顶点集合V-S中选出vj,满足dist[j]=Min{dist[i] | vi∈V-S},vj就是当前求得的一条从v0出发的最短路径的终点,令S=S∪{j}。
3)修改从v0出发到集合V-S上任一顶点vk可达的最短路径长度;如果dist[j]+arcs[j][k] < dist[k],则令dist[k]=dist[j]+arcs[j][k].
4)重复2)~3)操作共n-1次,直到所有的定点都包含在S中。
例如,对下图应用Dijkstra算法求其最短路径的步骤
显然,Dijkstra算法是基于贪心策略的。若使用邻接矩阵表示,它的时间复杂度为O(|V²|)。若使用带权的邻接表表示,虽然修改dist[]的时间可以减少,但由于在 dist[]中选择最小分量的时间不变,其时间复杂度仍未O(|V²|)。
人们可能只希望找到从原点到某一特定顶点的最短路径,但是,这个问题和求解源点到其他所有顶点的最短路径一样复杂,其实际复杂度也为O(|V|²)。
值得注意的是,如果边上带有负权值,Dijkstra算法并不适用。
vector<int> path(n);
vector<bool> vis(n,false);//判断节点是否被访问过
vector<int> dist(n, INT_MAX);//存放最短路径的值
void djstra(int dst){
for(int i =0 ; i < n; i++){
if(i != dst)
dist[i] = matrix[src][i];//matrix存放两点间的距离
}
dist[dst] = 0;
path.push_back(dst);
vis[dst] = true;
for(int i = 1; i < n; i++){
int minset = INT_MAX;
int tmp = dst;
for(int j = 0; j < n; j++){
if(dist[j] < minset && (!vis[j])){
tmp = j;
minset = dist[tmp];
}
}
vis[tmp] = true;
for(int j = 0; j < n; j++){
if( (!vis[j]) && matrix[tmp][j] < INT_MAX){
if((dist[tmp] + matrix[tmp][j] < dist[j])){
dist[j] = dist[tmp] + matrix[tmp][j];
path.push_back(tmp);//存放得出最短路径的前一个节点,用于路径还原
}
}
}
}
}
2.Floyd算法求各顶点之间最短路径问题
求所有顶点之间的最短路径问题描述如下:已知一个各边权值均大于0的带权有向图,对每一对顶点vi ≠vj,要求求出vi与vj之间的最短路径长度。
Floyd算法的基本思想:递推产生一个n阶方阵序列A^(-1), A^(0),…,A^(n-1),其中A^(k)[i][j]表示从顶点vi到顶点vj的路径长度,k表示绕行第k个顶点的运算步骤。初始时,对于任意两个顶点vi和vj,若他们之间存在边,则以次边上的权值作为它们之间的最短路径长度;若它们之间不存在有向边,则以∞作为他们之间的最短路径长度。以后逐步尝试在原路径中加入顶点k(k = 0,1,…,n-1)作为中间顶点。如果增加中间顶点后,将得到的路径比原来的路径长度减少了,则以新路径代替原路径。算法描述如下:
定义一个n阶方阵序列:A^(-1), A(0),…,A^(n-1),其中:
A^(-1)[i][j] = arcs[i][j]
A^(k)[i][j] = Min{A^(k-1)[i][j], A^(k-1)[i][k] + A^(k-1)[k][j]}, k =0,1,…,n-1
其中,A^(0)[i][j]是从顶点vi到vj,中间顶点是v0的最短路径的长度,A^(k)[i][j]是从顶点vi到vj,中间顶点的序号不大于k的最短路径的长度。Floyd算法是一个迭代的过程,每迭代一次,在从vi到vj的最短路径上就多考虑了一个顶点:经过n次迭代后所得到的A^(n-1)[i][j]就是vi到vj的最短路径长度,即方针A^(n-1)中保存了任意一对顶点之间的最短路径长度。
Floyd算法执行过程
Floyd算法的时间复杂度为(|V|³)。不过由于其代码很紧凑,而且不包含其他负债的数据结构,因此隐含的常数系数是很小的,即使对于中等规模的输入来说,它仍然是相当有效的。
Floyd算法允许图中有带负权值的边,但不允许有包含带负权值的边组成的回路。Floyd算法同样也适用于带权无向图,因为带权无向图可以视为有往返二重边的有向图,只要在顶点vi和vj之间存在无向边(vi,vj),就可以视为在这两个顶点之间存在权值相同的两条有向边< vi,vj>和< vj, vi>.
也可以用单源最短路径算法来解决每对顶点之间最短路径问题。每一次运行时,轮流将一个顶点作为源点,并且若所有边权值均为非负时,可以采用Dijkstra算法。