图论
一、理论基础
参见[图论理论基础]
(一)、基本概念
图论中的图是由一组顶点和一组边组成的,边连接顶点。顶点也称为[节点],代表实体,边代表顶点之间的关系。边可以是无向的,也可以是有向的。
- 无向边表示顶点之间的关系是对称的,由无向边和定点组成的图为无向图 。
- 有向边则表示顶点之间的关系是有方向的。由有向边和顶点组成的图为有向图 。
1、度
在无向图中,一个顶点的度是指,与该顶点相关联的边的数量。
在有向图中,顶点的度分为出度和入度。出度 是指从该顶点出发的边的数量,入度是指进入该顶点边的数量。
2、路径
从一个顶点到另一个顶点的边的序列,称为路径。路径的长度是指,路径中边的数量。
如果路径中没有重复的顶点,那就是简单路径。如果路径中的起点和终点相同,那么该路径称为回路或者环。
3、连通性
在图中,如果两个顶点之间存在路径,则说明这两个顶点是连通 的。如果一个图的任意两个顶点都是连通的,那么这个图就被称为连通图。
在有向图中,对于任意的两个顶点 <math xmlns="http://www.w3.org/1998/Math/MathML"> U U </math>U和 <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V,若同时存在 <math xmlns="http://www.w3.org/1998/Math/MathML"> U → V U \rightarrow V </math>U→V和 <math xmlns="http://www.w3.org/1998/Math/MathML"> V → U V \rightarrow U </math>V→U的路径,则说明该有向图是强连通的。
若将该有向图是作为无向图之后,无向图是连通的,则该图是弱连通的。
4、连通分量
无向图中的极大连通子图,一个无向图可以有多个连通分量。整个图是连通的,当前仅当它只有一个连通分量。
5、强连通分量
有向图中的极大连通子图,一个有向图可以有多个强连通分量。
6、生成树
一个连通无向图的生成树是指该图的一个子图,它是一棵树,包含图中的所有顶点。生成树通常用于寻找最小生成树,即权重之和最小的生成树。
7、生成森林
一个无向图的生成森林是指,该图的每一个连通分量的生成树的集合。
(二)、图的存储
一般,图的存储方式为邻接表和邻接矩阵。
1、邻接表
邻接表是一种用数组和链表结合起来表示图的方式。数组中的每一个元素表示顶点,其指向一个链表,链表中存储的是与该顶点相连的顶点。
java
import java.util.ArrayList;
import java.util.LinkedList;
public class Graph {
private int vertices; // 顶点数量
private ArrayList<LinkedList<Integer>> adjacencyList; // 邻接表
// 构造函数
public Graph(int vertices) {
this.vertices = vertices;
adjacencyList = new ArrayList<>(vertices);
// 初始化邻接表
for (int i = 0; i < vertices; i++) {
adjacencyList.add(new LinkedList<>());
}
}
// 添加一条边 u->v
public void addEdge(int u, int v) {
adjacencyList.get(u).add(v);
}
// 打印图的邻接表
public void printGraph() {
for (int i = 0; i < vertices; i++) {
System.out.print("顶点 " + i + " 的邻接表:");
for (int neighbor : adjacencyList.get(i)) {
System.out.print(neighbor + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
Graph graph = new Graph(5); // 创建一个有5个顶点的图
graph.addEdge(0, 1);
graph.addEdge(0, 4);
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(1, 4);
graph.addEdge(2, 3);
graph.addEdge(3, 4);
graph.printGraph();
}
}
2、邻接矩阵
邻接矩阵是一个二维数组,用于表示顶点之间的连接关系里如果顶点i
和j
之间有边,则matrix[i][j]
之间的值为1
,否则为0
。
java
public class Graph {
private int vertices; // 顶点数量
private int[][] adjacencyMatrix; // 邻接矩阵
// 构造函数
public Graph(int vertices) {
this.vertices = vertices;
adjacencyMatrix = new int[vertices][vertices];
}
// 添加一条边 u->v
public void addEdge(int u, int v) {
adjacencyMatrix[u][v] = 1;
// 如果是无向图,还需要设置 adjacencyMatrix[v][u] = 1
}
// 打印图的邻接矩阵
public void printGraph() {
System.out.println("顶点之间的邻接矩阵:");
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++) {
System.out.print(adjacencyMatrix[i][j] + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
Graph graph = new Graph(5); // 创建一个有5个顶点的图
graph.addEdge(0, 1);
graph.addEdge(0, 4);
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(1, 4);
graph.addEdge(2, 3);
graph.addEdge(3, 4);
graph.printGraph();
}
}
邻接表和邻接矩阵的比较:
邻接矩阵 | 邻接表 | |
---|---|---|
空间效率 | <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( V 2 ) O(V^2) </math>O(V2) | <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( V + E ) O(V + E) </math>O(V+E) |
增删顶点 | <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),需考虑数组扩容 | <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) |
判断边 | <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) | <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( E / V ) O(E / V) </math>O(E/V) |
适用场景 | 密集图,结构稳定 | 稀疏图,图结构动态变化比较大 |
(三)、图的遍历
图的遍历主要有两种,广度优先遍历(Board first search,BFS)和深度优先遍历(Deep first search, DFS)。
1、深度优先遍历
深度优先遍历就是,选择一个尚未被访问过的顶点,递归进行深度优先搜索,直到没有未访问过的邻接顶点为止。然后回溯到上一个顶点,继续访问其他未访问过的顶点。
DFS框架:
- 定义一个布尔数组
boolean[] visited
,记录某一个顶点是否被访问过 - 从某个顶点 <math xmlns="http://www.w3.org/1998/Math/MathML"> v i v_i </math>vi开始,标记该顶点已经被访问
- 遍历 <math xmlns="http://www.w3.org/1998/Math/MathML"> v i v_i </math>vi的所有邻接顶点 <math xmlns="http://www.w3.org/1998/Math/MathML"> w j w_j </math>wj,对于每个未访问 的 <math xmlns="http://www.w3.org/1998/Math/MathML"> w j w_j </math>wj,递归调用DFS函数
java
public void DFS(int startVertex) {
boolean[] visited = new boolean[vertices];
DFSUtil(startVertex, visited);
}
private void DFSUtil(int v, boolean[] visited) {
visited[v] = true;
System.out.print(v + " ");
for (int neighbor : adjacencyList.get(v)) {
if (!visited[neighbor]) {
DFSUtil(neighbor, visited);
}
}
}
2、广度优先遍历
从某个节点出发,先访问该顶点,然后一次访问其所有未访问过的邻接顶点,再按照这些邻接顶点的顺序一次访问其未访问过的邻接顶点,直到所有的节点都被访问过为止。
BFS框架
- 定义一个布尔数组
boolean[] visited
,记录每一个及诶的那是否被访问过 - 使用一个队列来辅助遍历,将起始顶点
v
入队,并标记为已经访问 - 当队列不为空的时候,取出队首元素,访问该节点,并将其所有未访问过的顶点入队,并标记为已经访问
java
public void BFS(int startVertex) {
boolean[] visited = new boolean[vertices];
Queue<Integer> queue = new LinkedList<>();
visited[startVertex] = true;
queue.add(startVertex);
while (!queue.isEmpty()) {
int v = queue.poll();
System.out.print(v + " ");
for (int neighbor : adjacencyList.get(v)) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.add(neighbor);
}
}
}