小航的算法日记图论

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小航的算法日记图论相关的知识,希望对你有一定的参考价值。


目录

  • ​​一、概念、模板​​
  • ​​存图方式:​​
  • ​​1.邻接矩阵​​
  • ​​2.邻接表​​
  • ​​3.类​​
  • ​​算法:​​
  • ​​拓扑排序:​​
  • ​​最短路问题:​​
  • ​​1.Floyd 「多源汇最短路」​​
  • ​​2.朴素 Dijkstra 「单源最短路」​​
  • ​​3.堆优化 Dijkstra 「单源最短路」​​
  • ​​4.Bellman Ford 「单源最短路」​​
  • ​​5.SPFA「单源最短路」​​
  • ​​最小生成树:​​
  • ​​1.Kruskal​​
  • ​​多源BFS:​​
  • ​​双向BFS:​​
  • ​​二、例题​​
  • ​​多源BFS​​
  • ​​题:417. 太平洋大西洋水流问题​​
  • ​​解:​​
  • ​​题:1162. 地图分析​​
  • ​​解:​​
  • ​​双向BFS​​
  • ​​题:433. 最小基因变化​​
  • ​​解:​​
  • ​​题:752. 打开转盘锁​​
  • ​​解:​​
  • ​​拓扑排序​​
  • ​​题:207. 课程表​​
  • ​​解:​​
  • ​​题:802. 找到最终的安全状态​​
  • ​​解:​​
  • ​​最短路​​
  • ​​题:743. 网络延迟时间​​
  • ​​解:​​
  • ​​题:787. K 站中转内最便宜的航班​​
  • ​​解:​​
  • ​​搜索​​
  • ​​题:841. 钥匙和房间​​
  • ​​解:​​
  • ​​题:127. 单词接龙​​
  • ​​解:​​
  • ​​最小生成树​​
  • ​​题:778. 水位上升的泳池中游泳​​
  • ​​解:​​
  • ​​题:1631. 最小体力消耗路径​​
  • ​​解:​​
  • ​​迭代加深​​
  • ​​题:863. 二叉树中所有距离为 K 的结点​​
  • ​​解:​​

一、概念、模板

存图方式:

1.邻接矩阵

