LeetCode刷题 - 图小结

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));//把每一个更新的长度加进队列
      }
    }
  }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值