无向图的遍历
DFS
- 算法思想
深度优先搜索思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
- 算法特点:栈
深度优先搜索是一个递归的过程。首先,选定一个出发点后进行遍历,如果有邻接的未被访问过的节点则继续前进。若不能继续前进,则回退一步再前进,若回退一步仍然不能前进,则连续回退至可以前进的位置为止。重复此过程,直到所有与选定点相通的所有顶点都被遍历。
深度优先搜索是递归过程,带有回退操作,因此需要使用栈
存储访问的路径信息。当访问到的当前顶点没有可以前进的邻接顶点时,需要进行出栈操作,将当前位置回退至出栈元素位置,直到栈为空。
BFS
- 算法思想
广度优先搜索思想:从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。
- 算法特点: 队列
广度优先搜索类似于树的层次遍历,是按照一种由近及远的方式访问图的顶点。在进行广度优先搜索时需要使用队列
存储顶点信息,每出队一个顶点,则将它的所有邻接的未入队的点放入队列,直到队列为空。
最小生成树
- 生成树是加权图中包含所有顶点的无环连通子图,显然生成树只存在于连通图中,最小生成树(MST)是权值最小的生成树;
- Prim 和 Kruskal 只能用于无向加权图中,用于生成MST,当存在相同权重的边时,MST可能不唯一。
Prim算法
Prim算法的基本思想是贪心算法,通过不断搜索最小横切边实现,算法实现的关键是用“优先队列”这种数据结构来表示横切边,其中又分为以下两种:
1. 延时实现 – 优先队列
2. 即时实现 – 索引优先队列
Kruskal算法
Kruskal算法的基本思想是先对边按照权值升序排序,每次取出最短的(借助于优先队列)然后借助UF算法
不断加边并合并,直至形成生成树;E条边算法时间复杂度为ElogE
实现
/*
加权无向图,示例和代码不做特殊说明都是在连通图下,不适用于包含多个极大连通子图的图
*/
import java.util.*;
/**
* 无向图的遍历
*/
public class UndirectedGraph {
// 这里为了简单,用0开始连续的数字代表顶点(顶点ID),实际的实现中应该设计成类 Node 的形式
private final int V; // 顶点数目
private int E; // 边的数目
private boolean[] marked; // 用于遍历时标记用
private HashMap<Integer, TreeSet<Edge>> adj; // 邻接表用Hash表实现,key=Node_ID, value=相邻的边组成的链表(这里的实现是红黑二叉树)
public UndirectedGraph(int v) {
this.V = v;
this.E = 0;
this.adj = new HashMap<>();
// 初始化哈希表
for (int i = 0; i < V; i++) {
adj.put(i,new TreeSet<>());
}
}
public int getV() {
return V;
}
public int getE() {
return E;
}
public void addEdge(Edge e) {
int from = e.from;
int to = e.to;
adj.get(from).add(e);
adj.get(to).add(e);
E++;
}
// 返回 v 点所有的连接边,Iterable<Edge>方便遍历
Iterable<Edge> adj(int v) {
return adj.get(v);
}
// 返回加权无向图中所有的边
public Iterable<Edge> edges() {
TreeSet<Edge> edges = new TreeSet<>();
for (Map.Entry<Integer,TreeSet<Edge>> entry : adj.entrySet())
edges.addAll(entry.getValue());
return edges;
}
/*
无向图的连通性
*/
// DFS
public Queue<Integer> depthFirstSearch() {
marked = new boolean[V]; // 每次遍历之前懒惰初始化,每次都保证初始全是false
LinkedList<Integer> outQueue = new LinkedList<>(); // 这个队列是为了方便输出
dfs(0, outQueue);
return outQueue;
}
private void dfs(int root, LinkedList<Integer> outQueue) {
outQueue.offer(root); // offer <-> poll
marked[root] = true;
for (Edge edge : this.adj(root)) {
int other = edge.getOther(root);
if (!marked[other])
dfs(other, outQueue);
}
}
// BFS
public Queue<Integer> breadthFirstSearch() {
marked = new boolean[V]; // 每次遍历之前懒惰初始化,每次都保证初始全是false
LinkedList<Integer> outQueue = new LinkedList<>(); // 这个队列是为了方便输出
LinkedList<Integer> levelQueue = new LinkedList<>(); // 这个队列为了执行层次遍历逻辑
levelQueue.offer(0); // 加入起点
marked[0] = true; // 标记起点为已入队点
while (!levelQueue.isEmpty()) {
Integer node = levelQueue.pop();
outQueue.offer(node); // 从层次队列中删除,并添加到输出队列中
for (Edge edge : this.adj(node)) {
int other = edge.getOther(node);
if (!marked[other]) {
levelQueue.offer(other); // 加入层次队列
marked[other] = true; // 标记已入队点
}
}
}
return outQueue;
}
@Override
public String toString() {
return "UndirectedGraph{" +
"V=" + V +
", E=" + E +
", adj=" + adj +
'}';
}
}
class Edge implements Comparable<Edge> {
int from;
int to;
double weight;
public Edge(int from, int to, double weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
public int getFrom() {
return from;
}
public int getTo() {
return to;
}
public double getWeight() {
return weight;
}
public int getOther(int vertex) {
if (vertex == from)
return to;
else if (vertex == to)
return from;
else
throw new RuntimeException("Inconsistent edge");
}
// 这里定义大小关系,平行边定义为相等,因为前面用的set存邻接边,即不允许添加平行边
@Override
public int compareTo(Edge that) {
if (this.from == that.from && this.to == that.to)
return 0;
else if (this.weight < that.weight)
return -1;
else if (this.weight > that.weight)
return 1;
else
return 1;
}
@Override
public String toString() {
return "Edge{" +
"from=" + from +
", to=" + to +
", weight=" + weight +
'}';
}
}
/**
* 最小生成树
*/
class GeneratMST {
/*
Prim算法的基本思想是贪心算法,通过不断搜索最小横切边实现,算法实现的关键是用“优先队列”这种数据结构来表示横切边,
其中又分为两种,<1>优先队列 --> 延时实现,
<2>索引优先队列 --> 即时实现。
*/
// 1. 延时实现:E条边的时间复杂度为ElogE
private static boolean[] marked; // 顶点标记
private static PriorityQueue<Edge> pq; // 优先队列
public static UndirectedGraph lazyPrimMST(UndirectedGraph graph) {
UndirectedGraph mst = new UndirectedGraph(graph.getV());
marked = new boolean[graph.getV()];
pq = new PriorityQueue<>();
lazyVisit(graph,0); // 从0开始,将0邻接边加入优先队列
while (!pq.isEmpty()) {
Edge minEdge = pq.remove(); // 得到最小边并从pq中删除
int from = minEdge.from;
int to = minEdge.to;
if (marked[from] && marked[to])
continue; // 边已失效,跳过
mst.addEdge(minEdge); // 否则将边加入到mst中
if (!marked[from]) // 并且将没被访问过顶点的邻接边加入pq
lazyVisit(graph,from);
if (!marked[to])
lazyVisit(graph,to);
}
return mst;
}
// 工具函数:标记顶点v,并将所有连接v的横切边且未访问的边加入优先队列
private static void lazyVisit(UndirectedGraph graph, int v) {
marked[v] = true;
for (Edge edge : graph.adj(v))
if (!marked[edge.getOther(v)])
pq.add(edge);
}
// 2. 即时实现:关键在于对邻接边的处理,我们不需要将所有邻接边都存进pq,只需要存最短的那条,那就需要在遍历邻接边时不断地对优先队列进行插入和更新;
// 而优先队列只能对堆顶元素进行删除,索引优先队列才支持对任意元素的更新,所以要用到索引优先队列;V个顶点,E条边,时间复杂度为ElogV
private static Edge[] edgeTo; // 如果v不在树中但至少有一条边和树相连,那么edgeTo[v]代表将v和树相连的最短边
private static double[] distTo; // distTo[v]是edgeTo[v]的权重,即树到v之间最短边的权值
// marked 同上
private static IndexMinPriorityQueue<Double> indexPq; // 索引优先队列,里面存储的是树 到 非树节点 最短边的权重
public static UndirectedGraph primMST(UndirectedGraph graph) {
UndirectedGraph mst = new UndirectedGraph(graph.getV());
edgeTo = new Edge[graph.getV()];
distTo = new double[graph.getV()];
marked = new boolean[graph.getV()];
indexPq = new IndexMinPriorityQueue<>(graph.getV());
for (int v = 0; v < graph.getV(); v++)
distTo[v] = Double.MAX_VALUE; // 先将最短距离置为最大值
distTo[0] = 0.0; // 设置树的起点,即第0点一直在树上
indexPq.insert(0 ,0.0); // 用顶点0和权重0初始化indexPq
while (!indexPq.isEmpty())
visit(graph,indexPq.delMin());
// 最后edgeTo数组(索引0不存储任何东西)存储的就是mst上的所有边
for (int i = 1; i < graph.getV(); i++)
mst.addEdge(edgeTo[i]);
return mst;
}
// 工具函数:标记顶点v,并更新索引优先队列
private static void visit(UndirectedGraph graph, int v) {
marked[v] = true; // 标记为已访问
for (Edge edge : graph.adj(v)) { // 遍历所有邻接边
int w = edge.getOther(v);
if (marked[w])
continue; // 跳过失效边
if (edge.getWeight() < distTo[w]) { // 找到树 到 非树上的点 w 更短的边,更新或添加
edgeTo[w] = edge;
distTo[w] = edge.getWeight();
if (indexPq.contains(w))
indexPq.change(w,distTo[w]); // 更新
else
indexPq.insert(w,distTo[w]); // 添加
}
}
}
/*
Kruskal算法的基本思想是先对边按照权值升序排序,每次取出最短的(借助于优先队列)然后不断加边并合并,直至形成生成树;E条边算法时间复杂度为ElogE
*/
// kruskal
public static UndirectedGraph kruskalMST(UndirectedGraph graph) {
UndirectedGraph mst = new UndirectedGraph(graph.getV());
PriorityQueue<Edge> pq = new PriorityQueue<>();
for (Edge edge : graph.edges())
pq.add(edge);
UnionFind uf = new UnionFind(graph.getV());
while (mst.getE() < (graph.getV()-1) && !pq.isEmpty()) {
Edge e = pq.remove(); // 从pq中得到权值最小的边
int from = e.from; int to = e.to;
if (uf.connected(from,to)) // 同根,即已经在同一棵树上
continue;
uf.union(from,to); // 合并两个子树
mst.addEdge(e); // 添加到mst
}
return mst;
}
}
// 测试类
class TestUndirectedGraph {
// test UndirectedGraph
public static void main(String[] args) {
// 测试用例参考:https://zhuanlan.zhihu.com/p/33162490
UndirectedGraph undirectedGraph = new UndirectedGraph(6); // 添加6个点,ID:0-5
undirectedGraph.addEdge(new Edge(0,1,10));
undirectedGraph.addEdge(new Edge(0,3,30));
undirectedGraph.addEdge(new Edge(0,4,100));
undirectedGraph.addEdge(new Edge(1,2,50));
undirectedGraph.addEdge(new Edge(2,3,20));
undirectedGraph.addEdge(new Edge(2,4,10));
undirectedGraph.addEdge(new Edge(3,4,60));
undirectedGraph.addEdge(new Edge(2,5,10));
// System.out.println(undirectedGraph);
// test dfs and bfs
System.out.println(undirectedGraph.depthFirstSearch());
System.out.println(undirectedGraph.breadthFirstSearch());
// test prim
UndirectedGraph lazyPrimMST = GeneratMST.lazyPrimMST(undirectedGraph);
System.out.println(lazyPrimMST);
UndirectedGraph MST = GeneratMST.primMST(undirectedGraph);
System.out.println(MST);
// test kruskal
UndirectedGraph kruskalMST = GeneratMST.kruskalMST(undirectedGraph);
System.out.println(kruskalMST);
}
}
// 工具类
/**
* 索引优先队列
* @param <T>
*/
class IndexMinPriorityQueue<T extends Comparable<T>> {
private T[] elements;
private int[] indexPq;
private int[] reIndexQp;
private int N = 0;
public IndexMinPriorityQueue(int maxN) {
elements = (T[]) new Comparable[maxN + 1]; // elements这里可以不加1,没影响,但可能有其他用途
indexPq = new int[maxN + 1];
reIndexQp = new int[maxN +1];
for (int i = 0; i <= maxN; i++)
reIndexQp[i] = -1;
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public boolean contains(int k) {
return reIndexQp[k] != -1;
}
// 插入:在k位置插入元素,位置k并不代表任何含义,只是存储在elements数组的索引位置;注意:这里k可以从索引0开始,没有任何影响
public void insert(int k, T value) {
N++;
elements[k] = value; // 放入索引k
indexPq[N] = k; // 记录此元素所在索引位置(k)
reIndexQp[k] = N; // 记录indexPq数组中哪个位置(N)存储着此元素的索引
swim(N); // 从堆底加入并上浮,维护indexPq 和 reIndexQp
}
public T min() {
return elements[indexPq[1]];
}
public int minIndex() {
return indexPq[1];
}
// 删除最小值,并返回其索引
public int delMin() {
int indexOfMax = indexPq[1];
if (elements[indexOfMax] == null) // 已为空,返回-1
return -1;
exch(1,N--); // 把最后一个元素(最小元素)放在顶端,然后N--(堆的大小-1)
sink(1); // 让“最后”一个元素下沉
elements[indexPq[N+1]] = null; // 将垃圾(删除的最小值)清空
reIndexQp[indexPq[N+1]] = -1; // 更新对应reIndexQp为-1
indexPq[N+1] = 0; // 更新最后一位删除的indexPq为0
return indexOfMax;
}
// 删除索引k位置的元素,与删除最小值类似
public void delete(int k) {
int indexOfPq = reIndexQp[k];
exch(indexOfPq,N--);
swim(indexOfPq);
sink(indexOfPq);
elements[k] = null;
reIndexQp[k] = -1;
indexPq[N+1] = 0;
}
// 更新值
public void change(int k, T newValue) {
elements[k] = newValue;
// 更新值后,可能出现三种情况:
// 1. 比父节点小:需要上浮
// 2. 比子节点大:需要下沉
// 3. 大小在父节点和子节点之间:不执行任何操作
// 所以此处采取的策略是先上浮在下沉(或先下沉再上浮)
swim(reIndexQp[k]); // 上浮
sink(reIndexQp[k]); // 下沉
}
// 用于堆实现的比较方法:这里怎么设计关乎着是大堆顶(<0)还是小堆顶(>0)
private boolean greater(int i, int j) {
return elements[indexPq[i]].compareTo(elements[indexPq[j]]) > 0;
}
// 用于堆实现的交换方法:交换indexPq[i]、indexPq[j] 和 reIndexPq[i]、reIndexPq[j]
private void exch(int i, int j) {
int tempPq = indexPq[i];
indexPq[i] = indexPq[j];
indexPq[j] = tempPq;
reIndexQp[indexPq[i]] = i;
reIndexQp[indexPq[j]] = j;
}
// 上浮
private void swim(int k) {
while(k > 1 && greater(k/2,k)) {
exch(k/2,k); // k/2默认向下取整
k = k/2;
}
}
// 下沉
private void sink(int k) {
while (2*k <= N) {
int j = 2*k;
if (j < N && greater(j,j+1)) // 找到较小的子节点,并将j指向它
j++;
if (!greater(k,j)) // 此时j一定指向较小的子节点,如果elements[indexPq[k]] <= elements[indexPq[j]],则下沉结束
break;
exch(k,j); // 如果没有break则说明elements[indexPq[k]] > elements[indexPq[j]],交换indexPq 和 reIndexQp
k = j; // 交换k、j,让k始终指向下沉的元素
}
}
@Override
public String toString() {
return " indexPq " +
Arrays.toString(indexPq) + "\n" +
" reIndexQp " +
Arrays.toString(reIndexQp) + "\n" +
"PriorityQueue " +
Arrays.toString(elements);
}
}
/**
* Union-find算法之加权quick-union的变种
*/
class UnionFind {
private int[] parent;
private byte[] rank;
private int count;
public UnionFind(int n) {
if (n < 0) throw new IllegalArgumentException();
count = n;
parent = new int[n];
rank = new byte[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public int find(int p) {
validate(p);
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
public int count() {
return count;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
if (rank[rootP] < rank[rootQ]) parent[rootP] = rootQ;
else if (rank[rootP] > rank[rootQ]) parent[rootQ] = rootP;
else {
parent[rootQ] = rootP;
rank[rootP]++;
}
count--;
}
private void validate(int p) {
int n = parent.length;
if (p < 0 || p >= n) {
throw new IllegalArgumentException("index " + p + " is not between 0 and " + (n-1));
}
}
}