适用边数较多的稠密图(边数量 m ≈ 点数量 n2

// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];

// 加边操作
void add(int a, int b, int c)
w[a][b] = c;

2.邻接表

使用边数较少的稀疏图(边数量 m ≈ 点数量 n)【这种存图方式又称:链式前向星存图】

int[] head = new int[N]; // 某节点对应的边
int[] edge = new int[M]; // 某条边指向的节点
int[] nextEdge = new int[M]; // 寻找下一条边
int[] weight = new int[M]; // 边的权重
int idx; // 对边进行编号

void add(int a, int b, int c)
edge[idx] = b;
nextEdge[idx] = head[a];
head[a] = idx;
weight[idx] = c;
idx++;

遍历所有由 a 点发出的边:

for (int i = head[a]; i != -1; i = nextEdge[i]) 
int b = edge[i], c = weight[i]; // 存在由 a 指向 b 的边,权重为 c

3.类

【确保某个操作复杂度严格为 O(m) 时才使用】

通过建立一个类来记录有向边信息:

class Edge 
// 代表从 a 到 b 有一条权重为 c 的边
int a, b, c;
Edge(int _a, int _b, int _c)
a = _a; b = _b; c = _c;

使用 List 存起所有的边对象,并在需要遍历所有边的时候,进行遍历:

List<Edge> es = new ArrayList<>();

...

for (Edge e : es)
...

算法:

拓扑排序:

「入度」和「出度」的概念:

  • 入度:有多少条边直接指向该节点;
  • 出度:由该节点指出边的有多少条。

在图论中,一个有向无环图必然存在至少一个拓扑序与之对应,反之亦然。

最短路问题:

1.Floyd 「多源汇最短路」

多源汇最短路算法 Floyd 也是基于动态规划,其原始的三维状态定义为 f[i][j][k] 代表从点 i 到点 j,且经过的所有点编号不会超过 k(即可使用点编号范围为 [1, k])的最短路径

2.朴素 Dijkstra 「单源最短路」
3.堆优化 Dijkstra 「单源最短路」

很多时候给定的图存在负权边,这时类似Dijkstra等算法没有用武之地(​​Dijkstra算法只能用来解决正权图的单源最短路径问题​​)

Bellman Ford/SPFA 都是基于​​动态规划​​,其原始的状态定义为 f[i][k] 代表从起点到 i 点,且经过最多 k 条边的最短路径。

4.Bellman Ford 「单源最短路」
  • 优点:可以解决​​有负权边的单源最短路径​​​问题,而且可以用来​​判断是否有负权回路​​​,Bellman_ford算法还可以​​求边数限制​​的最短路,SPFA不能。
  • 缺点:​​时间复杂度​​​ O(N*E) (N是点数,E是边数)普遍是要​​高于Dijkstra算法​​O(N²)的

实现概述:

1.建立一个dist数组(初始化为INF,该点到他本身赋为0)记录起始点到所有点的最短路径
2.确定​​​需要迭代的次数​​​(可能会有边数限制),每次遍历对​​所有的边​​​进行一次松弛操作,直到遍历结束。
3.遍历都结束后,若在进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路。

5.SPFA「单源最短路」

SPFA 是对 Bellman Ford 的优化实现,可以使用​​队列​​​进行优化,也可以使用​​栈​​进行优化。不过该算法的稳定性较差,在稠密图中SPFA算法时间复杂度会退化。

实现概述:

1.建立一个队列,初始时队列只有一个起始点,建立一个dist数组(初始化INF,该点到他本身赋为0)记录起始点到所有点的最短路径
2.松弛操作:用队列的点刷新起始点到所有点的最短路,如果刷新成功,且该点不在队列加入队列,直到队列为空

图解:

给定一个有向图,求A~E的最短路。

【小航的算法日记】图论_单源最短路


源点A首先入队,并且AB松弛

【小航的算法日记】图论_图论_02


扩展与A相连的边,B,C 入队并松弛

【小航的算法日记】图论_图论_03

B,C分别开始扩展,D入队并松弛

【小航的算法日记】图论_算法_04


D出队,E入队并松弛。

【小航的算法日记】图论_单源最短路_05


E出队,此时队列为空,源点到所有点的最短路已被找到,A->E的最短路即为8

【小航的算法日记】图论_搜索_06

最小生成树:

1.Kruskal

多源BFS:

男たちは グランド ライン を 目指し(めざし)梦を(ゆめを)追い(おい)続ける(つづける)。世はまさに大海贼 时代(じだい)。
于是男子汉们起程前往伟大的航路,追逐梦想,大海贼时代来临了。

一般,广度优先搜索都是从一个源点出发。

【小航的算法日记】图论_图论_07


多源广度优先搜索长这样。

【小航的算法日记】图论_单源最短路_08


如何证明理解多源点BFS的正确性?其实可以通过添加​​超级源点​​方式来思考。添加超级源点可以使多源BFS退化成单源BFS。

首先让我们用离散数学中图的概念来描述这张地图:

  1. 海洋和陆地都是图中的结点。
  2. 相邻网格的对应结点之间有一条无向边。

这样地图中的网格及其相邻关系构成了一张无向无权图。 如下图示:

【小航的算法日记】图论_单源最短路_09


我们向图中加入一个超级源点,并在超级源点与每个陆地结点之间建立一条边。如下图示:

【小航的算法日记】图论_单源最短路_10


现在我们从超级源点开始做单源BFS,发现原先的多个源点只不过是BFS的​​第二层​​而已~。

所以​​多源BFS没有改变BFS的本质,不会影响结果的正确性​​~

双向BFS:

实现思路:

  1. 创建「两个队列」分别用于两个方向的搜索;
  2. 创建「两个哈希表」用于「解决相同节点重复搜索」和「记录转换次数」;
  3. 为了尽可能让两个搜索方向“平均”,每次从队列中取值进行扩展时,先判断哪个队列容量较少;
  4. 如果在搜索过程中「搜索到对方搜索过的节点」,说明找到了最短路径。

伪代码:

// q1、q2 为两个方向的队列
// m1、m2 为两个方向的哈希表,记录每个节点距离起点的

// 只有两个队列都不空,才有必要继续往下搜索
// 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点,反向搜索也没必要进行了
while(!q1.isEmpty() && !q2.isEmpty())
if (q1.size() <= q2.size())
update(q1, m1, m2);
else
update(q2, m2, m1);



// update 为将当前队列 q 中包含的元素取出,进行「一次完整扩展」的逻辑(按层拓展)
void update(Queue q, Map cur, Map other)

二、例题

多源BFS

题:417. 太平洋大西洋水流问题

有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。

这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。

岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。

返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。

示例 1:

【小航的算法日记】图论_算法_11

输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]

