1.图的部分考察最多的就是Topological sort
2.其次是经典的BFS,DFS
3.然后是Union Find,这部分因为有板子反而是比较简单的
4.Dijkstra很多时候被用来当follow up
Graph的表示:邻接表,邻接矩阵,list of edges需要自己处理
一.有向图/无向图(找环)
①无向图找环?
1.DFS + parent node
2.Union Find
3.拓扑也可以,无向图改有向,完全不建议用在无向图
②有向图找环?
1.使用DFS + 双array标记,是visited set和recursion array
我们知道有环的图无法做拓扑排序
2.dfs三色法拓扑,也就是dfs版本的拓扑
3.bfs拓扑是否能遍历所有点,还有剩下的点的时候有环
总结
1.无向图Union Find最简单如果有板子,dfs with parent方法其次算标准解法
2.有向图双visited array方法很简便,当然熟练使用拓扑排序的也可以用拓扑板子
3.有向图双visited array还方便统计环的大小和具体node value,加一个prev[]来保存parent
4.以上解法时间复杂度均为线性O(n) = O(V + E)
模板
1.无向图
Map<Integer,List<Integer>> gragh = new HashMap<>();
private boolean isCyclicUndirectedGragh(int n) {
boolean[] visited = new boolean[n];
for (int i = 0;i < n;i++) {
if (!visited[i])
if (isCyclicUtil(i,visited,parent: -1)
return true;
}
return false;
}
private boolean isCyclicUtil(int cur,boolean[] visited,int parent) {
visited[cur] = true;
for (int nei :gragh.getOrDefault(cur,new ArrayList<>())) {
if (!visited[nei]) {
if(isCyclicUtil(nei,visited,cur)) return true;
} else {
if (nei != parent) return true;
}
}
return false;
}
2.有向图
private boolean isCyclicDirectedGragh(int n) {
boolean[] visited = new boolean[n];
boolean[] recStack = new boolean[n];
for (int i = 0;i < n;i++) {
if (!visited[i])
if (isCyclicUtil(i,visited,recStack))
return true;
}
return false;
}
private boolean isCyclicUtil(int cur,boolean[] visited,boolean[] recStack) {
if (recStack[cur]) return true;
if (visited[cur]) return false;
visited[cur] = true;
recStack[cur] = true;
for (int nei : gragh.getOrDefault(cur,new ArrayList<>()))
if (isCyclicUtil(nei,visited,recStack)) return true;
recStack[cur] = false;
return false;
}
二.单元最短路径
SSSP:Single Source Shortest Path Dijkstra Bellman-Ford
典型的单元最短路径算法,用于计算一个节点到其他所有节点的最短路径
主要特点是以起始点为中心向外层扩展,知道扩展到终点为止
//Dijkstra,用pq来找最低路径值,k步之内,谁先到终点谁就是最优解
//T(n) = O(E + nlogn),where E is the total number of flights
public int findCheapestPrice(int n,int[][] flights,int src,int dst,int k) {
Map<Integer,Map<Integer,Integer>> prices = new HashMap<>();
for (int[] f : flights) prices.computeIfAbsent(f[0],value -> new HashMap<>()).put(f[1],f[2]);
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[0] - b[0]);
pq.add(new int[] {0,src,k+1});//{cost,city,step)
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int price = cur[0],city = cur[1],stops = cur[2];
if (city == dst) return price;
if (stops > 0) {
Map<Integer,Integer> neighborsPrice = prices.getOrDefault(city,new HashMap<>());
for (int nei : neighborsPrice.keySet())
pq.add(new int[] {price + neighborsPrice.get(nei),nei,stops - 1});
}
}
return -1;
}
Bellman-Ford算法
Bellman-Ford算法是一种用于计算带权有向图中单元最短路径的算法
流程如下:
1.创建源顶点 v 到图中所有顶点的距离的集合distSet,为图中所有顶点指定一个距离值,初始均为Infinite,源顶点距离为0
2.计算最短路径,执行 V - 1 次遍历:对于图中的每条边,如果起点 u 的距离 d 加上边的权值 w 小于终点 v 的距离 d,则更新终点 v 的距离值 d
3.检测图中是否有负权边形成了环,遍历图中的所有边,计算 u 至 v 的距离,如果对于 v 存在更小的距离,则说明存在环
Bellman-Ford算法 VS Dijkstra算法
Bellman-Ford算法和Dijkstra算法同为解决单源最短路径的算法。对于带权有向图 G = (V , E),Dijkstra算法要求图G中边的权值均为非负,而Bellman-Ford算法能适应一般的情况(即存在负权边的情况)。一个实现的很好的Dijkstra算法比Bellman-Ford算法的运行时间要低
Bellman-Ford算法采用动态规划进行设计,实现的时间复杂度为O(V*E),其中V为顶点数量,E为边的数量。Dijkstra算法采用贪心算法范式进行设计,普通实现的时间复杂度为O(V^2),若基于Fibonacci heap的最小优先队列实现版本则时间复杂度为O(E + VlogV)
Bellman-Ford本身还可以被优化为spfa,其实是Bellman-ford + 队列优化,其实和 bfs 的关系更密一点
三.多源最短路径 Floyd-Warshall
Dijkstra:shortest path from one node to all nodes
Bellman-Ford:shortest path from one node to all nodes,negative edges allowed
Floyd-Warshall:shortest path between all pairs of vertices,negative edges allowed
四.强连通分量 SCC:Strongly Connected Component
有向图强连通分量:在有向图G中,如果两个顶点 vi,vj间(vi > vj)有一条从 vi 到 vj 的有向路径,同时还有一条从 vj 到 vi 的有向路径,则称两个顶点强联通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量
五.最小生成树(minimum spanning tree)
对于一个图而言,它可以生成很多树,生成树是将原图的全部顶点以最少的边连通的子图,对于有n个顶点的连通图,生成树有 n - 1 条边,若边数小于此数就不可能将各顶点连通,如果边的数量多于 n - 1 条边,必定会产生回路。对于一个带权连通图,生成树不同,树中各边上权值总和也不同,权值总和最小的生成树则称为图的最小生成树
有哪些处理MST的算法?
1.Prim Native implementation O(V^2) 稠密图
因为任意两个点都可以连接,所以属于稠密图,有一种特殊的Prim算法可以达到O(V^2) = O(n^2)的时间复杂度,而通常使用的Prim或Kruskal的时间复杂度为O(ElogV)= O(n^2logn)
常见的O(ElogV)的最小生成树算法大家都比较熟悉,这里着重介绍O(V^2)的特殊Prim算法
不适用pq来找往出走的最近边,而是暴力扫一遍所有往出走的边,找最小
vis[i] = false,顶点i,没加入到最小生成树
dist[i] = INT_MAX,边集E,只记录了到顶点v的最小权重的边
dist[0] = 0,初始是空树,到顶点0的边最短
for (u < N) {
1.从边集中,选出权重最小的边,这里vis[v] == false 且 dist[v] != INT_MAX,构成边集
即寻找vis[v] == false且dist[v]最小的顶点v
2.将v加入最小生成树
vis[v] = true;
3.更新边集,v在生成树中,v射出的边落在集合T中的顶点的距离都需要更新
for (i in v
if (vis[i] = false 且 v -> i 的权重 < dist[i]) {
dist[i] = v -> i 的权重;
}
}
}
2.Prim PQ implementation O(ElogV) 稀疏图
3.Kruskal UF implementation O(ElogE)(或者更大范围ElogV,E <= V2)稀疏图
Kruskal算法用于计算一个图的最小生成树,这个算法的步骤如下:
按照边的权重从小到大进行排序
依次将每条边增加到最小生成树中,除非这条边会造成回路
Kruskal算法是基于贪心的思想得到的。首先我们把所有的边按照权值现从小到大排列,接着按照顺序选取每条边,如果这条边的两个端点不属于同一集合,那么就将它们合并,直到所有点都属于同一个集合为止。至于怎么合并到一个集合,那么这里我们就可以用到一个工具——并查集,换言之,Kruskal算法就是基于并查集的贪心算法
class DSU {
int[] parent;
public DSU(int N) {
parent = new int[N];
for (int i = 0;i < N; i++) parent[i] = i;
}
public int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
}
public void union(int x,int y) {
parent[find(x)] = find(y);
}
}
注意区分Floyd warshall算法,Dijkstra算法以及Bellman-Ford算法,这些是用来求单源最短路径或任意两点最短路径
六.拓扑排序
在图论中,拓扑排序是一个有向无环图(DAG)的所有顶点的线性序列
且该序列必须满足下面两个条件:
1.每个顶点出现且只出现一次
2.若存在一条从顶点A到顶点B的路径,那么在序列中顶点A出现在顶点B的前面
拓扑排序通常用来排序具有依赖关系的任务
有向无环图才有拓扑排序,非DAG图没有拓扑排序一说
实现方式有两种:①BFS Kahn算法,基于贪心,每次从入度为0的点开始,正序为拓扑
②DFS基于搜索, 每次保证当前点出度为0后才遍历,逆序为拓扑
单纯拓扑不推荐DFS,如果是二分图才用DFS
BFS入度表法(Kahn算法)
1.从图中选择一个入度为0的顶点,输出该顶点
2.从图中删除该节点及其所有出边(即与之邻接的所有顶点入度 -1)
3.反复执行这两个步骤,直至所有节点都输出,即整个拓扑排序完成;或者直至剩下的图中再没有入度为0的节点,这就说民此图中没有回路,不可能进行拓扑排序
特别地,对于有向图,节点的入度是指进入该节点的边的条数,节点的出度是指从该节点出发的边的条数
模板1:BFS入度表法(Kahn算法)
public boolean canFinish(int N,int[][] edges) {
Map<Integer,List<Integer>> gragh = new HashMap<>();
int[] indegree = new int[N]; //1.建图
for (int[] edge : edges) { //2.建indegree
int end = edge[0],start = edge[1];
gragh.computeIfAbsent(start,x -> new ArrayList<>()).add(end);
indegree[end]++; //[0,1]图的方向是从1指向0,0的入度有所增加,end<-start
}
Queue<Integer> q = new LinkedList<>();//3.找到有向图的入口,入度为0的点
for (int i = 0;i < N;i++)
if (indegree[i] == 0) q.add(i);
int count = 0; //4.BFS拓扑排序
while (!q.isEmpty()) {
int cur = q.poll():
count++;
for (int nei : gragh.getOrDefault(cur,new ArrayList<>()))
if (--indegree[nei] == 0) q.offer(nei);
}
return count == N;
}
模板2:改进版DFS(三色法)
1.建图
2.建visited int[]
3.所有unvisited点展开
4.DFS内部逻辑
//DFS
//0:unvisited; 1:inProgress; 2:visited
List<List<Integer>> edges;
int[] visited;
boolean valid = true;
public boolean canFinish(int N,int[][] prerequisites) {
edges = new ArrayList<>();
for (int i = 0;i < N;i++)
edges.add(new ArrayList<Integer>());
visited = new int[N];
for (int[] edge : prerequisites)
edges.get(edge[1]).add(edge[0]);
for (int i = 0;i < N;i++)
if (visited[i] == 0) dfs(i);
return valid;
}
public void dfs(int u) {
visited[u] = 1;
for (int v: edges.get(u))
if (visited[v] == 0) dfs(v);
else if (visited[v] == 1) valid = false;
visited[u] = 2;
}
DFS内部逻辑
1.遍历当前点为进行中灰色1
2.遍历neighbor:遇到1则有环,遇到0继续dfs
3.所有neighbor遍历结束,标记当前点为黑色2,已遍历结束
七.Dijkstra
典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止
最短路径分为单源最短路径和多源最短路径
单源最短路径中我们常用:
1.BFS/DFS(unweighted gragh)
2.Dijkstra基础版本(非负edge,weighted gragh,all below)
3.Dijkstra优化版本(非负edge) Dijkstra+heap
4.Bellman-Ford(负edge)
5.Bellman-Ford(负edge) 队列优化版本SPFA(Shortest Path Faster Algorithm)
多源最短路径 Floyd-Warshall
第一个bfs/dfs考的多,dijkstra常用于follow up,后2个不怎么考
图的转化:node or vertex珠子——地点;edge绳长——路径。两点之间的最短路径,就是把珠子拉直,绷直的绳子就是最短路径
Dijkstra核心思想:我们从起点u作为中心慢慢地向四周扩散,同时不断更新到达其他点的最短距离,Dijikstra是一个贪心算法,每一步选择的都是从当前已经到达点出发,到没有visited过点的最小权重的edge,这样来找到达其他点的最短路径
总结
1.Dijkstra是用来解决单源最短路径,非负edge的题
2.可以有基础实现方式V2,也可以由pq实现方式ElogV
3.基础实现通过visited array & 每次for loop所有的此点往外延伸的edge来找下一步该走哪里
4.优化实现是通过pq来拿到当前最短的点,注意入pq前最好也与distance array作比较,更短才入pq
int vis[MAXN];
int dis[MAXN];
void dijkstra(int s,int n) {
for (int i = 0; i < n; i++) vis[i] = 0;
for (int i = 0; i < n; i++) dis[i] = (i == s ? 0 : INF);
priority_queue<PII,VII,greater<PII>>q;//声明优先队列,每次从队列中取出的是具有最高优先权的元素
//优先队列第一个参数为比较类型,第二个为容器类型,第三个为比较函数
//greater实现小顶堆,less实现大顶堆(默认为大顶堆)
q.push(MP(dis[s],s));//将它本身先推进优先队列
while(!q.empty()) {//当队列空时已将所有边加入队列并推出
PII p = q.top();
int x = p.second;
q.pop();
if (vis[x]) continue;
vis[x] = 1;
for (int i = 0; i < G[x].size(); i++) {//每一条与x相邻的边都要更新
int y = G[x][i].first;
int d = G[x][i].second;
if (!vis[y] && dis[x] + d < dis[y]) {
dis[y] = dis[x] + d;
q.push(MP(dis[y].y));//把每一个更新的长度加进队列
}
}
}
}