图的基本概念 基本概念
顶点
顶点的度
边
相邻
环
完全图: 所有顶都相邻
二分图: 对于一个图G, V(G)=X∪Y, X∩Y=∅, X中, Y 中任两顶不相邻
路径
圈
图的表示方式 两种,紧接矩阵和邻接表,如下。
邻接矩阵 邻接表 图的相关问题 其实对于很多非图的问题,也是可以把其看做图的,使用图遍历的方式其进行遍历。例如 LeetCode 138. Copy List with Random Pointer ,以图遍历的方式遍历链表;又或者是jump game III,将数组用图遍历的方式展现出来。
图的搜索 BFS 用Queue来遍历吧,先写个样例,以邻接表为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public void bfs (int [][] edges, int n) { ArrayList<Integer>[] graph = new ArrayList[n + 1 ]; for (int i = 1 ; i < n + 1 ; i++) { graph[i] = new ArrayList(); } for (int [] edge : edges) { graph[edge[0 ]].add(edge[1 ]); graph[edge[1 ]].add(edge[0 ]); } boolean [] visited = new boolean [n + 1 ]; Queue<Integer> queue = new LinkedList(); queue.add(1 ); isVisited[1 ] = true ; while (!queue.isEmpty()) { int node = queue.pop(); for (int i = 0 ; i < graph[node].size(); i++) { if (!isVisited[graph[node].get(i)]) { doSomething(); isVisited[graph[node].get(i)] = true ; queue.offer(graph[node.get[i]]); } } } return ; }
DFS 一般情况下是递归调用DFS较多。而且需要注意的是!!!
Attention: DFS遍历时也要注意preorder还是postorder遍历(有向图的情况下要注意),前者是先遍历当前节点,再去遍历neighbor;后者是先遍历neighbor,再遍历自身。为什么无向图不需要考虑postorder的可能呢?因为无向图都是双向的,如果一个无向图存在环,那么postorder就会引起无限循环,所以对于无向图,都是preorder。
那么对于有向图的postorder遍历有什么区别呢?如果都是在遍历完neighbor以后才将其置为visited的话,那么一旦存在一个有向环,就会无限循环的,和postorder一个道理,跟无向图没有postorder遍历一个道理。因此,我们就需要设置三种状态,unvisited,visiting和visited,当遇到unvisited的时候,我们继续DFS;发现visiting节点,则说明存在环;发现visited节点,则返回,因为已经遍历过了。
下面遍历方式都是对于无向图,有向图的遍历参考:LeetCode 207,最经典的就是拓扑排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public void testDFS (int [][] graph) { boolean [] isVisited = new int [graph.length]; Arrays.fill(isVisited, false ); for (int i = 0 ; i < graph.length; i++) { if (isVisited[i] == false ) { isVisited[i] = true ; dfs(graph, i, isVisited); } } } private void dfs (int [][] graph, int vertex, boolean isVisited) { for (int i = 0 ; i < graph[i].length; i++) { if (isVisited[graph[vertex][i]] == false ) { isVisited[graph[vertex][i]] = true ; dfs(graph, graph[vertex][i], isVisited); } else if (isVisited[graph[vertex][i]] == true ) { doSomething(); } } }
二分图 二分图的问题本质上是相当于染色问题,有点像红黑树的感觉。如果能够顺利将整个图染成2种不同颜色,则是二分图,否则不是。实现可以用BFS或DFS均可,下面是三个例题链接。
LeetCode-785-Is-Graph-Bipartite
LeetCode 886. Possible Bipartition
LeetCode-1042-Flower-Planting-With-No-Adjacent
深拷贝图
LeetCode 138. Copy List with Random Pointer
LeetCode 133. Clone Graph
拓扑排序 判断环 最小生成树 最小生成树是无向图里的概念!!!
Kruskal Algorithm 其核心思想在于按边查找。
首先将所有的边从小到大进行排序;
依次从前到后遍历所有的边,如果该条边不会使得已有的边形成“环”,加入这条边到MST里;否则,跳过;
直到有n-1条边被选出或者无边可选, MST生成。
O(T) = O(ElogE)
O(S) = O(V)
例题:LeetCode 1584
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Solution { class UF { int size; int [] parent; int [] weight; public UF (int n) { size = n; parent = new int [n]; weight = new int [n]; for (int i = 0 ; i < n; i++) { parent[i] = i; weight[i] = 1 ; } } public int find (int x) { if (x == parent[x]) { return x; } parent[x] = find(parent[x]); return parent[x]; } public void union (int x, int y) { int rootX = find(x), rootY = find(y); if (rootX == rootY) { return ; } if (weight[rootX] <= weight[rootY]) { parent[rootX] = rootY; weight[rootY] += weight[rootX]; } else { parent[rootY] = rootX; weight[rootX] += weight[rootY]; } size--; } } class Edge { int x, y, l; public Edge (int _x, int _y, int _l) { x = _x; y = _y; l = _l; } } public int minCostConnectPoints (int [][] points) { int n = points.length; PriorityQueue<Edge> pq = new PriorityQueue<>((a, b) -> a.l - b.l); for (int i = 0 ; i < n; i++) { for (int j = i + 1 ; j < n; j++) { Edge e = new Edge(i, j, Math.abs(points[i][0 ] - points[j][0 ]) + Math.abs(points[i][1 ] - points[j][1 ])); pq.add(e); } } UF uf = new UF(n); int cnt = 0 , res = 0 ; while (cnt < n - 1 ) { Edge e = pq.poll(); if (uf.find(e.x) == uf.find(e.y)) { continue ; } uf.union(e.x, e.y); res += e.l; cnt++; } return res; } }
Prim Algorithm 按顶点来进行查找。
将顶点分为两个set,visited和unvisited;
visited中初始化放入一个顶点,剩下的则放入unvisited中;
每次找到一条从visited到unvisited的最短的边,将其加入MST,并将该顶点加入MST(该顶点一定在unvisited中,因为边是从visited到unvisited的);
重复3,知道全部顶点已经加入visited或者无边可加。
单源最短路径 Dijkstra LeetCode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution { public int networkDelayTime (int [][] times, int n, int k) { int [] dists = new int [n + 1 ]; Arrays.fill(dists, Integer.MAX_VALUE); Map<Integer, List<int []>> graph = new HashMap<>(); dists[k] = 0 ; for (int [] edge : times) { graph.putIfAbsent(edge[0 ], new ArrayList<int []>()); graph.get(edge[0 ]).add(new int []{edge[1 ], edge[2 ]}); } boolean [] visited = new boolean [n + 1 ]; while (true ) { int candidateId = -1 , minD = Integer.MAX_VALUE; for (int i = 1 ; i <= n; i++) { if (!visited[i] && dists[i] < minD) { candidateId = i; minD = dists[i]; } } if (candidateId < 0 ) break ; visited[candidateId] = true ; if (graph.containsKey(candidateId)) { for (int [] nei : graph.get(candidateId)) { dists[nei[0 ]] = Math.min(dists[candidateId] + nei[1 ], dists[nei[0 ]]); } } } int res = 0 ; for (int i = 1 ; i <= n; i++) { if (dists[i] == Integer.MAX_VALUE) { return -1 ; } res = Math.max(res, dists[i]); } return res; } }
堆优化版Dijkstra 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 class Solution { public int networkDelayTime (int [][] times, int n, int k) { Map<Integer, List<int []>> graph = new HashMap<>(); for (int [] edge : times) { graph.putIfAbsent(edge[0 ], new ArrayList<>()); graph.get(edge[0 ]).add(new int []{edge[1 ], edge[2 ]}); } int [] dist = new int [n + 1 ]; Arrays.fill(dist, Integer.MAX_VALUE); dist[k] = 0 ; boolean [] seen = new boolean [n + 1 ]; PriorityQueue<int []> pq = new PriorityQueue<>((a, b) -> (a[1 ] - b[1 ])); pq.add(new int []{k, 0 }); while (!pq.isEmpty()) { int [] cand = pq.poll(); int candidate = cand[0 ], minD = cand[1 ]; if (seen[candidate]) { continue ; } dist[candidate] = minD; seen[candidate] = true ; if (graph.containsKey(candidate)) { for (int [] nei : graph.get(candidate)) { if (!seen[nei[0 ]]) pq.offer(new int []{nei[0 ], nei[1 ] + minD}); } } } int res = 0 ; for (int i = 1 ; i <= n; i++) { if (dist[i] == Integer.MAX_VALUE) { return -1 ; } res = Math.max(dist[i], res); } return res; } }
最短路径问题 欧拉路径 最大流