示例 2:

输入: heights = [[2,1],[1,2]]
输出: [[0,0],[0,1],[1,0],[1,1]]

提示:

m == heights.length
n == heights[r].length
1 <= m, n <= 200
0 <= heights[r][c] <= 105

解:

解题思路:由四周向中心BFS,取交集

AC代码:

class Solution 
int n, m;
int[][] g;
public List<List<Integer>> pacificAtlantic(int[][] heights)
g = heights;
m = heights.length; n = g[0].length;
Queue<int[]> q1 = new LinkedList<>(), q2 = new LinkedList<>();
boolean[][] res1 = new boolean[m][n], res2 = new boolean[m][n];
// 初始化访问队列
for(int i = 0; i < m; ++ i)
for(int j = 0; j < n; ++ j)
// 左上角
if(i == 0 || j == 0)
res1[i][j] = true;
q1.offer(new int[]i ,j);

// 右下角
if(i == m - 1 || j == n - 1)
res2[i][j] = true;
q2.offer(new int[]i, j);



// BFS
bfs(q1, res1); bfs(q2, res2);
// 取res1, res2的交集
List<List<Integer>> ans = new ArrayList<>();
for(int i = 0; i < m; ++ i)
for(int j = 0; j < n; ++ j)
if(res1[i][j] && res2[i][j])
ans.add(new ArrayList<Integer>(Arrays.asList(i, j)));



return ans;

int[][] dirs = new int[][] 1, 0, -1, 0, 0, 1, 0, -1;
void bfs(Queue<int[]> q, boolean[][] res)
while(!q.isEmpty())
int[] info = q.poll();
int x = info[0], y = info[1], w = g[x][y];
for(int[] dir : dirs)
int nx = x + dir[0], ny = y + dir[1];
// 向高处拓展
if(isMap(nx, ny) && !res[nx][ny] && w <= g[nx][ny])
q.offer(new int[] nx, ny);
res[nx][ny] = true;




boolean isMap(int x, int y)
return x >= 0 && x < m && y >= 0 && y < n;

解题思路:​​DFS​

AC代码:

class Solution 
int n, m;
int[][] g;
public List<List<Integer>> pacificAtlantic(int[][] heights)
m = heights.length; n = heights[0].length;
g = heights;
boolean[][] res1 = new boolean[m][n], res2 = new boolean[m][n];
// 初始化
for(int i = 0; i < m; ++ i)
for(int j = 0; j < n; ++ j)
if(i == 0 || j == 0)
if(!res1[i][j]) dfs(i, j, res1);

if(i == m - 1 || j == n - 1)
if(!res2[i][j]) dfs(i, j, res2);



List<List<Integer>> ans = new ArrayList<>();
for(int i = 0; i < m; ++ i)
for(int j = 0; j < n; ++ j)
if(res1[i][j] && res2[i][j])
ans.add(new ArrayList<Integer>(Arrays.asList(i, j)));



return ans;

int[][] dirs = new int[][] 1, 0, -1, 0, 0, 1, 0, -1;
void dfs(int x, int y, boolean[][] res)
res[x][y] = true;
for(int[] dir : dirs)
int dx = x + dir[0], dy = y + dir[1];
if(isMap(dx, dy) && !res[dx][dy] && g[dx][dy] >= g[x][y])
dfs(dx, dy, res);



boolean isMap(int x, int y)
return x >= 0 && x < m && y >= 0 && y < n;

解题思路:​​并查集​

AC代码:

class Solution 
int N = 200 * 200 + 10;
int[] p1 = new int[N], p2 = new int[N];
int n, m, tot, S, T;
int[][] g;
void union(int[] p, int a, int b)
p[find(p, a)] = p[find(p, b)];

int find(int[] p, int x)
if (p[x] != x) p[x] = find(p, p[x]);
return p[x];

boolean query(int[] p, int a, int b)
return find(p, a) == find(p, b);

int getIdx(int x, int y)
return x * n + y;

