图(Graph)
大约 2 分钟
todo https://www.youtube.com/watch?v=B5hxqxBL2d0
图的定义
图定义:
- 元素
- 图
G(V,E)
- 边
E
(Edge) - 节点
V
(Vertex)
- 图
- 有向(Directed)/无向(UnDirected)
- 有权(Weighted)/无权(UnWeighted)
概念:
- 桥/割边 —— 移除 “边” 后会增加连通分量的数量(白话:移除后会导致一个图变成两个图)
- 关节点/割点 —— 移除 “点” 后会增加连通分量的数量
图的问题
https://www.bilibili.com/video/BV1Cq421c7uL/
图的表示
关键词:邻接
表示:
邻接表(Adjacency List)
Vertex Neighbors A B B A,C C B 邻接矩阵(Adjacency Matrix)
问题:
- 拓扑排序 —— 有向无环图/拓扑序列
- 最短路径
邻接表
package org.example.test1;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
abstract class ListGraph {
List<List<Integer>> graphs;
public ListGraph(int size) {
graphs = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
graphs.add(new ArrayList<>());
}
}
public void addEdge(int start, int end) {
graphs.get(start).add(end);
}
public void removeEdge(int start, int end) {
graphs.get(start).remove(end);
}
abstract void traversal(Consumer<Integer> func);
}
问题:图遍历(Graph Traversal)
- 深度优先(DFS,Depth First Search)
- 广度优先(BFS,Breadth First Search)
DFS
package org.example.test1;
import java.util.LinkedList;
import java.util.Queue;
import java.util.function.Consumer;
public class DfsListGraph extends ListGraph {
public DfsListGraph(int v) {
super(v);
}
@Override
public void traversal(Consumer<Integer> func) {
DfsTraversal traversal = new DfsTraversal(this);
for (int i = 0; i < graphs.size(); i++) {
traversal.traversal(i, func);
}
}
/**
* 深度优先遍历
*/
private static class DfsTraversal {
ListGraph graph;
boolean[] visited;
// Deque<Integer> queue = new ArrayDeque<>(); // 数组,方便中间增删
Queue<Integer> queue = new LinkedList<>(); // 链表,方便前后增删
DfsTraversal(ListGraph graph) {
this.graph = graph;
visited = new boolean[graph.graphs.size()];
}
void traversal(int v, Consumer<Integer> func) {
offer(v);
while (!queue.isEmpty()) {
func.accept(queue.poll());
// add
graph.graphs.get(v).forEach(neighbor -> {
offer(v);
});
}
}
private void offer(int v) {
if (!visited[v]) {
queue.offer(v);
visited[v] = true;
}
}
}
}
BFS
package org.example.test1;
import java.util.function.Consumer;
public class BfsListGraph extends ListGraph {
public BfsListGraph(int size) {
super(size);
}
@Override
void traversal(Consumer<Integer> func) {
BfsTraversal traversal = new BfsTraversal(this);
for (int i = 0; i < graphs.size(); i++) {
traversal.traversal(i, func);
}
}
/**
* 广度优先遍历
*/
private static class BfsTraversal {
ListGraph graph;
boolean[] visited;
BfsTraversal(ListGraph graph) {
this.graph = graph;
visited = new boolean[graph.graphs.size()];
}
void traversal(int v, Consumer<Integer> func) {
if (visited[v]) {
return;
}
visited[v] = true;
func.accept(v);
// next
graph.graphs.get(v).forEach(neighbor -> {
traversal(neighbor, func);
});
}
}
}
遍历测试用例
package org.example.test1;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class ListGraphTravelTest {
@Test
void testBfs() {
ListGraph listGraph = new BfsListGraph(10);
for (int i = 1; i < listGraph.graphs.size(); i++) {
listGraph.addEdge(i-1, i);
}
listGraph.addEdge(2, 8);
listGraph.addEdge(3, 6);
//
assertTravel(listGraph);
}
@Test
void testDfs() {
ListGraph listGraph = new DfsListGraph(10);
for (int i = 1; i < listGraph.graphs.size(); i++) {
listGraph.addEdge(0, i);
}
listGraph.addEdge(2, 6);
listGraph.addEdge(6, 3);
listGraph.addEdge(6, 9);
//
assertTravel(listGraph);
}
private static void assertTravel(ListGraph listGraph) {
AtomicInteger count = new AtomicInteger();
listGraph.traversal(v -> {
log.info("{} cur: {}", listGraph.getClass().getName(), v);
count.getAndIncrement();
});
Assertions.assertEquals(listGraph.graphs.size(), count.get());
}
}
问题:判断连通性
解决:遍历
问题:最短路径(Shortest Path)
两个点在图中的最短连线
算法:
- BFS(无权图)
- 迪杰斯特拉算法
- 贝尔曼-福特算法
- 弗洛伊德-沃舍尔算法
A*
算法
问题:判断回路
问题:判断负权回路
算法:
- 贝尔曼-福特算法(
Bellman-Ford
) - 弗洛伊德-沃舍尔算法(
Floyd-Warshall
)
问题:最小生成树(MST,minimum spanning tree)
可选算法
- 普里姆(Prim) Native implementation
O(V2)
稠密图 - 普里姆(Prim) PQ implementation
O(ElogV)
稀疏图 - 克鲁斯卡尔(Kruskal) UF implementation
O(ElogE)
(更大范围ElogV, E<=V2
) 稀疏图 - 博鲁夫卡(Boruvka)算法
The Java API does not provide a Fibonacci heap. The
java.util.PriorityQueue
is a binary heap.
Prim 算法
有优先级的 DFS
问题:强连通分量
强连通分量(SCCs)可以被视为有向图中的自包含循环,其中给定循环中的每个顶点都能到达同一循环中的其他顶点。
算法:
- 塔扬算法(Tarjan)
- 科萨拉朱算法(Kosaraju)
问题:旅行商问题
给定一个城市列表和每对城市之间的距离,求访问每个城市恰好一次且返回原始城市的最短路径。
算法:
- 赫尔德-卡普算法(Held-Karp)
- 分支定界算法(Branch and Bound)
TSP 问题是 NP 难问题
问题:流网络最大流
图中 “源点” 和 “汇点” 之间,最大可连通流量。
算法:
- 福特-富尔克森算法(Ford-Fulkerson)
- 埃德蒙兹-卡普算法(Edmonds-Karp)
- 迪尼克算法(Dinic)