图论
拓扑排序
例题
实现
- 时间复杂度为 \(O(n+m)\)。
1 | private static int[] topologicalSort(int n, int[][] edges) { |
最小生成树
例题
Prim
实现一:朴素版本
- 时间复杂度为 \(O(n^{2})\)。
1 | private static int prim(int n, int[][] edges) { |
实现二:优先队列优化
- 时间复杂度为 \(O(m\log{m})\)。
1 | private static int prim(int n, int[][] edges) { |
Kruskal
- 时间复杂度为 \(O(m\log{m})\)。
1 | private static int kruskal(int n, int[][] edges) { |
最短路
例题
Dijkstra
- 使用场景:解决边权非负的单源最短路问题。
实现一:朴素版本
- 时间复杂度为 \(O(n^{2})\)。
1 | private static final int INF = (int) 1e9; |
实现二:优先队列优化
- 时间复杂度为 \(O(m\log{m})\)。
1 | private static final int INF = (int) 1e9; |
Bellman-Ford
- 时间复杂度为 \(O(nm)\)。
- 使用场景:解决任意边权的单源最短路问题;判断是否存在负环;解决有边数限制的单源最短路问题。
实现一:朴素版本
1 | private static final int INF = (int) 1e9; |
实现二:队列优化(不能存在负环)
1 | private static final int INF = (int) 1e9; |
Floyd-Warshall
- 时间复杂度为 \(O(n^{3})\)。
- 使用场景:解决任意边权的多源最短路问题。
1 | private static final int INF = (int) 1e9; |
最近公共祖先
例题
倍增
- 预处理时间复杂度为 \(O(n\log{n})\),查询时间复杂度为 \(O(\log{n})\)。
- 原理:\(f[i][j]\) 表示节点 \(j\) 的第 \(2^{i}\) 个祖先,当利用倍增得到 \(f\) 时,对于任意两个节点 \(x,y\),先将较深的节点向上跳到相同深度,然后两个节点贪心的向上跳到 \(\operatorname{lca}\) 下方距离它最近的节点,最后得到的节点就是 \(\operatorname{lca}\) 的直接子节点。(在进行倍增时,根节点的父节点可以是任何值,因为该值不会影响算法的正确性)
1 | private static void dfs(int x, int fa, List<Integer>[] g, int[][] f, int[] d) { |
Tarjan
- 离线查询算法,时间复杂度为 \(O((n+m)\log{n})\)。更精确的复杂度分析可以使用反阿克曼函数。
- 原理:每当处理完一个子树,就将该子树的根节点和其父节点合并,特别注意合并的方向是 \(f[y]=x\)。然后我们会遍历包含当前节点 \(x\) 的查询,如果另一个节点 \(y\) 访问过,则 \(\operatorname{lca}(x,y)=\operatorname{find}(y)\)。至于为什么是这样,可以通过分类讨论得到。注意 \(q\) 需要像无向图一样,为单个查询存储双向边。
1 | private static void tarjan(int x, List<Integer>[] g, boolean[] vis, UnionFind uf, List<int[]>[] q, int[] ans) { |
树链剖分
- 预处理时间复杂度为 \(O(n)\),查询时间复杂度为 \(O(\log{n})\)。
- 原理:将树划分为若干重链,树中的每条路径不会包含超过 \(\log{n}\) 条不同的重链,所以查询的时间复杂度为 \(O(\log{n})\)。第一次 DFS 得到每个节点的父节点,深度,以及根据子树大小得到每个节点的重子节点。第二次 DFS 通过优先遍历重子节点,再遍历轻子节点,从而得到每个节点所在重链的头节点。然后就可以进行查询,通过比较 \(x,y\) 所在重链的头节点,来向上跳跃,最终得到 \(\operatorname{lca}\)。
1 | private static void dfs1(int x, int fa, List<Integer>[] g, int[] f, int[] d, int[] s, int[] h) { |
强连通分量
例题
Tarjan
- 时间复杂度为 \(O(n+m)\)。
- 原理:\(dfn[x]\) 表示节点 \(x\) 的 DFS 编号;\(low[x]\) 表示节点 \(x\) 能够到达的节点的最小的 DFS 编号。我们将图看作一棵树,并定义四种边,那么强连通分量的根节点就是该分量中第一个被遍历到的节点,满足 \(dfn[x]=low[x]\),所以,过程很复杂,难以描述,直接看 wiki 吧。(注意使用的时候,将 \(dfn\) 初始化为 \(-1\),并且对所有节点调用该算法前,需要判断 \(dfn=-1\) 是否成立)
1 | private static int dfnCnt, sccCnt; |