public List<List<Integer>> pacificAtlantic(int[][] _g)
g = _g;
m = g.length; n = g[0].length; tot = m * n; S = tot + 1; T = tot + 2;
for (int i = 0; i <= T; i++) p1[i] = p2[i] = i;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
int idx = getIdx(i, j);
if (i == 0 || j == 0)
if (!query(p1, S, idx)) dfs(p1, S, i, j);

if (i == m - 1 || j == n - 1)
if (!query(p2, T, idx)) dfs(p2, T, i, j);



List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
int idx = getIdx(i, j);
if (query(p1, S, idx) && query(p2, T, idx))
List<Integer> list = new ArrayList<>();
list.add(i); list.add(j);
ans.add(list);



return ans;

int[][] dirs = new int[][]1,0,-1,0,0,1,0,-1;
void dfs(int[] p, int ori, int x, int y)
union(p, ori, getIdx(x, y));
for (int[] di : dirs)
int nx = x + di[0], ny = y + di[1];
if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
if (query(p, ori, getIdx(nx, ny)) || g[nx][ny] < g[x][y]) continue;
dfs(p, ori, nx, ny);


题:1162. 地图分析

你现在手里有一份大小为 ​​n x n​​ 的 网格 grid,上面的每个 单元格 都用 0 和 1 标记好了。其中 0 代表海洋,1 代表陆地。

请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的,并返回该距离。如果网格上只有陆地或者海洋,请返回 -1。

我们这里说的距离是「曼哈顿距离」( Manhattan Distance):​​(x0, y0)​​​ 和 ​​(x1, y1)​​​ 这两个单元格之间的距离是 ​​|x0 - x1| + |y0 - y1|​​ 。

示例 1:

【小航的算法日记】图论_图论_12

输入:grid = [[1,0,1],[0,0,0],[1,0,1]]
输出:2
解释:
海洋单元格 (1, 1) 和所有陆地单元格之间的距离都达到最大,最大距离为 2。

示例 2:

【小航的算法日记】图论_图论_13

输入:grid = [[1,0,0],[0,0,0],[0,0,0]]
输出:4
解释:
海洋单元格 (2, 2) 和所有陆地单元格之间的距离都达到最大,最大距离为 4。

提示:

n == grid.length
n == grid[i].length
1 <= n <= 100
grid[i][j] 不是 0 就是 1

解:

解题思路:

  • 为了方便,我们在使用哈希表记录距离时,将二维坐标 (x, y) 转化为对应的一维下标 idx = x * n + y 作为 key 进行存储

多源BFS扩散的图示:(1表示陆地,0表示海洋)

【小航的算法日记】图论_算法_14

AC代码:

class Solution 
// 0-海洋,1-陆地
public int maxDistance(int[][] grid)
int n = grid.length;
Queue<int[]> queue = new LinkedList<>();
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < n; ++ i)
for(int j = 0; j < n; ++ j)
if(grid[i][j] == 1)
queue.offer(new int[] i, j);
map.put(i * n + j, 0);



int ans = -1;
int[][] dirs = new int[][] 1, 0, -1, 0, 0, 1, 0, -1;
while(!queue.isEmpty())
int[] poll = queue.poll();
int dx = poll[0], dy = poll[1];
int step = map.get(dx * n + dy);
for(int[] dir : dirs)
int nx = dx + dir[0], ny = dy + dir[1];
// 在这块岛屿并且是海洋
if(isMap(nx, ny, n) && grid[nx][ny] == 0)
grid[nx][ny] = step + 1;
queue.offer(new int[] nx, ny);
map.put(nx * n + ny, step + 1);
ans = Math.max(ans, step + 1);



return ans;

boolean isMap(int x, int y, int n)
return x >= 0 && x < n && y >= 0 && y < n;

双向BFS

题:433. 最小基因变化

基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 ​​A、C、G 和 T​​ 之一。

假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。

  • 例如,​​"AACCGGTT" --> "AACCGGTA"​​ 就是一次基因变化。

另有一个基因库 ​​bank​​ 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。(变化后的基因必须位于基因库 bank 中)

给你两个基因序列 ​​start​​​ 和 ​​end​​​ ,以及一个基因库 ​​bank​​ ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。

注意:起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。

示例 1:

输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]
输出:1

示例 2:

输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]
输出:2

示例 3:

