拓扑排序
拓扑排序的本质是:广度优先遍历 + 贪心算法。
拓扑排序是广度优先遍历和贪心算法应用于有向图的专有名词。
应用场景:任务调度计划、课程安排。
拓扑排序的作用:
1.得到一个拓扑序,拓扑序不唯一;
2.检测有向图是否有环。(补充:无向图中检测是否有环,使用的数据结构是并查集。)
算法流程:
1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 0的结点放入队列。
2、只要队列非空,就从队首取出入度为 0 的结点,将这个结点输出到结果集中,并且将这个结点的所有邻接结点(它指向的结点)的入度减 1,在减 1 以后,如果这个被减 1的邻接结点的入度为 0,则入队。
3、当队列为空的时候,检查结果集中的顶点个数是否和图的顶点数相等即可。
在代码具体实现的时候,除了保存入度为 0 的队列,我们还需要两个辅助的数据结构:
1、邻接表:通过结点的索引,我们能够得到这个结点的后继结点;
2、入度数组:通过结点的索引,我们能够得到指向这个结点的结点个数。
力扣相关题目
1.207. 课程表
代码如下:
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
if (numCourses < 0) {
return false;
}
if (null == prerequisites || 0 == prerequisites.length || 0 == prerequisites[0].length) {
return true;
}
// 入度数组
int[] inDegree = new int[numCourses];
// 图的邻接表实现
HashSet<Integer>[] adj = new HashSet[numCourses];
for (int i = 0; i < numCourses; i ++) {
adj[i] = new HashSet<>();
}
for (int[] p : prerequisites) {
inDegree[p[0]] ++;
adj[p[1]].add(p[0]);
}
// 队列:java推荐实现
Queue<Integer> queue = new LinkedList<>();
// 找到一开始入度为0的顶点
for (int i = 0; i < numCourses; i ++) {
if (inDegree[i] == 0) {
queue.add(i);
}
}
int cnt = 0;
while (!queue.isEmpty()) {
Integer top = queue.poll();
cnt ++;
for (Integer successor : adj[top]) {
inDegree[successor] --;
if (inDegree[successor] == 0) {
queue.add(successor);
}
}
}
return cnt == numCourses;
}
}
// 拓扑排序的模板
class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
int[] res = new int[numCourses];
if (numCourses < 0 ) {
return res;
}
if ((null == prerequisites || 0 == prerequisites.length || 0 == prerequisites[0].length)) {
for (int i = 0; i < numCourses; i++) {
res[i] = numCourses - i - 1;
}
return res;
}
int[] inDegree = new int[numCourses];
HashSet<Integer>[] adj = new HashSet[numCourses];
for (int i = 0; i < numCourses; i ++) {
adj[i] = new HashSet<>();
}
for (int[] p : prerequisites) {
inDegree[p[0]] ++;
adj[p[1]].add(p[0]);
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i ++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
int cnt = 0;
while (!queue.isEmpty()) {
Integer top = queue.poll();
res[cnt ++] = top;
for (Integer successor : adj[top]) {
inDegree[successor] --;
if (inDegree[successor] == 0) {
queue.add(successor);
}
}
}
return cnt == numCourses ? res : new int[0];
}
}
3.802. 找到最终的安全状态
参考资料
代码如下:
class Solution {
public List<Integer> eventualSafeNodes(int[][] graph) {
int N = graph.length;
List<Set<Integer>> gra = new ArrayList<>();
List<Set<Integer>> rgra = new ArrayList<>();
boolean[] visited = new boolean[N];
List<Integer> res = new ArrayList<>();
for (int i = 0; i < N; i ++) {
gra.add(new HashSet<>());
rgra.add(new HashSet<>());
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < N; i ++) {
if (0 == graph[i].length) {
queue.offer(i);
}
for (int j : graph[i]) {
gra.get(i).add(j);
rgra.get(j).add(i);
}
}
while (!queue.isEmpty()) {
int j = queue.poll();
visited[j] = true;
for (int i : rgra.get(j)) {
gra.get(i).remove(j);
if (gra.get(i).isEmpty()) {
queue.offer(i);
}
}
}
for (int i = 0; i < N; i ++) {
if (visited[i]) {
res.add(i);
}
}
return res;
}
}
欧拉图
判断图是欧拉图或者是半欧拉图的条件:
无向图:
欧拉图充要条件:图是连通的且没有奇度顶点。
半欧拉图充要条件:图是连通的且恰有2个奇度顶点。
有向图:
欧拉图充要条件: 图的所有顶点都属于同一个连通量内且每一顶点的出度、入度相同。
半欧拉图充要条件:图的所有顶点都属于同一个连通量内且
1.恰有一个顶点的出度与入度差为1。
2.恰有一个顶点的入度与出度差为1。
3.其他顶点的出度与入度相同。
Hierholzer 算法
思路及算法
Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:
1.从起点出发,进行深度优先搜索。
2.每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。
3.如果没有可移动的路径,则将所在节点加入到栈中,并返回。
参考资料:官方题解
代码如下:
class Solution {
// 图的邻接表实现,由于题目要求要按字典序,所以用优先级队列,如果没有要求或者是int类型,可以用List
Map<String, PriorityQueue<String>> map = new HashMap<>();
List<String> itinerary = new ArrayList<>();
public List<String> findItinerary(List<List<String>> tickets) {
for (List<String> ticket : tickets) {
String src = ticket.get(0);
String dst = ticket.get(1);
if (!map.containsKey(src)) {
map.put(src, new PriorityQueue<String>());
}
map.get(src).offer(dst);
}
dfs("JFK");
Collections.reverse(itinerary);
return itinerary;
}
private void dfs(String cur) {
while (map.containsKey(cur) && map.get(cur).size() > 0) {
String temp = map.get(cur).poll();
dfs(temp);
}
itinerary.add(cur);
}
}
2.753. 破解保险箱
参考资料
代码如下:
class Solution {
Set<Integer> seen = new HashSet<Integer>();
StringBuffer ans = new StringBuffer();
int highest;
int k;
public String crackSafe(int n, int k) {
highest = (int) Math.pow(10, n - 1);
this.k = k;
dfs(0);
for (int i = 1; i < n; i++) {
ans.append('0');
}
return ans.toString();
}
public void dfs(int node) {
for (int x = 0; x < k; ++x) {
int nei = node * 10 + x;
if (!seen.contains(nei)) {
seen.add(nei);
dfs(nei % highest);
ans.append(x);
}
}
}
}
并查集
并查集是一种树型的数据结构。
并查集的作用:
1.处理不相交的集合的合并、查询问题。
2.检查一个无向图是否有环。
3.计算无向图的连通分量数目。
并查集模板:
class DSU {
private int[] parents;
public DSU(int n) {
parents = new int[n];
for (int i = 0; i < n; i ++) {
parents[i] = i;
}
}
// 路径压缩:隔代压缩
public int find(int x) {
while (x != parents[x]) {
parents[x] = parents[parents[x]];
x = parents[x];
}
return x;
}
// 合并
public void union(int x, int y) {
parents[find(x)] = find(y);
}
// 检查是否连接
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
1.684. 冗余连接
代码如下:
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int N = edges.length;
DSU dsu = new DSU(N + 1);
int[] res = new int[2];
for (int[] edge : edges) {
int from = edge[0];
int to = edge[1];
if (!dsu.isConnected(from, to)) {
dsu.union(from, to);
} else {
res[0] = from;
res[1] = to;
}
}
return res;
}
}
class DSU {
private int[] parents;
public DSU(int n) {
parents = new int[n];
for (int i = 0; i < n; i ++) {
parents[i] = i;
}
}
public int find(int x) {
while (x != parents[x]) {
parents[x] = parents[parents[x]];
x = parents[x];
}
return x;
}
public void union(int x, int y) {
parents[find(x)] = find(y);
}
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
2.323. 无向图中连通分量的数目
代码如下:
class Solution {
public int countComponents(int n, int[][] edges) {
int num = n;
DSU dsu = new DSU(n);
for (int[] edge : edges) {
int from = edge[0];
int to = edge[1];
if (!dsu.isConnected(from, to)) {
dsu.union(from, to);
num --;
}
}
return num;
}
}
class DSU {
private int[] parents;
public DSU(int n) {
parents = new int[n];
for (int i = 0; i < n; i ++) {
parents[i] = i;
}
}
public int find(int x) {
while (x != parents[x]) {
parents[x] = parents[parents[x]];
x = parents[x];
}
return x;
}
public void union (int x, int y) {
parents[find(x)] = find(y);
}
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
3.261. 以图判树
代码如下:
class Solution {
// 树是连通量为1,无环图
public boolean validTree(int n, int[][] edges) {
DSU dsu = new DSU(n);
int num = n;
for (int[] edge : edges) {
int from = edge[0];
int to = edge[1];
if (!dsu.isConnected(from, to)) {
dsu.union(from, to);
num --;
} else {
return false;
}
}
return num == 1 ? true : false;
}
}
class DSU {
private int[] parents;
public DSU (int n) {
parents = new int[n];
for (int i = 0; i < n; i ++) {
parents[i] = i;
}
}
public int find(int x) {
while (x != parents[x]) {
parents[x] = parents[parents[x]];
x = parents[x];
}
return x;
}
public void union(int x, int y) {
parents[find(x)] = find(y);
}
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
最小生成树
kruskal 算法:
按照边的权重顺序(从小到大)处理所有的边,将边加入到最小生成树中,加入的边不会与已经加入的边构成环,直到树中含有 N - 1 条边为止。这些边会由一片森林变成一个树,这个树就是图的最小生成树。
1.1135. 最低成本联通所有城市
代码如下:
// Kruskal算法
class Solution {
public int minimumCost(int N, int[][] connections) {
// 从小到大排序
Arrays.sort(connections, (a, b) -> a[2] - b[2]);
DSU dsu = new DSU(N);
int count = 0;
int cost = 0;
for (int[] conn : connections) {
if (!dsu.isConnnect(conn[0], conn[1])) {
dsu.union(conn[0], conn[1]);
count ++;
cost += conn[2];
}
// 最小生成树的边数等于N - 1
if (count == N - 1) {
break;
}
}
if (count != N - 1) {
return -1;
}
return cost;
}
}
// 并查集类
class DSU {
private int[] parent;
public DSU(int N) {
// 顶点序号1~N
parent = new int[N + 1];
for (int i = 0; i <= N; i ++) {
parent[i] = i;
}
}
// 隔代压缩(路径压缩)
private int find(int x) {
while (x != parent[x]) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return parent[x];
}
public void union(int x, int y) {
int parentx = find(x);
int parenty = find(y);
if (parentx != parenty) {
parent[parentx] = parenty;
}
}
public boolean isConnnect(int x, int y) {
return find(x) == find(y);
}
}
回溯算法
1.797. 所有可能的路径
代码如下:
class Solution {
List<List<Integer>> res;
public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
path.add(0);
dfsBacktracking(0, path, graph);
return res;
}
private void dfsBacktracking(int cur, List<Integer> path, int[][] graph) {
if (cur == graph.length - 1) {
res.add(new ArrayList<>(path));
return;
}
for (int node : graph[cur]) {
path.add(node);
dfsBacktracking(node, path, graph);
path.remove(path.size() - 1);
}
}
}
2.1059. 从始点到终点的所有路径
代码如下:
class Solution {
public boolean leadsToDestination(int n, int[][] edges, int source, int destination) {
List<Integer>[] adj = new ArrayList[n];
boolean[] visited = new boolean[n];
boolean[] onPath = new boolean[n];
for (int i = 0; i < n; i ++) {
adj[i] = new ArrayList<>();
}
for (int[] edge : edges) {
int from = edge[0];
int to = edge[1];
adj[from].add(to);
}
return dfs(adj, source, destination, visited, onPath);
}
private boolean dfs(List<Integer>[] adj, int node, int destination, boolean[] visited, boolean[] onPath) {
if (0 == adj[node].size()) {
return node == destination;
}
visited[node] = true;
onPath[node] = true;
for (Integer next : adj[node]) {
if ((visited[next] && onPath[next]) || (!visited[next] && !dfs(adj, next, destination, visited, onPath))) {
return false;
}
}
onPath[node] = false;
return true;
}
}
多源选最短路
floyd算法
1.1334. 阈值距离内邻居最少的城市
代码如下:
class Solution {
public int findTheCity(int n, int[][] edges, int distanceThreshold) {
// floyd用邻接矩阵存储图
int[][] map = new int[n][n];
for (int i = 0; i < n; i ++) {
for (int j = 0; j < n; j ++) {
if (i == j) {
// 自己则为0
map[i][j] = 0;
} else {
// 默认到达不了,设置为+无穷大
map[i][j] = Integer.MAX_VALUE;
}
}
}
for (int[] edge : edges) {
map[edge[0]][edge[1]] = edge[2];
map[edge[1]][edge[0]] = edge[2];
}
// floyd模板
for (int k = 0; k < n; k ++) {
for (int i = 0; i < n; i ++) {
for (int j = 0; j < n; j ++) {
if (i != j && j != k && map[i][k] != Integer.MAX_VALUE && map[k][j] != Integer.MAX_VALUE) {
map[i][j] = Math.min(map[i][j], map[i][k] + map[k][j]);
}
}
}
}
int min = n + 1;
int res = -1;
for (int i = 0; i < n; i ++) {
int count = 0;
for (int j = 0; j < n; j ++) {
if (i != j && map[i][j] <= distanceThreshold) {
count ++;
}
}
if (min >= count) {
min = count;
res = i;
}
}
return res;
}
}
图论代码技巧
1.邻接表实现(Set,List实现都可以)
HashSet<Integer>[] adj = new HashSet[numCourses];
for (int i = 0; i < numCourses; i ++) {
adj[i] = new HashSet<>();
}
for (int[] p : prerequisites) {
int from = p[1];
int to = p[0];
adj[from].add(to);
}
本文详细介绍了图论中的拓扑排序、欧拉图、并查集、最小生成树、回溯算法和多源最短路等概念,结合具体算法如Hierholzer、Kruskal和Floyd,并给出了力扣相关题目作为实践示例。
168万+

被折叠的 条评论
为什么被折叠?



