4.图
4.1 图的基本术语
用 表示图中顶点数目,用 表示边的数目。
(1)子图: 假设有两个图 和 ,如果 且 ,则称 为 的子图
(2)无向完全图和有向完全图: 对于无向图,若具有 条边,则称为无向完全图;对于有向图,若具有 条弧,则称为有向完全图
(3)稀疏图和稠密图: 有很少条边或弧(如 )的图称为稀疏图,反之称为稠密图
(4)权和网: 每条边可以标上具有某种含义的数值,该数值称为该边上的权;这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网
(5)邻接点: 对于无向图 ,如果图的边 ,则称顶点 和 互为邻接点,即 和 相邻接
(6)度、入度和出度: 顶点 的度是指和 相关联的边的数目,记为 ;入度是以顶点 为头的弧的数目,记为 ;出度是以顶点 为尾的弧的数目,记为 。如果顶点 的度记为 ,那么一个有 个顶点, 条边的图,满足如下关系
(7)路径和路径长度: 在无向图 中,从顶点 到顶点 的路径是一个顶点序列 ,其中 , 。路径长度是一条路径上经过的边或弧的数目
(8)回路或环: 第一个顶点和最后一个顶点相同的路径称为回路或环
(9)简单路径、简单回路或简单环: 序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环
(10)连通、连通图和连通分量: 在无向图 中,如果从顶点 到顶点 有路径,则称 和 是连通的。如果对于图中任意两个顶点 , 和 都是连通的,则称 是连通图。连通分量指的是无向图中的极大连通子图
(11)强连通图和强连通分量: 在有向图 中,如果对于每一对 ,,从 到 和从 到 都存在路径,则称 是强连通图。在有向图中的极大连通子图称作有向图的强连通分量
(12)连通图的生成树: 一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的 条边,这样的连通子图称为连通图的生成树
一棵有 个顶点的生成树有且仅有 条边。如果一个图有 个顶点和小于 条边,则是非连通图。如果它多于 条边,则一定有环。有 条边的图不一定是生成树
(13)有向树和生成森林: 有一个顶点的入度为0,其余顶点的入度均为1的有向图称为有向树。一个有向图的生成森林是由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧
4.2 图的存储结构
4.2.1 邻接矩阵
邻接矩阵是表示顶点之间相邻关系的矩阵。
的邻接矩阵是具有如下性质的 阶方阵:
若 是网,则邻接矩阵可以定义为:
(1)优点
- 便于判断两个顶点之间是否有边,即根据 或 来判断
- 便于计算各个顶点的度。对于无向图,邻接矩阵第 行元素之和就是顶点 的度;对于有向图,第 行元素之和就是顶点 的出度,第 列元素之和就是顶点 的入度
(2)缺点
- 不便于增加和删除顶点
- 不便于统计边的数目,需要扫描邻接矩阵所有元素才能统计完毕,时间复杂度为
- 空间复杂度高。如果是有向图, 个顶点需要 个单元存储边。如果是无向图,因其邻接矩阵是对称的,所以对规模较大的邻接矩阵可以采用压缩存储的方法,仅存储下三角(或上三角)的元素,这样需要 个单元即可。但无论以何种方式存储,邻接矩阵表示法的空间复杂度均为 ,这对于稀疏图而言尤其浪费空间
public class Graph {
public static void main(String[] args) {
Graph graph = new Graph();
AMGraph udn = graph.createUDN();
graph.print(udn);
}
// 无向图
AMGraph createUDN(){
Scanner input=new Scanner(System.in);
System.out.print("请输入顶点数:");
int vexNum = input.nextInt();
System.out.print("请输入边数:");
int arcNum = input.nextInt();
AMGraph graph = new AMGraph(vexNum, arcNum);
System.out.println("请输入所有顶点:");
for(int i=0;i<vexNum;i++){
System.out.print("顶点:");
String v = input.next();
graph.vexs[i] = v;
}
System.out.println("请输入所有边:");
for(int i=0;i<arcNum;i++){
System.out.println("---------------------");
System.out.print("请输入顶点v1:");
String v1 = input.next();
System.out.print("请输入顶点v2:");
String v2 = input.next();
System.out.print("请输入权值:");
int w = input.nextInt();
int n = locateVex(graph,v1);
int m = locateVex(graph,v2);
graph.arcs[n][m] = w;
graph.arcs[m][n] = w;
}
return graph;
}
int locateVex(AMGraph G,String v){
int n = G.vexNum;
for(int i=0;i<n;i++){
if(v.equals(G.vexs[i])){
return i;
}
}
return 0;
}
void print(AMGraph G){
for(int i=0;i<G.vexNum;i++){
for(int j=0;j<G.vexNum;j++){
System.out.print(G.arcs[i][j] + " ");
}
System.out.println();
}
}
}
class AMGraph{
String[] vexs;
int[][] arcs;
int vexNum,arcNum;
public AMGraph(int vexNum, int arcNum) {
this.vexNum = vexNum;
this.arcNum = arcNum;
vexs = new String[vexNum];
arcs = new int[vexNum][vexNum];
}
}
4.2.2 邻接表
邻接表是图的一种链式存储结构。
在邻接表中,对图中每个顶点 建立一个单链表,把与 相邻接的顶点放在这个链表中。
邻接表有两部分组成:
(1)表头结点表: 由所有表头结点以顺序结构的形式存储,以便可以随机访问任一顶点的边链表。表头结点包括数据域和链域两部分。数据域用于存储顶点 的名称或其他有关信息;链域用于指向链表中第一个结点(即域顶点 邻接的第一个邻接点)
(2)边表: 由表示图中顶点间关系的 个边链表组成。边链表中边结点包括邻接点域、数据域和链域三部分。邻接点域指示与顶点 邻接的点在图中的位置;数据域存储和边相关的信息,如权值等;链域指示与顶点 邻接的下一条边的结点
(1)优点
- 便于增加和删除顶点
- 便于统计边的数目,按顶点表顺序扫描所有边表可得到边的数目,时间复杂度为
- 空间效率高。对于一个具有 个顶点 条边的图 ,若 是无向图,则在其邻接表表示中有 个顶点表结点和 个边表结点;若 是有向图,则在它的邻接表表示或逆邻接表表示中均有 个顶点表结点和 个边表结点。因此,邻接表或逆邻接表表示的空间复杂度为 适合表示稀疏图。对于稠密图,考虑到邻接表中要附加链域,因此常采取邻接矩阵表示法
(2)缺点
- 不便于判断顶点之间是否有边,要判定 和 之间是否有边,就需扫描第 个边表,最坏情况下要耗费 时间
- 不便于计算有向图各个顶点的度。对于无向图,在邻接表表示中顶点 的度是第 个边表中的结点个数。在有向图的邻接表中,第 个边表上的结点个数是顶点 的出度;求 的入度比较困难,需遍历各顶点的边表。若有向图采用逆邻接表表示,则与邻接表表示相反,求顶点的入度比较容易,而求顶点的出度较难
public class Graph {
public static void main(String[] args) {
Graph graph = new Graph();
ALGraph udg = graph.createUDG();
graph.print(udg);
}
// 无向图
ALGraph createUDG(){
Scanner input=new Scanner(System.in);
System.out.print("请输入顶点数:");
int vexNum = input.nextInt();
System.out.print("请输入边数:");
int arcNum = input.nextInt();
ALGraph graph = new ALGraph(vexNum, arcNum);
System.out.println("请输入所有顶点:");
for(int i=0;i<vexNum;i++){
System.out.print("顶点:");
String v = input.next();
graph.vertices[i] = new VexNode(v);
}
System.out.println("请输入所有边:");
for(int i=0;i<arcNum;i++){
System.out.println("---------------------");
System.out.print("请输入顶点v1:");
String v1 = input.next();
System.out.print("请输入顶点v2:");
String v2 = input.next();
System.out.print("请输入权值:");
int w = input.nextInt();
int n = locateVex(graph,v1);
int m = locateVex(graph,v2);
ArcNode p1 = new ArcNode(m,w);
ArcNode p2 = new ArcNode(n,w);
p1.next = graph.vertices[n].firstArc;
graph.vertices[n].firstArc = p1;
p2.next = graph.vertices[m].firstArc;
graph.vertices[m].firstArc = p2;
}
return graph;
}
int locateVex(ALGraph G,String v){
int n = G.vexNum;
for(int i=0;i<n;i++){
if(v.equals(G.vertices[i].data)){
return i;
}
}
return 0;
}
void print(ALGraph G){
for(int i=0;i<G.vexNum;i++){
System.out.print(G.vertices[i].data + ":");
ArcNode arcNode = G.vertices[i].firstArc;
while(arcNode != null){
System.out.print(G.vertices[arcNode.adjVex].data + "\t");
arcNode = arcNode.next;
}
System.out.println();
}
}
}
/**
* 边结点
*/
class ArcNode{
// 该边所指向的顶点的位置
int adjVex;
ArcNode next;
// 权重
int w;
public ArcNode(int adjVex, int w) {
this.adjVex = adjVex;
this.w = w;
this.next = null;
}
}
/**
* 表头结点
*/
class VexNode{
// 顶点信息
String data;
// 指向第一条依附该顶点的边的指针
ArcNode firstArc;
public VexNode(String data) {
this.data = data;
this.firstArc = null;
}
}
class ALGraph{
VexNode[] vertices;
int vexNum,arcNum;
public ALGraph(int vexNum, int arcNum) {
this.vexNum = vexNum;
this.arcNum = arcNum;
vertices = new VexNode[vexNum];
}
}
4.2.3 十字链表
十字链表是有向图的另一种链式存储结构。可以看成是将有向图的邻接表和逆邻接表结合起来得到的一种链表。
十字链表由两部分组成:
(1)弧结点:尾域(tailvex) 指示弧尾;头域(headvex) 指示弧头;链域(hlink) 指向弧头相同的下一条弧;链域(tlink) 指向弧尾相同的下一条弧;信息域(info) 指向该弧的相关信息
(2)顶点结点:数据域(data) 存储和顶点相关的信息;链域(firstin) 指向以该顶点为弧头的第一个弧结点;链域(firstout) 指向以该顶点为弧尾的第一个弧结点
4.2.4 邻接多重表
邻接多重表是无向图的另一种链式存储结构。
在某些图的应用问题中需要对边进行某种操作,如对已被搜索过的边作记号或删除一条边等,此时需要找到表示同一条边的两个结点。因此,在进行这一类操作的无向图的问题中采用邻接多重表作存储结构更为适宜。
邻接多重表由两部分组成:
(1)边结点:标志域(mark) 可用以标记该条边是否被搜索过;ivex 和 jvex 为该边依附的两个顶点在图中的位置;ilink 指向下一条依附于顶点 ivex 的边;jlink 指向下一条依附于顶点 jvex 的边;info 为指向和边相关的各种信息的指针域
(2)顶点结点:数据域(data) 存储和该顶点相关的信息;firstedge 域指示第一条依附该顶点的边
4.3 图的遍历
4.3.1 深度优先搜索
深度优先搜索(DFS)遍历类似于树的先序遍历,是树的先序遍历的推广。
深度优先搜索遍历的过程如下:
(1)从图中某个顶点 出发,访问
(2)找出刚访问过的顶点的第一个未被访问的邻接点,访问该顶点。以该顶点为新顶点,重复此步骤,直至刚访问过的顶点没有未被访问的邻接点为止
(3)返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,访问该顶点
(4)重复步骤(2)和(3),直至图中所有顶点都被访问过,搜索结束
深度优先搜索遍历的算法分析:
(1)当用邻接矩阵表示图时,查找每个顶点的邻接点的时间复杂度为 ,其中 为图中顶点数
(2)当用邻接表表示图时,查找每个顶点的邻接点的时间度为 ,其中 为图中边数;因此,时间复杂度为
/**
* 深度优先搜索遍历非连通图
*/
void DFSTraverse(AMGraph G){
boolean[] visited = new boolean[G.vexNum];
for(int v=0;v<G.vexNum;v++){
if(!visited[v]){
DFS_AM(G,v,visited);
}
}
}
/**
* 采用邻接矩阵表示图的深度优先搜索遍历
*/
void DFS_AM(AMGraph G,int v,boolean[] visited){
// 访问该结点
System.out.println(G.vexs[v]);
visited[v] = true;
for(int w=0;w<G.vexNum;w++){
if(G.arcs[v][w]!=0 && !visited[w]){
DFS_AM(G,w,visited);
}
}
}
/**
* 采用邻接表表示图的深度优先搜索遍历
*/
void DFS_AL(ALGraph G,int v,boolean[] visited){
// 访问该结点
System.out.print(G.vertices[v].data);
visited[v] = true;
ArcNode p = G.vertices[v].firstArc;
while(p != null){
int w = p.adjVex;
if(!visited[w]){
DFS_AL(G,w,visited);
}
p = p.next;
}
}
4.3.2 广度优先搜索
广度优先搜索(BFS)遍历类似于树的层次遍历的过程。
广度优先搜索遍历的过程如下:
(1)从图中某个顶点 出发,访问
(2)依次访问 的各个未曾访问过的邻接点
(3)分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问。重复步骤(3),直至图中所有已被访问的顶点的邻接点都被访问到
广度优先搜索遍历的算法分析:
(1)当用邻接矩阵存储时,时间复杂度为
(2)当用邻接表存储时,时间复杂度为
/**
* 广度优先搜索遍历非连通图
*/
void BFSTraverse(AMGraph G){
boolean[] visited = new boolean[G.vexNum];
for(int v=0;v<G.vexNum;v++){
if(!visited[v]){
BFS_AM(G,v,visited);
}
}
}
/**
* 采用邻接矩阵表示图的广度优先搜索遍历
*/
void BFS_AM(AMGraph G,int v,boolean[] visited){
// 访问该结点
System.out.println(G.vexs[v]);
visited[v] = true;
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(v);
while(!queue.isEmpty()){
int v2 = queue.poll();
for(int w=0;w<G.vexNum;w++){
if(G.arcs[v2][w]!=0 && !visited[w]){
System.out.print(G.vexs[w]);
visited[w] = true;
queue.offer(w);
}
}
}
}
/**
* 采用邻接表表示图的广度优先搜索遍历
*/
void BFS_AL(ALGraph G,int v,boolean[] visited){
// 访问该结点
System.out.print(G.vertices[v].data);
visited[v] = true;
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(v);
while (!queue.isEmpty()){
int v2 = queue.poll();
ArcNode p = G.vertices[v2].firstArc;
while(p != null){
int w = p.adjVex;
if(!visited[w]){
System.out.print(G.vertices[w].data);
visited[w] = true;
queue.offer(w);
}
p = p.next;
}
}
}
4.4 图的应用
4.4.1 最小生成树
在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树称为该连通网的最小代价生成树,简称最小生成树。
4.4.1.1 普里姆算法(Prim算法)
普里姆算法(Prim算法) ,图论中的一种算法,可在加权连通图里搜索最小生成树。
普里姆算法的构造过程:
假设 是连通网, 是 上最小生成树中边的集合。
(1),
(2)在所有 , 的边 中找一条权值最小的边 并入集合 ,同时 并入
(3)重复步骤(2),直到 为止
下面是Prim算法的执行过程,该方法又称为加点法:
public class Graph {
/**
* 权值为 0 表示不连通
*/
/**
* 基于邻接矩阵表示的图的 Prim算法
*/
void MiniSpanTree_Prime(AMGraph G,String u){
// 顶点 u 的下标
int k = locateVex(G,u);
CloseEdge[] closeEdge = new CloseEdge[G.vexNum];
// 初始化,并将与顶点 u 相连的边存入 closeEdge 集合
for(int j=0;j<G.vexNum;j++){
closeEdge[j] = new CloseEdge();
if(k != j){
closeEdge[j].adjVex = k;
closeEdge[j].lowCost = G.arcs[k][j];
}
}
// 初始,U={u}
closeEdge[k].lowCost = 0;
for(int i=1;i<G.vexNum;i++){
k = getMinAdjVex(closeEdge);
// u0 为最小边的一个顶点,u0 属于 U
int u0 = closeEdge[k].adjVex;
// v0 为最小边的另一个顶点,v0 属于 V-U
int v0 = k;
System.out.println("(" + G.vexs[u0] + "," + G.vexs[v0] + ") -> "
+ G.arcs[u0][v0]);
// 将顶点 v0 并入集合 U
closeEdge[v0].lowCost = 0;
// 重新选择最小边
for(int j=0;j<G.vexNum;j++){
if(G.arcs[v0][j]!=0 && G.arcs[v0][j] < closeEdge[j].lowCost){
closeEdge[j].adjVex = v0;
closeEdge[j].lowCost = G.arcs[v0][j];
}
}
}
}
/**
* 基于邻接表的图的 Prim算法
*/
void MiniSpanTree_Prime(ALGraph G,String u){
// 顶点 u 的下标
int k = locateVex(G,u);
CloseEdge[] closeEdge = new CloseEdge[G.vexNum];
// 初始化,并将与顶点 u 相连的边存入 closeEdge 集合
ArcNode p = G.vertices[k].firstArc;
while(p != null){
closeEdge[p.adjVex] = new CloseEdge();
closeEdge[p.adjVex].adjVex = k;
closeEdge[p.adjVex].lowCost = p.w;
p = p.next;
}
for(int j=0;j<G.vexNum;j++){
if(closeEdge[j] == null){
closeEdge[j] = new CloseEdge();
}
}
// 初始,U={u}
closeEdge[k].lowCost = 0;
for(int i=1;i<G.vexNum;i++){
k = getMinAdjVex(closeEdge);
// u0 为最小边的一个顶点,u0 属于 U
int u0 = closeEdge[k].adjVex;
// v0 为最小边的另一个顶点,v0 属于 V-U
int v0 = k;
System.out.println("(" + G.vertices[u0].data + ","
+ G.vertices[v0].data + ") -> " + closeEdge[v0].lowCost);
// 将顶点 v0 并入集合 U
closeEdge[v0].lowCost = 0;
// 重新选择最小边
p = G.vertices[v0].firstArc;
while(p != null){
if(p.w < closeEdge[p.adjVex].lowCost){
closeEdge[p.adjVex].adjVex = v0;
closeEdge[p.adjVex].lowCost = p.w;
}
p = p.next;
}
}
}
/**
* 求出最小边点的索引位置
*/
int getMinAdjVex(CloseEdge[] closeEdges){
int minAdjVex = -1;
for(int i=0;i< closeEdges.length;i++){
if(closeEdges[i].lowCost != 0){
if(minAdjVex == -1){
minAdjVex = i;
}else{
minAdjVex = closeEdges[minAdjVex].lowCost < closeEdges[i].lowCost
? minAdjVex:i;
}
}
}
return minAdjVex;
}
int locateVex(AMGraph G,String v){
int n = G.vexNum;
for(int i=0;i<n;i++){
if(v.equals(G.vexs[i])){
return i;
}
}
return 0;
}
int locateVex(ALGraph G,String v){
int n = G.vexNum;
for(int i=0;i<n;i++){
if(v.equals(G.vertices[i].data)){
return i;
}
}
return 0;
}
}
class CloseEdge{
// 最小边在U中的那个顶点
int adjVex;
// 最小边上的权值
int lowCost;
public CloseEdge() {
this.adjVex = -1;
this.lowCost = 0;
}
}
// 通过BFS实现Prim
public class Prim {
// 核心数据结构,存储「横切边」的优先级队列
private PriorityQueue<int[]> pq;
// 类似 visited 数组的作用,记录哪些节点已经成为最小生成树的一部分
boolean[] inMST;
private int weightSum;
private List<int[]>[] graph;
public Prim(List<int[]>[] graph){
this.graph = graph;
int n = graph.length;
pq = new PriorityQueue<>((a,b) -> a[2]-b[2]);
inMST = new boolean[n];
weightSum = 0;
while (!pq.isEmpty()){
int[] edge = pq.poll();
int from = edge[0];
int to = edge[1];
int weight = edge[2];
if(inMST[to]){
continue;
}
inMST[to] = true;
weightSum += weight;
cut(to);
}
}
private void cut(int s){
for(int[] edge: graph[s]){
int to = edge[1];
if(inMST[to]){
continue;
}
pq.offer(edge);
}
}
public int weightSum(){
return weightSum;
}
public boolean allConnected(){
for(int i=0;i< inMST.length;i++){
if(!inMST[i]){
return false;
}
}
return true;
}
}
4.4.1.2 克鲁斯卡尔算法(Kruskal算法)
克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。与普里姆算法不同,它的时间复杂度为 ( 为网中的边数),所以,适合于求边稀疏的网的最小生成树。
克鲁斯卡尔算法的构造过程:
假设连通网 ,将 中的边按权值从小到大的顺序排序。
(1)初始状态为只有 个顶点而无边的非连通图 ,图中每个顶点自成一个连通分量
(2)在 中选择权值最小的的边,若该边依附的顶点落在 中不同的连通分量上(即不形成回路),则将此边加入到 中,否则舍去此边而选择下一条权值最小的边
(3)重复(2),直到 中所有顶点都在同一连通分量上为止
下面是Kruskal算法的执行过程,该方法又称为加边法:
public class Graph {
/**
* 权值为 0 表示不连通
*/
/**
* 基于邻接矩阵表示的图的 Kruskal算法
*/
void MiniSpanTree_Kruskal(AMGraph G){
// 保存每条边的始点、终点和权值
Edge[] edges = new Edge[G.arcNum];
// 保存连通分量,vexSet[i] = j 表示顶点 i 属于连通分量 j
int[] vexSet = new int[G.vexNum];
int index = 0;
// 初始化
for(int i=0;i<G.vexNum;i++){
for(int j=i+1;j<G.vexNum;j++){
if(G.arcs[i][j] != 0){
edges[index] = new Edge();
edges[index].head = i;
edges[index].tail = j;
edges[index].lowCost = G.arcs[i][j];
index++;
}
}
}
// 将数组 edges 中的元素按权值从小到大排序
Arrays.sort(edges, Comparator.comparingInt(a -> a.lowCost));
// 各顶点自成一个连通分量
for(int i=0;i<G.vexNum;i++){
vexSet[i] = i;
}
for(int i=0;i<G.arcNum;i++){
int v1 = edges[i].head;
int v2 = edges[i].tail;
int vs1 = vexSet[v1];
int vs2 = vexSet[v2];
// 边的两个顶点分属不同的连通分量
if(vs1 != vs2){
System.out.println("(" + G.vexs[v1] + "," + G.vexs[v2] + ") -> "
+ edges[i].lowCost);
// 合并 vs1 和 vs2 两个连通分量
for(int j=0;j<G.vexNum;j++){
if(vexSet[j] == vs2){
vexSet[j] = vs1;
}
}
}
}
}
/**
* 基于邻接表表示的图的 Kruskal算法
*/
void MiniSpanTree_Kruskal(ALGraph G){
// 保存每条边的始点、终点和权值
Edge[] edges = new Edge[G.arcNum];
// 保存连通分量,vexSet[i] = j 表示顶点 i 属于连通分量 j
int[] vexSet = new int[G.vexNum];
int index = 0;
// 初始化
Map<Integer, Set<Integer>> vexRecord = new HashMap<>();
for(int i=0;i<G.vexNum;i++){
ArcNode p = G.vertices[i].firstArc;
while(p != null){
Set<Integer> setV1 = vexRecord.getOrDefault(i,new HashSet<>());
Set<Integer> setV2 = vexRecord.getOrDefault(p.adjVex,new HashSet<>());
if(!setV1.contains(p.adjVex) && !setV2.contains(i)){
edges[index] = new Edge();
edges[index].head = i;
edges[index].tail = p.adjVex;
edges[index].lowCost = p.w;
setV1.add(p.adjVex);
setV2.add(i);
index++;
}
vexRecord.put(i,setV1);
vexRecord.put(p.adjVex,setV2);
p = p.next;
}
}
// 将数组 edges 中的元素按权值从小到大排序
Arrays.sort(edges, Comparator.comparingInt(a -> a.lowCost));
// 各顶点自成一个连通分量
for(int i=0;i<G.vexNum;i++){
vexSet[i] = i;
}
for(int i=0;i<G.arcNum;i++){
int v1 = edges[i].head;
int v2 = edges[i].tail;
int vs1 = vexSet[v1];
int vs2 = vexSet[v2];
// 边的两个顶点分属不同的连通分量
if(vs1 != vs2){
System.out.println("(" + G.vertices[v1].data + ","
+ G.vertices[v2].data + ") -> " + edges[i].lowCost);
// 合并 vs1 和 vs2 两个连通分量
for(int j=0;j<G.vexNum;j++){
if(vexSet[j] == vs2){
vexSet[j] = vs1;
}
}
}
}
}
}
class Edge{
// 边的始点
int head;
// 边的终点
int tail;
// 边上的权值
int lowCost;
}
// 通过Union-Find实现Kruskal
public class Kruskal {
// edges ——> edges[i][0] 起点,edges[i][1] 终点,edges[i][2] 权重
public static int getMinWeight(int n,int[][] edges){
UF uf = new UF(n);
Arrays.sort(edges,(a,b) -> a[2]-b[2]);
int mst = 0;
for(int[] edge: edges){
int u = edge[0];
int v = edge[1];
int weight = edge[2];
if(uf.connected(u,v)){
continue;
}
mst += weight;
uf.union(u,v);
}
return uf.count()==1 ? mst:-1;
}
private static class UF{
private int count;
private int[] parent;
public UF(int n){
parent = new int[n];
for(int i=0;i<n;i++){
parent[i] = i;
}
this.count = n;
}
public void union(int p,int q){
int rootP = find(p);
int rootQ = find(q);
if(rootP == rootQ){
return;
}
parent[rootP] = rootQ;
count--;
}
public boolean connected(int p,int q){
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
public int find(int x){
if(parent[x] != x){
parent[x] = find(parent[x]);
}
return x;
}
public int count(){
return count;
}
}
}
4.4.2 最短路径
用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
在带权有向网中,称路径上的第一个顶点为源点,最后一个顶点为终点。
4.4.2.1 迪杰斯特拉(Dijkstra)算法
1.从某个源点到其余各顶点的最短路径
给定带权有向图 和源点 ,求从 到 中其余各项点的最短路径。
迪杰斯特拉(Dijkstra)算法——一个按路径长度递增的次序产生最短路径的算法
迪杰斯特拉算法的求解过程:
对于网 ,将 中的顶点分成两组:
- 第一组 :已求出的最短路径的终点集合(初始时只包含源点 )
- 第二组 :尚未求出的最短路径的顶点集合(初始时为 )
(1)按各顶点与 间最短路径长度递增的次序排列,逐个将集合 中的顶点加入到集合 中
(2)总保持从 到集合 中各顶点的路径长度始终不大于到集合 中各顶点的路径长度
迪杰斯特拉算法分析:
求解最短路径的主循环共进行 次,每次执行的时间是 ,所以算法的时间复杂度是
案例解析:
1.初始化
- 将源点 加入 集合,即
- 将 到各个终点的最短路径长度初始化为权值,即
- 如果 和 之前有弧,则 的前驱置为 ,即 ,否则
以上案例的初始化结果如下所示:
2.循环执行以下操作:
- 选择下一条最短路径的终点 ,使得:
- 将 加到 中,即
- 根据条件更新从 出发到集合 上任一顶点的最短路径的长度。若条件 成立,则更新 ,同时更改 的前驱为 ,即
以上案例的执行流程如下所示:
void ShortestPath_DIJ(AMGraph G,int v0){
int n = G.vexNum;
// 记录从源点 v0 到终点 vi 是否已被确定最短路径长度
boolean[] S = new boolean[n];
// Path[i]:记录从源点 v0 到终点 vi 的当前最短路径上 vi 的直接前驱顶点序号
// 其初值为:如果从 v0 到 vi 有弧,则 Path[i] 为 v0,否则为 -1
int[] Path = new int[n];
Arrays.fill(Path,-1);
// D[i]:记录从源点 v0 到终点 vi 的当前最短路径长度
// 其初值为:如果从 v0 到 vi 有弧,则 D[i] 为弧上的权值,否则为 ∞
int[] D = new int[n];
// 初始化
for(int v=0;v<n;v++){
S[v] = false;
D[v] = G.arcs[v0][v];
if(D[v] < Integer.MAX_VALUE){
Path[v] = v0;
}
}
S[v0] = true;
D[v0] = 0;
for(int i=1;i<n;i++){
int v = v0;
int minArc = Integer.MAX_VALUE;
// 选择一条当前最短路径,终点为 v
for(int w=0;w<n;w++){
if(!S[w] && D[w]<minArc){
v = w;
minArc = D[w];
}
}
S[v] = true;
PrintPath(G,Path,v,minArc,v0);
// 更新从 v0 出发到集合 V-S 上所有顶点的最短路径长度
for(int w=0;w<n;w++){
if(!S[w] && G.arcs[v][w]!=Integer.MAX_VALUE
&& (D[v]+G.arcs[v][w] < D[w])){
D[w] = D[v] + G.arcs[v][w];
Path[w] = v;
}
}
}
}
void PrintPath(AMGraph graph,int[] path,int v,int weight,int v0){
if(v != v0){
StringBuffer sb = new StringBuffer();
while(v != -1){
sb.insert(0,graph.vexs[v] + " ");
v = path[v];
}
sb.insert(0,"( ");
sb.append(" ) -> " + weight);
System.out.println(sb.toString());
}
}
public class Dijkstra {
public int[] dijkstra(int start, List<int[]>[] graph){
int V = graph.length;
// 记录最短路径的权重
// 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重
int[] distTo = new int[V];
Arrays.fill(distTo,Integer.MAX_VALUE);
distTo[start] = 0;
PriorityQueue<State> pq = new PriorityQueue<>((a,b)
-> a.distFromStart-b.distFromStart);
pq.offer(new State(start,0));
while (!pq.isEmpty()){
State curState = pq.poll();
int curId = curState.id;
int curDistFromStart = curState.distFromStart;
if(curDistFromStart > distTo[curId]){
continue;
}
for(int[] neighbor: graph[curId]){
int nextNodeId = neighbor[0];
int weight = neighbor[1];
int distToNextNode = curDistFromStart + weight;
if(distToNextNode < distTo[nextNodeId]){
distTo[nextNodeId] = distToNextNode;
pq.offer(new State(nextNodeId,distToNextNode));
}
}
}
return distTo;
}
class State{
// 图节点的 id
int id;
// 从 start 节点到当前节点的距离
int distFromStart;
State(int id, int distFromStart) {
this.id = id;
this.distFromStart = distFromStart;
}
}
}
4.4.2.2 弗洛伊德(Floyd)算法
2.每一对顶点之间的最短路径
求解每一对顶点之间的最短路径,有两种方法:
(1)调用 次迪杰斯特拉算法,时间复杂度为
(2)采用弗洛伊德(Floyd)算法,时间复杂度为
说明: 中更新 ;首先加入结点为 ,查看 和 是否连通,可知 ;再查看结点 和 是否连通,可知 ;所以以结点 为过渡结点时,,更新矩阵中距离。
/**
* Floyd算法
*/
void ShortestPath_Floyd(AMGraph G){
int n = G.vexNum;
// Path[i][j]:表明从顶点 i 到顶点 j 的路径上,顶点 j 的前一顶点的序号
int[][] Path = new int[n][n];
// D[i][j]:记录顶点 i 和 j 之间的最短路径长度
int[][] D = new int[n][n];
// 初始化
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
D[i][j] = G.arcs[i][j];
if(D[i][j] < Integer.MAX_VALUE){
// 如果 i 和 j 之间有弧,则将 j 的前驱置为 i
Path[i][j] = i;
}else{
// 如果 i 和 j 之间无弧,则将 j 的前驱置为 -1
Path[i][j] = -1;
}
}
}
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
// 从 i 经 k 到 j 的一条路径更短
if(D[i][k]!=Integer.MAX_VALUE && D[k][j]!=Integer.MAX_VALUE
&& (D[i][k]+D[k][j] < D[i][j])){
D[i][j] = D[i][k] + D[k][j];
Path[i][j] = Path[k][j];
}
}
}
}
PrintPath(G,D,Path);
}
void PrintPath(AMGraph graph,int[][] D,int[][] path){
for(int i=0;i<graph.vexNum;i++){
for(int j=0;j< graph.vexNum;j++){
if(i != j){
if(D[i][j] == Integer.MAX_VALUE){
System.out.println("( " + graph.vexs[i] + " " + graph.vexs[j]
+ " ) -> No path");
}else{
StringBuffer sb = new StringBuffer();
int pre = path[i][j];
while(pre != i){
sb.insert(0,graph.vexs[pre] + " ");
pre = path[i][pre];
}
sb.insert(0,"( " + graph.vexs[i] + " ");
sb.append(graph.vexs[j] + " ) -> " + D[i][j]);
System.out.println(sb.toString());
}
}
}
}
}
4.4.3 拓扑排序
1.AOV-网
一个无环的有向图称作有向无环图,简称DAG图。
有向无环图是描述一项工程或系统的进行过程的有效工具。通常把计划、实施工程、生产流程、程序流程等都当成一个工程。
用顶点表示活动,用弧表示活动时间的优先关系的有向图称为顶点表示活动的网,简称AOV-网。
判定网中是否存在环——对有向图的顶点进行拓扑排序。
拓扑排序就是将AOV-网中所有顶点排成一个线性序列,该序列满足:若在AOV-网中由顶点 到顶点 有一条路径,则在该线性序列中的顶点 必定在顶点 之前。
拓扑排序过程:
(1)在有向图中选一个无前驱的顶点且输出它
(2)从图中删除该顶点和所有以它为尾的弧
(3)重复(1)和(2),直至不存在无前驱的顶点
(4)若此时输出的顶点数小于有向图中的顶点数,则说明有向图中存在环,否则输出的顶点序列即为一个拓扑序列
拓扑排序的实现:
可采用邻接表做有向图的存储结构。
- 求出各顶点的入度存入数组 中,并将入度为 的顶点入栈
- 只要栈非空,则重复以下操作:将栈顶顶点 出栈并保存在拓扑序列数组 中;对顶点 的每个邻接点 的入度减 ;如果 的入度变为 ,则将 入栈
- 如果 数组中的顶点个数少于 AOV-网的顶点个数,则网中存在有向环,无法进行拓扑排序,否则拓扑排序成功
拓扑排序算法分析:
对有 个顶点和 条边的有向图而言,建立求各顶点入度的时间复杂度为 ;建立零入度顶点栈的时间复杂度为 ;在拓扑排序过程中,若有向图无环,则每个顶点进一次栈,出一次栈,入度减1的操作在循环中共执行 次,所以总的时间复杂度为 。
/**
* AOV-网 拓扑排序
*/
void TopologicalSort(ALGraph G){
int n = G.vexNum;
// 保存拓扑排序
List<String> topo = new ArrayList<>();
// 存放各顶点入度
int[] inDegree = new int[n];
// 计算各顶点入度
for(int i=0;i<n;i++){
ArcNode p = G.vertices[i].firstArc;
while(p != null){
inDegree[p.adjVex]++;
p = p.next;
}
}
// 暂存所有入度为 0 的顶点
Stack<Integer> stack = new Stack<>();
for(int i=0;i<n;i++){
if(inDegree[i] == 0){
stack.push(i);
}
}
while(!stack.isEmpty()){
int v = stack.pop();
topo.add(G.vertices[v].data);
ArcNode p = G.vertices[v].firstArc;
while(p != null){
inDegree[p.adjVex]--;
if(inDegree[p.adjVex] == 0){
stack.push(p.adjVex);
}
p = p.next;
}
}
if(topo.size() == n){
StringBuilder sb = new StringBuilder();
for(String vex: topo){
sb.append(vex + " ");
}
System.out.println(sb.toString());
}else{
System.out.println("AOV-网 存在环");
}
}
4.4.4 关键路径
1.AOE-网
AOE-网是一个带权的有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的时间。
AOE-网可用来估算工程的完成时间。
通常需要解决以下两个问题:
(1)估算完成整项工程至少需要多少时间
(2)判断哪些活动是影响工程进度的关键
在正常的情况(无环)下,网中只有一个入度为零的点,称作源点;只有一个出度为零的点,称作汇点。
在AOE-网中,一条路径各弧上的权值之和称为该路径的带权路径长度(后面简称路径长度)。
要估算整项工程完成的最短时间,就是要找一条从源点到汇点的带权路径长度最长的路径,称为关键路径。
关键路径上的活动叫做关键活动,这些活动是影响工程进度的关键,它们的提前或拖延将使整个工程提前或拖延。
如何确定关键路径,首先定义4个描述量:
(1)事件 的最早发生时间
进入事件 的每一活动都结束, 才可以发生,所以 是从源头到 的最长路径长度。
求 的值,可根据拓扑排序从源点开始向汇点递推。
其中, 是所有以 为头的弧的集合, 是弧 的权值,即对应活动 的持续时间。
(2)事件 的最迟发生事件
事件 的发生不得延误 的每一后继事件的最迟发生时间。
求出 后,可根据逆拓扑排序从汇点开始向源点递推,求出 。
(3)活动 的最早开始时间
只有事件 发生了,活动 才能开始。所以,活动 的最早开始事件等于事件 的最早发生时间 ,即
(4)活动 的最晚开始时间
活动 的最晚开始时间需保证不延误事件 的最迟发生时间。所以,活动 的最晚开始时间 等于事件 的最迟发生时间 减去活动 的持续时间 ,即:
对于关键活动而言, 。
一个活动 的最迟开始时间 和最早开始时间 的差值 是该活动完成的时间余量。
关键路径求解例子:
关键路径算法分析:
在求每个事件的最早开始时间和最迟开始时间,以及活动的最早和最迟开始时间时,都要对所有顶点以及每个顶点边表中所有的边结点进行检查,所以,求关键路径算法的时间复杂度为 。
关键路径算法实现:
/**
* AOE-网 关键路径
*/
void CriticalPath(ALGraph G){
List<Integer> topo = new ArrayList<>();
if(!TopologicalSort(G,topo)){
System.out.println("AOE-网 存在环");
return;
}
int n = G.vexNum;
// 保存每个事件的最早发生时间
int[] ve = new int[n];
// 保存每个事件的最迟发生时间
int[] vl = new int[n];
// 求每个事件的最早发生时间
for(int i=0;i<n;i++){
int k = topo.get(i);
ArcNode p = G.vertices[k].firstArc;
while(p != null){
int j = p.adjVex;
ve[j] = Math.max(ve[j],ve[k]+p.w);
p = p.next;
}
}
// 给每个事件的最迟发生时间值初值 ve[n-1]
Arrays.fill(vl,ve[n-1]);
// 求每个事件的最迟发生时间
for(int i=n-1;i>=0;i--){
int k = topo.get(i);
ArcNode p = G.vertices[k].firstArc;
while(p != null){
int j = p.adjVex;
vl[k] = Math.min(vl[k],vl[j]-p.w);
p = p.next;
}
}
// 计算关键活动,每次循环针对 vi 为活动开始点的所有活动
for(int i=0;i<n;i++){
ArcNode p = G.vertices[i].firstArc;
while(p != null){
int j = p.adjVex;
// 计算活动 <vi,vj> 的最早开始时间
int e = ve[i];
// 计算活动 <vi,vj> 的最迟开始时间
int l = vl[j] - p.w;
if(e == l){
System.out.println("< " + G.vertices[i].data + " , "
+ G.vertices[j].data + " >");
}
p = p.next;
}
}
}
/**
* AOV-网 拓扑排序
*/
boolean TopologicalSort(ALGraph G,List<Integer> topo){
int n = G.vexNum;
// 存放各顶点入度
int[] inDegree = new int[n];
// 计算各顶点入度
for(int i=0;i<n;i++){
ArcNode p = G.vertices[i].firstArc;
while(p != null){
inDegree[p.adjVex]++;
p = p.next;
}
}
// 暂存所有入度为 0 的顶点
Stack<Integer> stack = new Stack<>();
for(int i=0;i<n;i++){
if(inDegree[i] == 0){
stack.push(i);
}
}
while(!stack.isEmpty()){
int v = stack.pop();
topo.add(v);
ArcNode p = G.vertices[v].firstArc;
while(p != null){
inDegree[p.adjVex]--;
if(inDegree[p.adjVex] == 0){
stack.push(p.adjVex);
}
p = p.next;
}
}
if(topo.size() == n){
return true;
}else{
return false;
}
}