输入:start = "AAAAACCC", end = "AACCCCCC", bank =   ["AAAACCCC","AAACCCCC","AACCCCCC"]
输出:3

提示:

start.length == 8
end.length == 8
0 <= bank.length <= 10
bank[i].length == 8
start、end 和 bank[i] 仅由字符 [A, C, G, T] 组成

解:

解题思路:​​双向BFS​

AC代码:

class Solution 
static char[] items = new char[] A, C, G, T;
Set<String> set = new HashSet<>();
public int minMutation(String start, String end, String[] bank)
set.add(start);
for(String s : bank) set.add(s);
// 变化后的基因必须位于基因库 bank 中
if(!set.contains(end)) return -1;
Queue<String> q1 = new LinkedList<>(), q2 = new LinkedList<>();
q1.offer(start); q2.offer(end);
Map<String, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
m1.put(start, 0); m2.put(end, 0);
while(!q1.isEmpty() && !q2.isEmpty())
int t = -1;
if(q1.size() <= q2.size()) t = update(q1, m1, m2);
else t = update(q2, m2, m1);
if(t != -1) return t;

return -1;

int update(Queue<String> q, Map<String, Integer> cur, Map<String, Integer> other)
int m = q.size();
while(m -- > 0)
String s = q.poll();
char[] cs = s.toCharArray();
int step = cur.get(s);
// 基因序列可以表示为一条由 8 个字符组成的字符串
for(int i = 0; i < 8; ++ i)
for(char item : items)
char[] clone = cs.clone();
clone[i] = item;
String newStr = String.valueOf(clone);
if(!set.contains(newStr) || cur.containsKey(newStr)) continue;
if(other.containsKey(newStr)) return other.get(newStr) + step + 1;
q.offer(newStr);
cur.put(newStr, step + 1);



return -1;

题:752. 打开转盘锁

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ​​0, 1, 2, 3, 4, 5, 6, 7, 8, 9​​ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 ​​0000​​ ,一个代表四个拨轮的数字的字符串。

列表 ​​deadends​​ 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。

示例 1:

输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。

示例 2:

输入: deadends = ["8888"], target = "0009"
输出:1
解释:把最后一位反向旋转一次即可 "0000" -> "0009"。

示例 3:

输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:无法旋转到目标数字且不被锁定。

提示:

1 <= deadends.length <= 500
deadends[i].length == 4
target.length == 4
target 不在 deadends 之中
target 和 deadends[i] 仅由若干位数字组成

解:

解题思路:​​双向BFS​

AC代码:

class Solution 
Set<String> set = new HashSet<>();
public int openLock(String[] deadends, String end)
String start = "0000";
if(start.equals(end)) return 0;
// 判断是否存在于某集合中
for(String d : deadends) set.add(d);
if(set.contains(start)) return -1;
// 双向BFS
Queue<String> q1 = new LinkedList<>(), q2 = new LinkedList<>();
HashMap<String, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
q1.offer(start); q2.offer(end);
m1.put(start, 0); m2.put(end, 0);
while(!q1.isEmpty() && !q2.isEmpty())
int t = -1;
if(q1.size() <= q2.size()) t = update(q1, m1, m2);
else t = update(q2, m2, m1);
if(t != -1) return t;

return -1;

int[] dirs = new int[] 1, -1; // 1 :正向转,-1 :反向转
int update(Queue<String> q, Map<String, Integer> cur, Map<String, Integer> other)
int m = q.size();
while(m -- > 0)
String poll = q.poll();
char[] pcs = poll.toCharArray();
int step = cur.get(poll);
// 每个字符串共有四个字符
for(int i = 0; i < 4; ++ i)
// 遍历方向
for(int dir : dirs)
// 替换字符串
int next = (pcs[i] - 0 + dir + 10) % 10;
char[] clone = pcs.clone();
clone[i] = (char)(next + 0);
String str = String.valueOf(clone);
if(set.contains(str) || cur.containsKey(str)) continue;
if(other.containsKey(str)) return step + 1 + other.get(str);
q.offer(str);
cur.put(str, step + 1);



return -1;

拓扑排序

题:207. 课程表

你这个学期必须选修 ​​numCourses​​​ 门课程,记为​​0​​​ 到 ​​numCourses - 1​​ 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 ​​prerequisites[i] = [ai, bi] ​​​,表示如果要学习课程 ​​ai​​​ 则 必须 先学习课程 ​​bi​​ 。

  • 例如,先修课程对 ​​[0, 1]​​​ 表示:想要学习课程 ​​0​​​ ,你需要先完成课程 ​​1​​ 。

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

