无向图
无向图
图模型
四种重要的图模型:
- 无向图
- 有向图
- 加权图
- 加权有向图
无向图:边仅仅是两个顶点之间的连接。
术语表
特殊的情况:
- 自环
- 平行边
术语表:
- 相邻
- 度数
- 子图
- 路径:由边顺序连接的一系列顶点
- 路径的长度:其中所包含的边数
- 简单路径:一条没有重复顶点的路径
- 连通
- 连通图
- 极大连通子图
- 生成树
- 稀疏图/稠密图/二分图
表示无向图的数据模型
无向图 API:
public class Graph {
Graph(int V); // 创建一个含有 V 个顶点但不含有边的图
Graph(In in); // 从标准输入流 in 读取一幅图
int V(); // 顶点数
int E(); // 边数
void addEdge(int v, int w); // 添加一个边 v-w
Iterable<Integer> adj(int v); // 和 v 相邻的所有顶点
String toSting(); // 图的字符串表示
}
本节将学习的所有算法都基于
adj()
方法所抽象的基本操作
实现图 API 的三种数据结构:
- 邻接矩阵:用 V 乘 V 的布尔矩阵表示,顶点存在连接时值为 true,否则为 false(空间消耗大)
- 边的数组:使用一个 Edge 对象表示边(实现
adj()
需要检查图中所有边) - 邻接表数组:使用一个以顶点为索引的列表数组,列表保存该顶点的相邻顶点(本章选择的数据结构)
邻接表数组数据结构:
V 相邻顶点(邻接表)
0 -> 1, 2
1 -> 0
2 -> 0
3 -> 4
4 -> 3
// 无向图
public class Graph {
private int V; // 顶点数
private int E; // 边数
private Bag<Integer>[] adj; // 邻接表数组
// 创建一个包含 V 个顶点,但不含有边的图
public Graph(int V) {
this.V = V;
this.E = 0;
this.adj = new Bag[V];
for (int i = 0; i < V; i++) {
adj[i] = new Bag<>();
}
}
// 从标准输入流读取一幅图
public Graph(In in) {
this(in.readInt());
int E = in.readInt(); // 局部变量
for (int i = 0; i < V; i++) {
int v = in.readInt();
int w = in.readInt();
addEdge(v, w);
}
}
// 添加一条边 v-w
public void addEdge(int v, int w) {
adj[v].add(w);
adj[w].add(v);
E++;
}
// 顶点 v 的所有相邻顶点
public Iterable<Integer> adj(int v) {
return adj[v];
}
public int V() {
return V;
}
public int E() {
return E;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(String.format("%d vertices, %d edges%n", V, E));
for (int v = 0; v < V; v++) {
builder.append(v + ": ");
for (int w : adj(v)) {
builder.append(w +" ");
}
builder.append(System.lineSeparator());
}
return builder.toString();
}
}
图处理算法设计模式
为每个任务(算法)创建一个相应的类,用例可以创建相应的对象来完成任务。
用例:
- 创建一幅图
- 将图传递给实现某个算法的类
- 调用实现算法的类对象完成任务
图搜索算法
搜索迷宫:检查入口和出口是否连通
图搜索算法 API:
public class Search {
Search(Graph G, int s); // 找到和起点 s 连通的所有顶点
boolean marked(int v); // v 和 s 是否连通
int count(); // 与 s 连通的顶点总数
}
深度优先搜索:
// 深度优先搜索
// 在访问一个顶点时:
// * 将它标记为已访问
// * 递归地访问它的所有没有被标记过的邻居顶点
public class DepthFirstSearch implements Search {
private boolean[] marked;
private int count;
// s:起点
public DepthFirstSearch(Graph G, int s) {
marked = new boolean[G.V()];
count = 0;
dfs(G, s);
}
private void dfs(Graph G, int v) {
marked[v] = true;
count++;
for (int w : G.adj(v)) {
if (!marked[w]) {
dfs(G, w);
}
}
}
@Override
public boolean marked(int v) {
return marked[v];
}
@Override
public int count() {
return count;
}
}
单点路径
单点路径 API:
public class Paths {
Paths(Graph G, int s); // 在 G 中找出所有起点为 s 的路径
boolean hasPathTo(int v); // 是否存在从 s 到 v 的路径
Iterable<Integer> pathTo(int v); // s 到 v 的路径,如果不存在则返回 null
}
深度优先搜索:
// 使用深度优先搜索解决单点路径问题
public class DepthFirstPaths implements Paths {
private boolean[] marked;
private int[] pathTo;
private int s;
// s: 起点
public DepthFirstPaths(Graph G, int s) {
marked = new boolean[G.V()];
pathTo = new int[G.V()];
this.s = s;
dfs(G, s);
}
private void dfs(Graph G, int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w]) {
pathTo[w] = v;
dfs(G, w);
}
}
}
// 是否存在 s 到 v 的路径
@Override
public boolean hasPathTo(int v) {
return marked[v];
}
// 返回 s 到 v 的路径,如果不存在返回 null
@Override
public Iterable<Integer> pathTo(int v) {
if (!hasPathTo(v)) {
return null;
}
Stack<Integer> stack = new Stack<>();
while (v != s) {
stack.push(v);
v = pathTo[v];
}
stack.push(s);
return stack;
}
}
广度优先搜索(单点最短路径):
public class BreadthFirstPaths implements Paths {
private boolean[] marked;
private int[] pathTo;
private int s;
public BreadthFirstPaths(Graph G, int s) {
marked = new boolean[G.V()];
pathTo = new int[G.V()];
this.s = s;
bfs(G, s);
}
private void bfs(Graph G, int s) {
Queue<Integer> queue = new Queue<>();
queue.add(s);
marked[s] = true;
while (!queue.isEmpty()) {
int v = queue.remove();
for (int w : G.adj(v)) {
if (!marked[w]) {
pathTo[w] = v;
queue.add(w);
marked[w] = true;
}
}
}
}
@Override
public boolean hasPathTo(int v) {
return marked[v];
}
@Override
public Iterable<Integer> pathTo(int v) {
if (!hasPathTo(v)) {
return null;
}
Stack<Integer> stack = new Stack<>();
while (v != s) {
stack.push(v);
v = pathTo[v];
}
stack.push(s);
return stack;
}
}
连通分量
很多用例都需要能够独立地处理每个连通分量。
连通分量 API:
public class CC {
CC(Graph G); // 预处理构造函数
boolean connected(int v, int w); // v 和 w 连通吗
int count(); // 连通分量数量
int id(int v); // v 所在的连通分量的标识符 (0 - count() - 1)
}
深度优先搜索:
public class CC {
private boolean[] marked;
private int[] ids;
private int count;
public CC(Graph G) {
marked = new boolean[G.V()];
ids = new int[G.V()];
count = 0;
for (int v = 0; v < G.V(); v++) {
if (!marked[v]) {
dfs(G, v, count);
count++;
}
}
}
private void dfs(Graph G, int v, int id) {
marked[v] = true;
ids[v] = id;
for (int w : G.adj(v)) {
if (!marked[w]) {
dfs(G, w, id);
}
}
}
public boolean connected(int v, int w) {
return ids[v] == ids[w];
}
public int count() {
return count;
}
public int id(int v) {
return ids[v];
}
}