计算机基础速通--数据结构·图的基础应用二(基础图算法)

如有问题大概率是我的理解比较片面,欢迎评论区或者私信指正。

最近了解到了一个新的改变和提高自己的方法·时刻记录不论多小的事情都记下,我目前用了4天,之前感觉一天天忙死但没啥收获,但是记录了之后知道自己的时间花在了哪里,后期该如何调整,并且可以很好缓解焦虑情绪。

一、最小生成树(最小代价树)

1、基础概念

最小生成树基本概念

MST的定义:权值和最小的生成树(连通所有顶点、无环、边数=顶点数-1、砍掉一条边则不连通)。

性质:权值和唯一,但树形可能不唯一(存在相同权值的边时)。

适用场景:连通图(非连通图只有生成森林)。

Prim算法

核心思想贪心策略,从单个顶点逐步扩展,每次添加与当前树连接的最小权值边对应的顶点。

实现关键

  • 维护lowCost[]数组存储顶点到生成树的最小代价。
  • isJoin[]标记顶点是否已加入树(避免成环)。

时间复杂度(适合稠密图)。

Kruskal算法

核心思想:贪心策略、按边权升序选择边、避免成环。

实现关键

边排序

并查集(Union-Find)判断边的两点是否连通。

时间复杂度(适合稀疏图)。

两种算法对比

适用场景:稠密图用Prim,稀疏图用Kruskal。

时间复杂度差异:Prim依赖顶点数,Kruskal依赖边数。

2、Prim 算法(普⾥姆)

数据结构

lowCost[]:记录各顶点加入生成树的最小代价(初始:起点为0,直连顶点为边权,非直连为∞)