提示:

1 <= numCourses <= 105
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i] 中的所有课程对 互不相同

解:

解题思路:

1.存图:若存在前置课程,寸边,并统计所有的点入度
2.创建队列,将入度为0的点加入队列
3.拓扑排序,若所有点都被访问,则所有课程都能访问,

AC代码:

class Solution 
// 邻接表
int N = 100010, M = 5010;
int[] head = new int[N], edge = new int[M], nextEdge = new int[M];
int idx;
int[] count = new int[N]; // 统计点的入度
void add(int a, int b)
edge[idx] = b;
nextEdge[idx] = head[a];
head[a] = idx;
idx ++;
count[b] ++;

public boolean canFinish(int numCourses, int[][] prerequisites)
Arrays.fill(head, -1);
for(int[] info : prerequisites) add(info[1], info[0]);
int ans = 0;
Queue<Integer> queue = new LinkedList<>();
for(int i = 0; i < numCourses; ++ i)
if(count[i] == 0) queue.offer(i); // 将没有先行课的先加入队列

while(!queue.isEmpty())
int t = queue.poll();
ans ++;
for(int i = head[t]; i != -1; i = nextEdge[i])
int j = edge[i];
if(-- count[j] == 0) queue.offer(j); // 将没有的课加入队列继续遍历


return ans == numCourses;

题:802. 找到最终的安全状态

有一个有 n 个节点的有向图,节点按 0 到 n - 1 编号。图由一个 索引从 0 开始 的 2D 整数数组 graph表示, graph[i]是与节点 i 相邻的节点的整数数组,这意味着从节点 i 到 graph[i]中的每个节点都有一条边。

如果一个节点没有连出的有向边,则它是 ​​终端节点​​​ 。如果没有出边,则节点为终端节点。如果从该节点开始的所有可能路径都通向 ​​终端节点​​​ ,则该节点为 ​​安全节点​​ 。

返回一个由图中所有 ​​安全节点​​ 组成的数组作为答案。答案数组中的元素应当按 升序 排列。

示例 1:

【小航的算法日记】图论_最短路_15

输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]]
输出:[2,4,5,6]
解释:示意图如上。
节点 5 和节点 6 是终端节点,因为它们都没有出边。
从节点 2、4、5 和 6 开始的所有路径都指向节点 5 或 6 。

示例 2:

输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]
输出:[4]
解释:
只有节点 4 是终端节点,从节点 4 开始的所有路径都通向节点 4 。

提示:

n == graph.length
1 <= n <= 104
0 <= graph[i].length <= n
0 <= graph[i][j] <= n - 1
graph[i] 按严格递增顺序排列。
图中可能包含自环。
图中边的数目在范围 [1, 4 * 104] 内。

解:

解题思路:​​反向图​​​ + ​​拓扑排序​

安全序列:对于一个起始节点,如果从该节点出发,无论每一步选择沿哪条有向边行走,最后必然在有限步内到达终点,则将该起始节点称作是安全的。 => 某个点所有的出路没有回路

原图的出度为0的点 => 反向图的入度为0的点

AC代码:

class Solution 
int N = (int)1e4+10, M = 4 * N;
int idx;
int[] he = new int[N], e = new int[M], ne = new int[M];
int[] cnts = new int[N];
void add(int a, int b)
e[idx] = b;
ne[idx] = he[a];
he[a] = idx++;

public List<Integer> eventualSafeNodes(int[][] g)
int n = g.length;
// 存反向图,并统计入度
Arrays.fill(he, -1);
for (int i = 0; i < n; i++)
for (int j : g[i])
add(j, i);
cnts[i]++;


// BFS 求反向图拓扑排序
Deque<Integer> d = new ArrayDeque<>();
for (int i = 0; i < n; i++)
if (cnts[i] == 0) d.addLast(i);

while (!d.isEmpty())
int poll = d.pollFirst();
for (int i = he[poll]; i != -1; i = ne[i])
int j = e[i];
if (--cnts[j] == 0) d.addLast(j);


// 遍历答案:如果某个节点出现在拓扑序列,说明其进入过队列,说明其入度为 0
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < n; i++)
if (cnts[i] == 0) ans.add(i);

return ans;

最短路

题:743. 网络延迟时间

有 ​​n​​ 个网络节点,标记为 1 到 n。

给你一个列表 times,表示信号经过 有向边的传递时间。 ​​times[i] = (ui, vi, wi)​​,其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

示例 1:

【小航的算法日记】图论_图论_16

输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2

示例 2:

输入:times = [[1,2,1]], n = 2, k = 1
输出:1

示例 3:

输入:times = [[1,2,1]], n = 2, k = 2
输出:-1

提示:

1 <= k <= n <= 100
1 <= times.length <= 6000
times[i].length == 3
1 <= ui, vi <= n
ui != vi
0 <= wi <= 100
所有 (ui, vi) 对都 互不相同(即,不含重复边)

解:

此题只告诉了出发点,需要到达所有点 => 最后for循环遍历取最大

解题思路:​​Floyd​

枚举从任意点出发,到达任意点的距离

AC代码:

class Solution 
int N = 105, M = 6005;
int[][] w = new int[N][N]; // 邻接矩阵
int INF = 0x3f3f3f3f;
int n, k;
public int networkDelayTime(int[][] times, int _n, int _k)
n = _n; k = _k;
// 初始化邻接矩阵
for(int i = 1; i <= n; ++ i)
for(int j = 1; j <= n; ++ j)
w[i][j] = w[j][i] = i == j ? 0 : INF;


// 存图
for(int[] t : times)
int u = t[0], v = t[1], c = t[2];
w[u][v] = c;

// 最短路
floyd();
// 遍历答案
int ans = 0;
for(int i = 1; i <= n; ++ i)
ans = Math.max(ans, w[k][i]);

return ans >= INF / 2 ? -1 : ans;

void floyd()
// 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作
for(int p = 1; p <= n; ++ p)
for(int i = 1; i <= n; ++ i)
for(int j = 1; j <= n; ++ j)
w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]);




解题思路:​​朴素 Dijkstra​

AC代码:

class Solution 
int N = 105, M = 6005;
// 邻接矩阵
int[][] w = new int[N][N];
// 源点到x的最短距离y dist[x] = y;
int[] dist = new int[N];
// 标记哪些点已经被访问过
boolean[] visited = new boolean[N];
int INF = 0x3f3f3f3f;
int n, k;
public int networkDelayTime(int[][] ts, int _n, int _k)
n = _n; _k = k;
// 初始化邻接矩阵
for(int i = 1; i <= n; ++ i)
for(int j = 1; j <= n; ++ j)
w[i][j] = w[j][i] = i == j ? 0 : INF;


// 存图
for(int[] t : ts)
int u = t[0], v = t[1], c = t[2];
w[u][v] = c;

// 最短路
dijkstra();
// 遍历取最大
int ans = 0;
for(int i = 1; i <= n; ++ i)
ans = Math.max(ans, dist[i]);

return ans > INF / 2 ? -1 : ans;

// 最短路
void dijkstra()
// 初始化所有点标记,和距离数组
Arrays.fill(dist, INF); // 源点与任何点(除自己)不可达
dist[k] = 0;
Arrays.fill(visited, false); // 都未访问过
for(int p = 1; p <= n; ++ i)
// 找一个没见过的,距离当前点最近的下标
int t = -1;
for(int i = 1; i <= n; ++ i)
if(!visited[i] && (t == -1 || dist[i] < dist[t])) t = i;

// 标记更新
visited[t] = true;
// 将t点对各点的距离更新到dist取最小
for(int i = 1; i <= n; ++ i)
dist[i] = Math.min(dist[i], dist[t] + w[t][i]);



解题思路:堆优化 Dijkstra(邻接表)

​add方法:​

【小航的算法日记】图论_单源最短路_17

AC代码:

class Solution 
int N = 105, M = 6005;
// 邻接表
int[] head = new int[N], edge = new int[M], nextEdge = new int[M], weight = new int[M];
int[] dist = new int[N];
boolean[] visited = new boolean[N];
int n, k, idx = 0;
int INF = 0x3f3f3f3f;
// src:源点 desv:目标点 weight边权重
// 由源点src出发经编号idx的边指向目标点desv
void add(int src, int desV, int weight)
// 1.新建一条编为idx,指向desv的边
this.edge[idx] = desV;
// 2.头插法
this.nextEdge[idx] = head[src];
this.head[src] = idx;
// 3.赋权值
this.weight[idx] = weight;
idx ++;