isJoin[]:标记顶点是否已加入生成树(初始:仅起点为true

核心流程

for i in range(n-1):  # 共n-1轮# 1. 扫描lowCost找最小值顶点u(未加入的顶点)u = find_min(lowCost, isJoin)  isJoin[u] = True  # 加入生成树# 2. 更新邻接点v的lowCostfor v in graph[u]:if not isJoin[v] and graph[u][v] < lowCost[v]:lowCost[v] = graph[u][v]

时间复杂度

朴素实现:O(|V|^2)

堆优化:O(|E| log |V|)(用优先队列存(lowCost, v)

易错点

初始化错误

  • 未将起点lowCost设为0(误设为∞)
  • 未标记起点isJoin=True(导致重复加入)

更新逻辑错误

  • 更新邻接点时未检查 isJoin[v](已加入顶点不应更新)
  • 错误更新非邻接点(应只更新当前顶点的邻接点)

非连通图处理

  • 未检查是否加入所有顶点(若未完成但lowCost全为∞,说明图不连通)

权值比较错误

  • 更新时用边权与旧lowCost比较(正确:graph[u][v] < lowCost[v]
  • 未处理重边(应取最小权值边)
基础模板如下:

import java.util.*;public class PrimMST {// 朴素版Prim算法实现(O(V^2))public static int prim(int[][] graph) {int n = graph.length;// 易错点1:初始化 - 未正确设置起点会导致逻辑错误int[] lowCost = new int[n];      // 各顶点加入生成树的最小代价boolean[] isJoin = new boolean[n]; // 标记顶点是否已加入生成树Arrays.fill(lowCost, Integer.MAX_VALUE);lowCost[0] = 0; // 起点最小代价设为0(易错点1.1:起点设为0)int totalCost = 0;int selectedCount = 0; // 已选顶点计数(用于检测非连通图)// 核心流程:共需选择n个顶点(包括起点)for (int i = 0; i < n; i++) {// 1. 寻找当前lowCost中的最小值顶点uint u = -1;for (int v = 0; v < n; v++) {// 只考虑未加入的顶点(易错点2:已加入顶点不能重复选择)if (!isJoin[v] && (u == -1 || lowCost[v] < lowCost[u])) {u = v;}}isJoin[u] = true; // 将u加入生成树totalCost += lowCost[u];selectedCount++;// 2. 更新u的邻接点的lowCost值for (int v = 0; v < n; v++) {// 易错点4:更新条件判断(必须同时满足三个条件)if (!isJoin[v] && graph[u][v] != 0 && graph[u][v] < lowCost[v]) {// 注意:graph[u][v]!=0 表示存在边(0表示无直接边)// 易错点5:权值比较(需严格小于当前值才更新)lowCost[v] = graph[u][v];}}}// 易错点3补充:检查是否加入全部顶点if (selectedCount != n) {System.out.println("图不连通!");return -1;}return totalCost;}public static void main(String[] args) {int[][] graph = {{0, 1, 5, 6},{1, 0, 5, 0},  // 易错点7:无向图需要对称{5, 5, 0, 4},{6, 0, 4, 0}};int cost = prim(graph);if (cost != -1) {System.out.println("最小生成树权值和: " + cost); // 正确结果:10}}
}
练习1:训练Prim的二维坐标处理能力

1584. 连接所有点的最小费用 - 力扣(LeetCode)https://leetcode.cn/problems/min-cost-to-connect-all-points/description/?utm_source=LCUS&utm_medium=ip_redirect&utm_campaign=transfer2china

class Solution {public int minCostConnectPoints(int[][] points) {int n=points.length;//标记数组boolean[] isJion=new boolean[n];//最小代价数组int[] lowCost=new int[n];//初始化Arrays.fill(isJion,false);Arrays.fill(lowCost,Integer.MAX_VALUE);lowCost[0]=0;int selectedCount=0;//非连通图检测//记录最终答案int ans=0;//prim算法for(int i=0;i<n;i++){//依次将lowCost中代价最小的节点加入生成树int u=-1;//找出lowCost中代价最小的节点for(int v=0;v<n;v++){//节点未加入生成树 && (第一次·生成树的根节点 || 更小花费)if(!isJion[v] && (u==-1 || lowCost[v]<lowCost[u])){u=v;}}//加入生成树并处理isJion[u]=true;ans+=lowCost[u];selectedCount++;//更新邻接点的lowCostfor(int v=0;v<n;v++){if(!isJion[v] && cost(u,v,points)<lowCost[v]){lowCost[v]=cost(u,v,points);}}}return ans;}public int cost(int u,int v,int[][] points){return Math.abs(points[u][0]-points[v][0])+Math.abs(points[u][1]-points[v][1]);}
}

3、Kruskal 算法(克鲁斯卡尔)

数据结构

边集数组:按权值排序 edges = [(weight, u, v), ...]

并查集:判断连通性(初始每个顶点独立集合)

核心流程

edges.sort()  # 按权值升序排序
for weight, u, v in edges:if find(u) != find(v):  # 不连通union(u, v)         # 合并集合add_edge(u, v)      # 加入生成树if edges_added == n-1: break  # 边数达标终止

时间复杂度

排序:O(|E| log |E|)

并查集:O(\alpha(|V|))每轮(近似常数)

排序遗漏

  • 未对边排序直接遍历(破坏贪心策略正确性)

并查集错误

  • 未初始化并查集(每个顶点应为独立集合)
  • 合并后未更新父节点(需路径压缩优化)

环判断错误

  • 未检查连通性直接加边(应用并查集而非DFS/BFS判环)

终止条件缺失

  • 未在边数达n-1时提前终止(多余计算)

重边处理

  • 未去重直接排序(应保留最小权值重边)
import java.util.*;public class KruskalMST {// 边类static class Edge implements Comparable<Edge> {int src, dest, weight;public Edge(int src, int dest, int weight) {this.src = src;this.dest = dest;this.weight = weight;}@Overridepublic int compareTo(Edge other) {// 易错点1:排序遗漏 - 必须按权值排序return this.weight - other.weight;}}// 并查集类static class UnionFind {int[] parent, rank;public UnionFind(int n) {parent = new int[n];rank = new int[n];// 易错点2:并查集未初始化 - 每个顶点独立集合for (int i = 0; i < n; i++) {parent[i] = i;  // 初始每个顶点独立成集合}}public int find(int x) {// 路径压缩优化if (parent[x] != x) {parent[x] = find(parent[x]);}return parent[x];}public void union(int x, int y) {int rootX = find(x);int rootY = find(y);if (rootX == rootY) return;// 按秩合并if (rank[rootX] < rank[rootY]) {parent[rootX] = rootY;} else if (rank[rootX] > rank[rootY]) {parent[rootY] = rootX;} else {parent[rootY] = rootX;rank[rootX]++;}}}public static List<Edge> kruskal(List<Edge> edges, int n) {// 易错点1:排序遗漏 - 必须先对边排序Collections.sort(edges);UnionFind uf = new UnionFind(n);List<Edge> mst = new ArrayList<>();int edgesAdded = 0;for (Edge edge : edges) {// 易错点3:环判断错误 - 必须检查连通性if (uf.find(edge.src) != uf.find(edge.dest)) {uf.union(edge.src, edge.dest);mst.add(edge);edgesAdded++;// 易错点4:终止条件缺失 - 边数达标提前终止if (edgesAdded == n - 1) break;}}// 检查是否形成完整生成树if (edgesAdded != n - 1) {System.out.println("图不连通,无法形成最小生成树");return null;}return mst;}public static void main(String[] args) {int n = 4;List<Edge> edges = new ArrayList<>();edges.add(new Edge(0, 1, 1));edges.add(new Edge(0, 2, 5));edges.add(new Edge(0, 3, 6));edges.add(new Edge(1, 2, 5));edges.add(new Edge(2, 3, 4));// 处理重边(可选)// 易错点5:重边处理 - 保留最小权值边// 本例无重边,实际应用中需要预处理List<Edge> mst = kruskal(edges, n);if (mst != null) {System.out.println("最小生成树边集:");int totalWeight = 0;for (Edge edge : mst) {System.out.println(edge.src + " - " + edge.dest + " : " + edge.weight);totalWeight += edge.weight;}System.out.println("总权重: " + totalWeight); // 正确结果:4+2+4+4+4=18}}
}
练习2:掌握Kruskal并查集实现

1584. 连接所有点的最小费用 - 力扣(LeetCode)https://leetcode.cn/problems/min-cost-to-connect-all-points/?utm_source=LCUS&utm_medium=ip_redirect&utm_campaign=transfer2china

class Solution {public int minCostConnectPoints(int[][] points) {//极端if(points.length==0 || points==null)return 0;//边List<Eadge> eadges=new ArrayList<>();//并查集UnionFind uf=new UnionFind(points.length);//终止条件int selectedCount=0;int ans=0;//初始化边for(int v=0;v<points.length;v++){for(int u=v+1;u<points.length;u++){eadges.add(new Eadge(v,u,cost(v,u,points)));}}//排序Collections.sort(eadges);for(Eadge e:eadges){//判断连通性if(uf.find(e.s)!=uf.find(e.e)){uf.union(e.s,e.e);selectedCount++;ans+=e.weight;}//终止if(selectedCount==points.length-1)break;}return ans;}public int cost(int u,int v,int[][] points){return Math.abs(points[u][0]-points[v][0])+Math.abs(points[u][1]-points[v][1]);}
}//边类
class Eadge implements Comparable<Eadge>{int s;int e;int weight;public Eadge(int s,int e,int weight){this.s=s;this.e=e;this.weight=weight;}//重写比较·升序@Overridepublic int compareTo(Eadge other) {return this.weight - other.weight;}
}//并查集类class UnionFind {//双亲数组·记录当前下标节点的双亲节点下标int[] parent;//rank数组·union优化int[] rank;//初始化public UnionFind(int n){parent=new int[n];rank=new int[n];Arrays.fill(rank,0);//初始·每个节点都是独立集合for(int i=0;i<n;i++){parent[i]=i;}}//查找·路径优化public int find(int x){//终止条件·根节点的父节点是自己本身if(parent[x]!=x){parent[x]=find(parent[x]);}return parent[x];}//合并·高度压缩(小树并大树上)public void union(int x,int y){//找根int v=find(x);int u=find(y);if(v==u)return ;//同一个集合不需要合并//检查秩if(rank[v]<rank[u]){parent[v]=u;}else if(rank[v]>rank[u]){parent[u]=v;}else{parent[v]=u;rank[v]++;}}}

4、综合练习·考察MST关键边的理解

1489. 找到最小生成树里的关键边和伪关键边 - 力扣(LeetCode)https://leetcode.cn/problems/find-critical-and-pseudo-critical-edges-in-minimum-spanning-tree/description/

二、最短路径

基础

1. 算法适用场景
算法无权图带权图负权边负权回路单源/多源
BFS单源(无权图)
Dijkstra单源(带权图)
Floyd多源(任意两点)
2. 核心原理
  • BFS:层序遍历,距离=层数(权值视为1)。

  • Dijkstra:贪心策略,每次选距离起点最近的未访问点,更新邻接点。

  • Floyd:动态规划,A[k][i][j] = min(A[k-1][i][j], A[k-1][i][k] + A[k-1][k][j])

3. 时间复杂度
算法时间复杂度空间复杂度
BFSO(|V| + |E|)O(|V|)
Dijkstra(基础)O(|V|²)O(|V|)
FloydO(|V|³)O(|VBFS

BFS

问题1:“为什么DFS不适合?权值不等时为何BFS失效?”(为何BFS适合解决​​无权图单源最短路径​​(每条边权值为1,路径长度=边数))

​答案​​:DFS可能探索非最短路径;权值不等时路径长度≠边数,需Dijkstra算法。

问题2:代码中的​​三个核心数组​

  • d[]:记录源点到各顶点的最短距离(初始化为)。
  • visited[]:标记顶点是否被访问(初始化为false)。
  • path[]:记录路径前驱(初始化为-1)。
if (!visited[w]) {d[w] = d[u] + 1;  // 更新距离path[w] = u;       // 记录前驱visited[w] = TRUE;EnQueue(Q, w);
}

问题3:“BFS与Dijkstra在权值为1时的关系?”

​答案​​:BFS是Dijkstra在无权图中的特例(优先级队列退化为普通队列)。

问题4:若图非连通,部分顶点d[]保持∞,如何处理?

​方法​​:遍历所有顶点,对未访问顶点再次调用BFS。

问题5:

“如何输出完整路径?”→ 用path[]回溯。

“若需记录多条最短路径?”→ 将path[]改为存储链表。

基础代码实现:

import java.util.*;public class BFSShortestPath {private List<List<Integer>> graph; // 邻接表存储图结构private int[] dist;                // 存储源点到各顶点的最短距离private int[] parent;              // 存储最短路径的前驱节点private boolean[] visited;         // 访问标记数组public void bfsMinDistance(int start) {// 使用graph的大小(包含索引0)int n = graph.size();dist = new int[n];parent = new int[n];visited = new boolean[n];// 初始化Arrays.fill(dist, Integer.MAX_VALUE);Arrays.fill(parent, -1);Queue<Integer> queue = new LinkedList<>();dist[start] = 0;visited[start] = true;queue.offer(start);while (!queue.isEmpty()) {int u = queue.poll();// 安全遍历(防止空指针)if (graph.get(u) != null) {for (int w : graph.get(u)) {if (!visited[w]) {dist[w] = dist[u] + 1;parent[w] = u;visited[w] = true;queue.offer(w);}}}}}// 重建最短路径public List<Integer> getPath(int target) {LinkedList<Integer> path = new LinkedList<>();for (int v = target; v != -1; v = parent[v]) {path.addFirst(v);}return path;}// 图初始化public static void main(String[] args) {BFSShortestPath bfs = new BFSShortestPath();int vertexCount = 8; // 顶点数量int arraySize = vertexCount + 1; // 数组大小=顶点数+1// 创建包含索引0~8的图(共9个位置)bfs.graph = new ArrayList<>(arraySize);for (int i = 0; i < arraySize; i++) {bfs.graph.add(new ArrayList<>());}// 添加图的边(索引1~8对应顶点1~8)bfs.graph.get(1).add(2); bfs.graph.get(2).add(1);bfs.graph.get(1).add(5); bfs.graph.get(5).add(1);bfs.graph.get(2).add(6); bfs.graph.get(6).add(2);bfs.graph.get(6).add(3); bfs.graph.get(3).add(6);bfs.graph.get(6).add(7); bfs.graph.get(7).add(6);bfs.graph.get(3).add(4); bfs.graph.get(4).add(3);bfs.graph.get(7).add(8); bfs.graph.get(8).add(7);bfs.bfsMinDistance(2); // 从顶点2(索引2)开始System.out.println("从2到8的最短距离: " + bfs.dist[8]);System.out.println("路径: " + bfs.getPath(8)); // [2, 6, 7, 8]}
}

练习:1091. 二进制矩阵中的最短路径 - 力扣(LeetCode)https://leetcode.cn/problems/shortest-path-in-binary-matrix/

class Solution {//移动数组int[][] dir=new int[][]{{-1,1},{0,1},{1,1},{-1,0},{1,0},{-1,-1},{0,-1},{1,-1}};public int shortestPathBinaryMatrix(int[][] grid) {//起始判断if(grid[0][0]==1)return -1;int n=grid.length;//路径距离int[][] dist=new int[n][n];//路径节点int[][][] path=new int[n][n][2];//标记数组boolean[][] visited=new boolean[n][n];//初始化for(int i=0;i<n;i++){Arrays.fill(dist[i],Integer.MAX_VALUE);}Deque<int[]> deque=new ArrayDeque<>();//起始节点入队deque.add(new int[]{0,0});visited[0][0]=true;dist[0][0]=1;path[0][0]=new int[]{-1,-1};while(!deque.isEmpty()){//出队int[] node=deque.poll();//处理int x=node[0],y=node[1];if(x==n-1 && y==n-1){PrintPath(grid,path);return dist[x][y];}//邻居入队for(int i=0;i<8;i++){int newx=x+dir[i][0];int newy=y+dir[i][1];//边界if(newx<0 || newx>=n || newy<0 || newy>=n)continue;//符合条件的邻居if(!visited[newx][newy] && grid[newx][newy]==0){deque.add(new int[]{newx,newy});dist[newx][newy]=dist[x][y]+1;visited[newx][newy]=true;path[newx][newy]=new int[]{x,y};}}}return -1;}//输出路径public void PrintPath(int[][] grid,int[][][] path){int n=grid.length;int x=path[n-1][n-1][0];int y=path[n-1][n-1][1];System.out.print("("+ (n-1) +", "+(n-1) +")"+"<-");while(x!=-1 && y!=-1){System.out.print("("+ x +", "+y +")"+"<-");int[] node=path[x][y];x=node[0];y=node[1];}}}

为了更加直观,添加了路径打印

Dijkstra

重点理解更新过程与负权图的局限性

1. 初始化阶段

易错:漏掉不可达顶点的初始化

正确:dist[V] = ∞path[V] = -1

难点:源点到自身(dist[V0]=0)和直接邻接点的处理

2. 顶点选择(每轮循环)
  • 易错:选择已确定顶点(final[i]=true的顶点不应再选)

  • 难点:当存在多个相同dist值时,选择任意一个均可(需说明)

3. 更新邻接点
  • 核心公式
    若 dist[i] + arcs[i][j] < dist[j],则更新 dist[j] 和 path[j]

  • 易错

    • 未检查final[j]==false

    • 忽略不可达顶点(∞参与计算)

  • 难点:理解"松弛操作"本质(通过新顶点缩短路径)

4. 负权图问题

负权值的破坏性​​:

当存在负权边(如 V₁→V₂ 权值为 -5),已标记为 final=true的节点可能被​​更短路径推翻​

算法路径:V₀→V₂(dist=7)

实际更优路径:V₀→V₁→V₂(10 + (-5) = 5)

但 V₂ 被提前标记为 final=true,导致无法更新:

import java.util.Arrays;public class DijkstraAlgorithm {// 使用邻接矩阵存储图(易错点1:需确保矩阵对称性)private static final int INF = Integer.MAX_VALUE; // 表示无穷大public static void dijkstra(int[][] graph, int source) {int n = graph.length;int[] dist = new int[n];       // 最短路径长度数组int[] path = new int[n];       // 前驱节点数组(易错点2:需记录路径)boolean[] finalSet = new boolean[n]; // 已确定最短路径的顶点集// 初始化三个核心数组(关键点1)Arrays.fill(dist, INF);Arrays.fill(path, -1);dist[source] = 0;  // 源点到自身距离为0// 主循环:执行n-1轮(关键点2)for (int count = 0; count < n - 1; count++) {// 步骤1:找当前dist最小的未确定节点int u = -1;int minDist = INF;for (int v = 0; v < n; v++) {if (!finalSet[v] && dist[v] <= minDist) { // 易错点3:注意等号minDist = dist[v];u = v;}}if (u == -1) break; // 所有顶点已处理完finalSet[u] = true; // 标记为已确定// 步骤2:更新邻接点(关键点3)for (int v = 0; v < n; v++) {// 易错点4:需同时检查4个条件if (!finalSet[v] &&graph[u][v] != INF &&dist[u] != INF &&dist[u] + graph[u][v] < dist[v]) {dist[v] = dist[u] + graph[u][v];path[v] = u; // 记录前驱节点}}}// 打印结果(示例)printSolution(dist, path, source);}private static void printSolution(int[] dist, int[] path, int source) {System.out.println("源点: " + source);System.out.println("顶点\t距离\t路径");for (int i = 0; i < dist.length; i++) {System.out.printf("%d\t%d\t", i, dist[i]);printPath(path, i);System.out.println();}}// 递归打印路径(关键点4)private static void printPath(int[] path, int current) {if (current == -1) return;printPath(path, path[current]);System.out.print(current + " ");}public static void main(String[] args) {int[][] graph = {{0, 10, INF, INF, 5},{INF, 0, 1, INF, 2},{INF, INF, 0, 4, INF},{7, INF, 6, 0, INF},{INF, 3, 9, 2, 0}};dijkstra(graph, 0);}
}

练习:

743. 网络延迟时间 - 力扣(LeetCode)https://leetcode.cn/problems/network-delay-time/description/?utm_source=LCUS&utm_medium=ip_redirect&utm_campaign=transfer2china

class Solution {public int networkDelayTime(int[][] times, int n, int k) {//异常判断if(times.length==0 || times==null || k>n)return -1;//构建图int[][] g=new int[n][n];final int INF =Integer.MAX_VALUE/2;for(int i=0;i<n;i++){Arrays.fill(g[i],INF);//g[x][y]==INF 无边}for(int[] t:times){int x=t[0]-1,y=t[1]-1;g[x][y]=t[2];}//初始化boolean[] used=new boolean[n];//used[x]==true 存在x到源点最短距离int[] dist=new int[n];//dist[x] 源点到x最短距离Arrays.fill(dist,INF);dist[k-1]=0;int[] path=new int[n];Arrays.fill(path,-1);//计算其他n-1个节点的distfor(int i=0;i<n-1;i++){//找最短距离的邻居(第一轮是初始节点)int u=-1;for(int v=0;v<n;v++){if(!used[v] && (u==-1 || dist[v]<dist[u]))u=v;}used[u]=true;//更新distfor(int j=0;j<n;j++){if(!used[j] && g[u][j]!=INF & dist[u]!=INF && dist[u]+g[u][j]<dist[j]){dist[j]=dist[u]+g[u][j];path[j]=u;}}}//直观演示// Print(path,dist,k-1);int ans=Arrays.stream(dist).max().getAsInt();return ans==INF? -1:ans;}public void Print(int[] path,int[] dist,int source){System.out.println("源点: " + source);System.out.println("顶点\t距离\t路径");for (int i = 0; i < dist.length; i++) {System.out.printf("%d\t%d\t", i, dist[i]);printPath(path, i);System.out.println();}}public void printPath(int[] path,int curr){if(curr==-1)return ;printPath(path,path[curr]);System.out.print(curr+"->");} 
}

Floyd

1、算法思想(建立直观理解)

通过逐步增加中转点,动态更新所有顶点之间的最短路径

初始化:假设一张地图有多个城市(顶点),先记录所有城市间直达的路径长度(没有直达记为无穷大)。

动态中转:逐个考虑每个城市作为中转站(比如先允许通过城市A中转,再允许通过A和B中转,以此类推)。

检查优化:对于每一对城市(比如X到Y),检查如果从X先到中转站K,再从K到Y,总距离是否比当前已知的X到Y路径更短。

如果更短:更新X到Y的最短路径为 X->K->Y 的路径,并记录这个中转站K;否则:保持原有路径。

完成:当所有城市都作为中转站被考虑过后,最终得到任意两城市间的最短路径。


想象你要为所有城市之间规划最短快递路线。Floyd的做法是:

  1. 第一轮:只允许快递从起点直达终点。

  2. 第二轮:允许快递在城市A中转一次(比如 上海->A->广州 可能比 上海->广州 更快)。

  3. 第三轮:允许在城市A和B中转,依此类推...
    直到所有城市都可作为中转站,最终得到全局最优路线。

关键

  • 能处理任意两点间的最短路径(单源/多源都适用)。

  • 支持带权图(包括负权边,但不能有负权环路)。

  • 本质是动态规划,通过三重循环实现(外层控制中转点)。

可以去看看这个博主的讲解,很直观:

图-最短路径-Floyd(弗洛伊德)算法_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV19k4y1Q7Gj?spm_id_from=333.788.videopod.sections&vd_source=4b89f462036a892baf8931104a1f36b1

注意:视频的path数组初始化和本文所采用的初始化方式不同(本文参考王道教材),但大体算法思路一致,可以参考。
算法演示:

Floyd-Warshall All-Pairs Shortest Pathhttps://www.cs.usfca.edu/~galles/visualization/Floyd.html

2、代码实现·明确易错点和难点

距离矩阵 dist

定义与初始化

int[][] dist = new int[n][n];
  • 含义dist[i][j] 表示从顶点 i 到顶点 j 的当前已知最短路径长度

  • 初始化

    • 如果 i == j,则 dist[i][j] = 0(顶点到自身距离为0)

    • 如果存在边 i→j,则 dist[i][j] = weight(i, j)(边的权重)

    • 如果不存在边 i→j,则 dist[i][j] = Integer.MAX_VALUE(表示无穷大)

更新原理(动态规划核心)

if (dist[i][k] + dist[k][j] < dist[i][j]) {dist[i][j] = dist[i][k] + dist[k][j];
}
  • 物理意义:检查通过中间点 k 的路径是否比当前路径更短

  • 状态转移方程

    dist^{(k)}[i][j] = \min \left( dist^{(k-1)}[i][j],\ dist^{(k-1)}[i][k] + dist^{(k-1)}[k][j] \right)

  • 动态规划思想:当考虑允许通过顶点 k 中转时,更新所有 i→j 的路径

路径矩阵 path

定义与初始化

int[][] path = new int[n][n];
  • 含义path[i][j] 记录从 i 到 j 的最短路径上的第一个中转点

  • 初始化

    • path[i][j] = -1 表示没有中转点(直接相连)

    • path[i][j] = k 表示路径 i→j 需要经过中转点 k

更新原理

if (dist[i][k] + dist[k][j] < dist[i][j]) {path[i][j] = k;  // 记录中转点
}
  • 关键点:当发现通过 k 中转的路径更短时,记录这个关键中转点

  • 路径重建原理:路径信息是递归存储的:

    • 要获取 i→j 的完整路径,需要:

      1. 找到 path[i][j] = k

      2. 递归查找 i→k 的路径

      3. 递归查找 k→j 的路径

路径重建算法

void printPath(int i, int j, int[][] path) {if (path[i][j] == -1) {System.out.print(j + " "); // 直接连接return;}int k = path[i][j];printPath(i, k, path); // 前半段路径printPath(k, j, path); // 后半段路径
}
import java.util.Arrays;public class FloydAlgorithm {public static void floyd(int[][] graph) {int n = graph.length;// 初始化距离矩阵(存储最短路径长度)int[][] dist = new int[n][n];// 初始化路径矩阵(存储中转点)int[][] path = new int[n][n];// 易错点1:正确初始化矩阵for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {dist[i][j] = graph[i][j];  // 复制原始图// 难点1:路径矩阵初始化// -1表示没有中转点(直接相连)path[i][j] = -1;}}// 核心三重循环// 易错点2:循环顺序必须是k->i->jfor (int k = 0; k < n; k++) {          // 考虑每个顶点作为中转点for (int i = 0; i < n; i++) {      // 遍历所有起点// 难点2:跳过无效路径提升性能if (dist[i][k] == Integer.MAX_VALUE) continue;for (int j = 0; j < n; j++) {  // 遍历所有终点// 跳过不可达路径if (dist[k][j] == Integer.MAX_VALUE) continue;// 发现更短路径// 易错点3:避免整数溢出long newDist = (long) dist[i][k] + dist[k][j];if (newDist < dist[i][j]) {dist[i][j] = (int) newDist;path[i][j] = k;  // 记录中转点}}}}// 打印最终结果printSolution(dist, path);}// 递归打印路径(难点3:路径回溯)private static void printPath(int[][] path, int i, int j) {if (path[i][j] == -1) {System.out.print(" → " + j);return;}printPath(path, i, path[i][j]);  // 前半段路径printPath(path, path[i][j], j);  // 后半段路径}private static void printSolution(int[][] dist, int[][] path) {int n = dist.length;System.out.println("距离矩阵:");for (int[] row : dist) {System.out.println(Arrays.toString(row));}System.out.println("\n路径矩阵:");for (int[] row : path) {System.out.println(Arrays.toString(row));}System.out.println("\n详细路径:");for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {if (i != j && dist[i][j] != Integer.MAX_VALUE) {System.out.print(i + "→" + j + ": " + i);printPath(path, i, j);System.out.println(" (长度: " + dist[i][j] + ")");}}}}public static void main(String[] args) {// 示例图(使用MAX_VALUE表示无穷大)int INF = Integer.MAX_VALUE;int[][] graph = {{0,   6,  13},{10,  0,   4},{5, INF,   0}};floyd(graph);}
}
3、练习1334. 阈值距离内邻居最少的城市 - 力扣(LeetCode)https://leetcode.cn/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/?utm_source=LCUS&utm_medium=ip_redirect&utm_campaign=transfer2china
class Solution {public int findTheCity(int n, int[][] edges, int distanceThreshold) {//极端条件if(edges==null || edges.length==0)return 0;int INF=Integer.MAX_VALUE/2;//构建图int[][] g=new int[n][n];for(int i=0;i<n;i++){Arrays.fill(g[i],INF);}for(int[] e:edges){int x=e[0],y=e[1];g[x][y]=e[2];g[y][x]=e[2];//无向图}//距离数组int[][] dist=new int[n][n];//路径数组·存中转点int[][] path=new int[n][n];//初始化for(int i=0;i<n;i++){Arrays.fill(path[i],-1);}for(int i=0;i<n;i++){for(int j=0;j<n;j++){if(i==j)dist[i][j]=0;else dist[i][j]=g[i][j];}}//floy流程for(int i=0;i<n;i++){//中转点for(int s=0;s<n;s++){//源点if(dist[s][i]==INF)continue;//源点无法到达中转点for(int e=0;e<n;e++){//终点if(dist[i][e]==INF)continue;//中转点无法到达终点//更新dist与path·发现通过中转点有更小的路径就更新int curr=dist[s][i]+dist[i][e];if(curr<dist[s][e]){dist[s][e]=curr;path[s][e]=i;}}}}//结果int max_index=-1;int min_city=INF/2;for(int i=0;i<n;i++){int count=0;for(int j=0;j<n;j++){if(dist[i][j]<=distanceThreshold)count++;}if(count<=min_city){min_city=count;max_index=i;//i是递增的}}// printSolution(dist,path);//可视化return max_index;}// 递归打印路径private static void printPath(int[][] path, int i, int j) {if (path[i][j] == -1) {System.out.print(" → " + j);return;}printPath(path, i, path[i][j]);  // 前半段路径printPath(path, path[i][j], j);  // 后半段路径}private static void printSolution(int[][] dist, int[][] path) {int n = dist.length;System.out.println("距离矩阵:");for (int[] row : dist) {System.out.println(Arrays.toString(row));}System.out.println("\n路径矩阵:");for (int[] row : path) {System.out.println(Arrays.toString(row));}System.out.println("\n详细路径:");for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {if (i != j && dist[i][j] != Integer.MAX_VALUE) {System.out.print(i + "→" + j + ": " + i);printPath(path, i, j);System.out.println(" (长度: " + dist[i][j] + ")");}}}}
}
4、细节处理

无穷大(∞)的处理

代码中需用足够大的数表示∞,注意避免运算溢出。

Floyd与Dijkstra的区别?

Floyd:解决所有顶点对最短路径,支持负权边,时间复杂度O(|V|³)。

Dijkstra:解决单源最短路径,不支持负权边,时间复杂度O(|V|²)。

为何Floyd能处理负权边,但不支持负权回路?

负权边可通过中转优化路径。

负权回路导致路径长度无限降低,无确定最短路径。

如何用Floyd检测负权回路?

检查最终距离矩阵A对角线:若存在A[i][i] < 0,则顶点i在负权回路中。

空间复杂度优化?

使用单个矩阵滚动更新(无需保留所有阶段矩阵),空间复杂度O(|V|²)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/919053.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/919053.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式-策略模式 Java

模式概述 策略模式是一种行为型设计模式&#xff0c;它通过定义一系列可互换的算法&#xff0c;并将每个算法封装成独立类&#xff0c;使客户端能够根据需要动态切换算法 简单代码示例 // 1. 抽象策略接口 interface PaymentStrategy {void pay(int amount); }// 2. 具体策略实…

【机器学习深度学习】客观评估训练程度

目录 前言 一、什么是客观评估&#xff1f; 二、客观评估的两大核心方法 1. 判别式评测&#xff08;Discriminative Evaluation&#xff09; 2. 生成式评测&#xff08;Generative Evaluation&#xff09; 三、为什么客观评估成本更高&#xff1f; 1.训练目标收紧 2.训…

Linux软件编程:线程间通信

目录 一、线程间通信基础 1. 概念 2. 通信基础&#xff1a;共享空间 二、互斥锁&#xff08;Mutex&#xff09; 1. 概念 2. 使用流程 3. 函数接口 三、死锁 1. 概念 2. 死锁产生的 4 个必要条件 3. 避免死锁的方法 四、信号量&#xff08;Semaphore&#xff09; 1…

【学习笔记】JVM GC回收机制

1.三种基本的垃圾回收算法 1>标记-清除法 ①先将从树根开始&#xff0c;可以到达的对象标记为可达&#xff08;JVM中的对象们存储为一颗树&#xff09; ②将没有标记的对象清除掉 缺点&#xff1a;会产生大量内存碎片 2>复制算法&#xff08;新生代&#xff09; ①先将a区…

软件的终极:为70亿人编写70亿个不同的软件

这是个脑洞大开的想法。昨天晚上&#xff0c;我在用Claude code帮我写一个小工具&#xff0c;用来管理我本地那些乱七八糟的文档。写着写着&#xff0c;突然意识到一个问题&#xff1a;这个工具完全是按照我的工作习惯定制的——我喜欢用Markdown&#xff0c;习惯把TODO放在文件…

LakeHouse--湖仓一体架构

大家可能发现了,近些年湖仓一体数据架构被提及的频率越来越高。各家大厂也有湖仓一体架构的实践,也有很多公开分享。 那什么是湖仓一体?为什么出现了湖仓一体架构,换言之,它解决了以前数据仓库、数据湖+数仓两层架构所不能解决的什么问题? 本文会从数仓、数据湖依次介绍…

基于FPGA的实时图像处理系统(1)——SDRAM回环测试

SDRAM回环设计 文章目录SDRAM回环设计一、SDRAM简介1、引脚2、内部结构框图3、操作指令二、系统设计三、实现流程1、SDRAM接口2、FIFO设置3、内部SDRAM的控制模块4、其他四、实现效果五、总结六、代码1、top2、sdram_top3、sdram_ctrl一、SDRAM简介 SDRAM英文全称“Synchronou…

一键检测接口是否存活:用 Python/Shell 写个轻量级监控脚本

网罗开发&#xff08;小红书、快手、视频号同名&#xff09;大家好&#xff0c;我是 展菲&#xff0c;目前在上市企业从事人工智能项目研发管理工作&#xff0c;平时热衷于分享各种编程领域的软硬技能知识以及前沿技术&#xff0c;包括iOS、前端、Harmony OS、Java、Python等方…

优秀工具包-Hutool工具详解

优秀工具包-Hutool工具详解 课程概述 Hutool简介 定位&#xff1a; 小而全的Java工具库&#xff0c;简化开发流程。对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装。 核心优势&#xff1a;零依赖、高性能、中文网页完善。 应用场景&#xff1a;Web开发、数…

《深度解构:构建浏览器端Redis控制台的WebSocket协议核心技术》

Redis作为高性能的内存数据库,其原生客户端多依赖命令行或桌面应用,而浏览器端控制台的缺失,成为制约Web化管理的关键瓶颈,WebSocket协议的出现,打破了HTTP协议单向通信的局限,为浏览器与Redis服务之间建立持久、双向的实时连接提供了可能。本文将从协议本质、交互逻辑、…

Pushgateway安装和部署,以及对应Prometheus调整

目录Pushgateway简介安装验证Prometheus的配置&#xff1a;其它命令Pushgateway简介 Pushgateway 是 Prometheus 生态系统中的一个组件。主要特点是推送而非拉取&#xff1a;Prometheus 默认采用拉取&#xff08;pull&#xff09;模式收集指标&#xff0c;但 Pushgateway 允许…

JAVA面试汇总(四)JVM(一)

久违的重新写了一篇面试汇总的&#xff0c;关于JVM的一篇&#xff0c;一共三篇&#xff0c;今天写了第一篇&#xff0c;继续重新学习&#xff0c;重新卷起来&#xff0c;come on baby 1.什么情况下会触发类的初始化&#xff1f; &#xff08;1&#xff09;首先是类未被初始化时…

Agent中的memory

rag系列文章目录 文章目录rag系列文章目录前言一、Memory机制作用二、memory分类三、langgraph实践总结前言 众所周知&#xff0c;大模型是无状态的。但是基于大模型的agent一般是有状态的&#xff0c;也就是它有记忆功能。在AI Agent框架中&#xff0c;Memory机制是核心组件之…

AI与IT从业者的未来:替代焦虑还是协作革命?

​​引言&#xff1a;技术渗透与核心命题​​2025年&#xff0c;人工智能技术已从实验室走向产业核心。国务院《关于深入实施“人工智能”行动的意见》推动AI在医疗、制造、金融等领域的规模化落地&#xff0c;全球AI应用用户规模突破2.3亿&#xff0c;生成式AI工具渗透率达16.…

手机版碰一碰发视频系统批量剪辑功能开发,支持OEM贴牌

引言在当今短视频盛行的时代&#xff0c;视频内容的快速生产与分享变得愈发重要。手机版碰一碰发视频系统&#xff0c;借助 NFC 等近场通信技术&#xff0c;实现了便捷的数据交互与视频分享&#xff0c;而在此基础上集成的批量剪辑功能&#xff0c;更是为内容创作者和商家带来了…

Spring AMQP如何通过配置文件避免硬编码实现解耦

在使用Spring AMQP基于注解声明监听者时&#xff0c;可通过抽取常量来避免硬编码&#xff1a;RabbitListener(bindings QueueBinding(exchange Exchange(MQConstant.USER_EXCHANGE),value Queue(MQConstant.USER_QUEUE),key MQConstant.USER_REDIS_BINDING))public void de…

解决zabbix图片中文乱码

要把 Zabbix 前端字体替换为 simkai.ttf&#xff08;楷体&#xff0c;解决乱码常用&#xff09;&#xff0c;按以下步骤操作&#xff1a;1. 确认 simkai.ttf 路径 先找到系统里 simkai.ttf 字体文件&#xff0c;若没有&#xff0c;可从 Windows 系统&#xff08;C:\Windows\Fon…

实例分割-动手学计算机视觉13

介绍 实例分割(instance segmentation)的目的是从图像中分割出每个目标实例的掩模(mask)。与语义分割相比&#xff0c;实例分割不但要区分不同的类别&#xff0c;还要区分出同一种类别下的不同目标实例。如图13-1所示 语义分割的结果中&#xff0c;不同的羊对应的标签是一样的…

水环境遥感分析!R语言编程+多源遥感数据预处理;水体指数计算、水深回归分析、水温SVM预测、水质神经网络建模及科研级可视化制图

系统性地整合R语言编程、遥感数据处理及机器学习建模&#xff0c;涵盖水线提取&#xff08;水体指数与阈值法&#xff09;、水深反演&#xff08;多元回归&#xff09;、水温预测&#xff08;支持向量机&#xff09;、水质评估&#xff08;神经网络&#xff09;等核心内容&…

微信公众号/小程序百万级OpenID自动化获取工具

摘要 本报告详细阐述了微信用户列表数据获取与处理工具的设计思路,包括分页处理机制、频率控制策略、断点续传功能和分布式存储方案。针对微信API调用限制和用户数据规模特点,该工具旨在高效、安全地获取和存储微信用户列表数据,同时严格遵守微信API调用频率限制,确保系统…