public int networkDelayTime(int[][] ts, int _n, int _k)
n = _n; k = _k;
// 初始化链表头
Arrays.fill(head, -1);
// 存图
for(int[] t : ts)
int u = t[0], v = t[1], c = t[2];
add(u, v, c);

// 最短路
dijkstra();
int ans = -1;
// 遍历答案
for(int i = 1; i <= n; ++ i)
ans = Math.max(ans, dist[i]);

return ans > INF / 2 ? -1 : ans;

void dijkstra()
// 初始化:未访问,不可达
Arrays.fill(visited, false);
Arrays.fill(dist, INF);
dist[k] = 0;
// 使用【优先队列】存储
// 以(点编号,到起点的距离)进行存储,优先弹出最短距离较小的点
PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[1] - b[1]);
q.add(new int[]k , 0);
while(!q.isEmpty())
int[] poll = q.poll();
int id = poll[0], step = poll[1];
// 如果弹出的点已被标记
if(visited[id]) continue;
visited[id] = true;
for(int i = head[id]; i != -1; i = nextEdge[i])
int j = edge[i]; // 得到的边指向的点
if(dist[j] > dist[id] + weight[i])
dist[j] = dist[id] + weight[i];
q.add(new int[]j, dist[j]);





解题思路:​​Bellman Ford & 类​

AC代码:

class Solution 
class Edge
int a, b, c;
Edge(int _a, int _b, int _c)
a = _a; b = _b; c = _c;


int N = 105, M = 6005;
int[] dist = new int[N];
int INF = 0x3f3f3f3f;
int n, m, k;
List<Edge> es = new ArrayList<>();
public int networkDelayTime(int[][] ts, int _n, int _k)
n = _n; k = _k; m = ts.length;
// 存图
for(int[] t : ts)
int u = t[0], v = t[1], w = t[2];
es.add(new Edge(u, v, w));

// 最短路
bf();
// 遍历答案
int ans = 0;
for(int i = 1; i <= n; ++ i)
ans = Math.max(ans, dist[i]);

return ans > INF / 2 ? -1 : ans;

void bf()
// 初始化
Arrays.fill(dist, INF); dist[k] = 0;
for(int p = 1; p <= n; ++ p)
int[] prev = dist.clone();
for(Edge e : es)
int a = e.a, b = e.b, c = e.c;
dist[b] = Math.min(dist[b], prev[a] + c);



解题思路:​​Bellman Ford & 邻接表​

由于边的数量级大于点的数量级,因此能够继续使用【邻接表】的方式进行遍历

遍历所有边的复杂度的下界为 O(n),上界可以确保不超过 O(m)

AC代码:

class Solution 
int N = 110, M = 6010;
// 邻接表
int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
// dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
int[] dist = new int[N];
int INF = 0x3f3f3f3f;
int n, m, k, idx;
void add(int a, int b, int c)
e[idx] = b;
ne[idx] = he[a];
he[a] = idx;
w[idx] = c;
idx++;

public int networkDelayTime(int[][] ts, int _n, int _k)
n = _n; k = _k;
m = ts.length;
// 初始化链表头
Arrays.fill(he, -1);
// 存图
for (int[] t : ts)
int u = t[0], v = t[1], c = t[2];
add(u, v, c);

// 最短路
bf();
// 遍历答案
int ans = 0;
for (int i = 1; i <= n; i++)
ans = Math.max(ans, dist[i]);

return ans > INF / 2 ? -1 : ans;

void bf()
// 起始先将所有的点标记为「距离为正无穷」
Arrays.fill(dist, INF);
// 只有起点最短距离为 0
dist[k] = 0;
// 迭代 n 次
for (int p = 1; p <= n; p++)
int[] prev = dist.clone();
// 每次都使用上一次迭代的结果,执行松弛操作
for (int a = 1; a <= n; a++)
for (int i = he[a]; i != -1; i = ne[i])
int b = e[i];
dist[b] = Math.min(dist[b], prev[a] + w[i]);




解题思路:​小航的算法日记数组

小航的算法日记素数筛选

小航的算法日记变量交换算法

小航的算法日记大数计算

小航的算法日记字符串

小航的算法日记素数判定