From 8bc2677267a2fed87be4833044d00dcb5d74b887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=BD=E4=BA=BA=E9=9A=BE=E5=BD=93?= <1580166554@qq.com> Date: Thu, 22 Aug 2024 01:03:48 +0000 Subject: [PATCH] =?UTF-8?q?!1261=20=E5=9F=BA=E4=BA=8ESPFA=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E5=AE=9E=E7=8E=B0=E6=9C=89=E5=90=91=E5=9B=BE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=20*=20=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A=20*=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A=20*=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E4=BF=A1=E6=81=AF=20*=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E8=B4=9F=E7=8E=AF=E6=A3=80=E6=9F=A5=E7=9A=84=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E6=9D=A1=E4=BB=B6=20=E5=BD=93=E5=8E=9F=E5=9B=BE=E4=B8=BA?= =?UTF-8?q?=E9=9D=9E=E8=81=94=E9=80=9A=E5=9B=BE=20=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E5=BC=80=E5=A7=8B=E8=8A=82=E7=82=B9=E5=8F=AF?= =?UTF-8?q?=E8=A7=A6=E8=BE=BE=E8=8A=82=E7=82=B9=E5=B0=8F=E4=BA=8E=E6=80=BB?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=95=B0=20=E6=88=96=E8=80=85=E7=8E=AF?= =?UTF-8?q?=E8=B7=AF=E8=B7=AF=E6=AE=B5=E8=BE=83=E7=9F=AD=E6=97=B6=20?= =?UTF-8?q?=E6=89=80=E9=9C=80=E8=A6=81=E5=BE=AA=E7=8E=AF=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E7=9A=84=E6=AC=A1=E6=95=B0=20*=20Merge=20branch=20'v6-dev'=20o?= =?UTF-8?q?f=20https://gitee.com/hrnd/hutool=20into=20v6-dev=20*=20?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=20*=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=A5=E9=98=9FSLF=E4=BC=98=E5=8C=96=20*?= =?UTF-8?q?=20=E6=9C=89=E5=90=91=E5=9B=BE=20=E5=9F=BA=E4=BA=8ESPFA=20?= =?UTF-8?q?=E7=AE=97=E6=B3=95=E5=AE=9E=E7=8E=B0=20*=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=20*=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=20*=20Merge=20branch=20'v6?= =?UTF-8?q?-dev'=20of=20https://gitee.com/hrnd/hutool=20into=20v6-dev=20*?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E6=9C=89=E5=90=91=E5=9B=BE=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E6=9C=80=E7=9F=AD=E8=B7=AF=E5=BE=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/map/multi/DirectedWeightGraph.java | 294 ++++++++++++++++++ .../core/map/DirectedWeightGraphTest.java | 55 ++++ 2 files changed, 349 insertions(+) create mode 100644 hutool-core/src/main/java/org/dromara/hutool/core/map/multi/DirectedWeightGraph.java create mode 100644 hutool-core/src/test/java/org/dromara/hutool/core/map/DirectedWeightGraphTest.java diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/map/multi/DirectedWeightGraph.java b/hutool-core/src/main/java/org/dromara/hutool/core/map/multi/DirectedWeightGraph.java new file mode 100644 index 000000000..196a7616e --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/map/multi/DirectedWeightGraph.java @@ -0,0 +1,294 @@ +package org.dromara.hutool.core.map.multi; + +import java.util.*; + +/** + * 权重有向图 + * 基于 SPFA 算法实现 可以处理负边 可以进行负权环路检查 + * + * @author NewshiJ + * @date 2024/8/16 09:01 + */ +public class DirectedWeightGraph { + + // 全部节点 + private final Set allPoints = new HashSet<>(); + + // 邻接边 + private final Map>> neighborEdgeMap = new HashMap<>(); + + /** + * 添加边 + * @param fromPoint 开始点 + * @param nextPoint 结束点 + * @param weight 权重 + */ + public void putEdge(T fromPoint, T nextPoint, long weight) { + allPoints.add(fromPoint); + allPoints.add(nextPoint); + Map> nextPointMap = neighborEdgeMap.computeIfAbsent(fromPoint, k -> new HashMap<>()); + nextPointMap.put(nextPoint, new Edge<>(fromPoint, nextPoint, weight)); + } + + /** + * 删除边 + * @param fromPoint + * @param nextPoint + */ + public void removeEdge(T fromPoint, T nextPoint) { + Map> nextPointMap = neighborEdgeMap.computeIfAbsent(fromPoint, k -> new HashMap<>()); + nextPointMap.remove(nextPoint); + + // 重新计算 所有点位 + allPoints.clear(); + neighborEdgeMap.forEach((f,m) -> { + allPoints.add(f); + m.forEach((t,e) -> { + allPoints.add(t); + }); + }); + } + + /** + * 删除点 + * @param point + */ + public void removePoint(T point){ + allPoints.remove(point); + neighborEdgeMap.remove(point); + neighborEdgeMap.forEach((f,m) -> { + m.remove(point); + }); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + neighborEdgeMap.forEach((from,edgeMap) -> { + edgeMap.forEach((to,edge) -> { + builder.append(edge); + builder.append("\r\n"); + }); + }); + return builder.toString(); + } + + + /** + * 计算 从 startPoint 到 所有点 最短路径 + * 基于 SPFA 算法实现 + * + * @param startPoint 开始节点 + * @throws NegativeRingException 存在负权环路 + * @return 最佳路径集合 如果无可触达顶点 返回空 map + */ + public Map> bestPathMap(T startPoint) throws NegativeRingException{ + // 全部节点数量 + int pointSize = allPoints.size(); + // 待访问队列 + LinkedList pointQueue = new LinkedList<>(); + // 待访问队列中的节点 加速判断 + HashSet inQueuePoints = new HashSet<>(); + // 最佳路径集合 + HashMap> bestPathMap = new HashMap<>(); + + + Map> map = neighborEdgeMap.get(startPoint); + // 无可触达路径 + if(map == null || map.isEmpty()){ + return new HashMap<>(); + } + + map.forEach((to,edge) -> { + Path path = new Path<>(edge); + bestPathMap.put(to, path); + pointQueue.add(to); + inQueuePoints.add(to); + }); + + + while (!pointQueue.isEmpty()){ + // 当前节点 开始对 currentPoint 进行扩展 + T currentPoint = pointQueue.removeFirst(); + // 到当前节点的最短路径 + Path currentPath = bestPathMap.get(currentPoint); + // 标记已出队列 + inQueuePoints.remove(currentPoint); + + Map> edgeMap = neighborEdgeMap.get(currentPoint); + if(edgeMap == null){ + continue; + } + + // 扩展当前点的边 + Set>> entrySet = edgeMap.entrySet(); + for (Map.Entry> entry : entrySet) { + T nextPoint = entry.getKey(); + Edge edge = entry.getValue(); + + // 不存在路径 第一次访问 将当前路径放置到 bestPathMap 中 + Path oldPath = bestPathMap.get(nextPoint); + if(oldPath == null){ + Path nextPath = currentPath.nextPoint(edge); + bestPathMap.put(nextPoint,nextPath); + + // 不在队列里就入队 + if(!inQueuePoints.contains(nextPoint)){ + inQueuePoints.add(nextPoint); + + // SLF优化 入队优化 + // 每次出队进行判断扩展出的点与队头元素进行判断,若小于进队头,否则入队尾 + // 尽可能的让 负环路 上的节点 先进入队列头 + if(pointQueue.isEmpty()){ + pointQueue.addLast(nextPoint); + continue; + } + T first = pointQueue.getFirst(); + Path fristPath = bestPathMap.get(first); + if(nextPath.weight < fristPath.weight){ + pointQueue.addFirst(nextPoint); + }else { + pointQueue.add(nextPoint); + } + } + continue; + } + + long newWeight = currentPath.weight + edge.weight; + // 新路径更糟糕 没有优化的必要 + if(newWeight >= oldPath.weight){ + continue; + } + + // 更新最佳路径 如果下一跳没有在队列中 将下一跳放到队列里 + Path nextPath = currentPath.nextPoint(edge); + bestPathMap.put(nextPoint,nextPath); + // 不在队列里就入队 + if(!inQueuePoints.contains(nextPoint)){ + inQueuePoints.add(nextPoint); + + // SLF优化 入队优化 + // 每次出队进行判断扩展出的点与队头元素进行判断,若小于进队头,否则入队尾 + // 尽可能的让 负环路 上的节点 先进入队列头 + if(pointQueue.isEmpty()){ + pointQueue.addLast(nextPoint); + continue; + } + T first = pointQueue.getFirst(); + Path fristPath = bestPathMap.get(first); + if(nextPath.weight < fristPath.weight){ + pointQueue.addFirst(nextPoint); + }else { + pointQueue.addLast(nextPoint); + } + } + } + } + return bestPathMap; + } + + + /** + * 边 + * @param + */ + public static class Edge { + // 起始点 + public T fromPoint; + // 目标点 + public T nextPoint; + // 权重 + public long weight; + + public Edge(T fromPoint, T nextPoint, long weight) { + this.fromPoint = fromPoint; + this.nextPoint = nextPoint; + this.weight = weight; + } + + @Override + public String toString() { + return fromPoint + "->" + nextPoint + "(" + weight + ")"; + } + } + + public static class Path { + // 开始节点 + public T startPoint; + // 结束节点 + public T endPoint; + + /** + * 道路 即依次按照顺序经过的边 + */ + public LinkedList> way = new LinkedList<>(); + + /** + * 已经经过的点 如果 有一个点已经多次经过了 可以判定已经成环 + * 当源图是一个非联通图时 或者 开始节点处于图路径中下游时 或者 成环路经过的节点数量较少时 + * 使用判断节点经过次数与全部节点数量进行比较会有冗余判断 + * 用成环判断 可以加速这种情况 是针对一些特殊的图结构优化了最差情况 + */ + public Set passedPoints = new HashSet<>(); + + // 总权重 + public long weight; + + public Path(Edge edge){ + startPoint = edge.fromPoint; + endPoint = edge.nextPoint; + way.add(edge); + weight = edge.weight; + passedPoints.add(edge.fromPoint); + passedPoints.add(edge.nextPoint); + } + public Path(){} + + /** + * 生成下一跳 + * @param edge + * @throws NegativeRingException 负环路 + * @return + */ + public Path nextPoint(Edge edge) throws NegativeRingException { + Path nextPath = new Path<>(); + nextPath.startPoint = startPoint; + nextPath.endPoint = edge.nextPoint; + nextPath.way.addAll(way); + nextPath.way.add(edge); + nextPath.weight = weight + edge.weight; + nextPath.passedPoints.addAll(passedPoints); + + // 负环检查 + if(nextPath.passedPoints.contains(edge.nextPoint)){ + throw new NegativeRingException("路径:" + nextPath + "存在负环路"); + } + + nextPath.passedPoints.add(edge.nextPoint); + return nextPath; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("[%s->%s(%d)] ", startPoint, endPoint, weight)); + for (Edge edge : way) { + builder.append(edge); + builder.append(" "); + } + return builder.toString(); + } + } + + /** + * 负环异常 + */ + public static class NegativeRingException extends Exception { + public NegativeRingException(String msg){ + super(msg); + } + } +} + + + diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/map/DirectedWeightGraphTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/map/DirectedWeightGraphTest.java new file mode 100644 index 000000000..083dd5861 --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/map/DirectedWeightGraphTest.java @@ -0,0 +1,55 @@ +package org.dromara.hutool.core.map; + +import org.dromara.hutool.core.map.multi.DirectedWeightGraph; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +/** + * @author newshiJ + * @date 2024/8/14 17:07 + */ +public class DirectedWeightGraphTest { + + @Test + public void test1(){ + DirectedWeightGraph graph = new DirectedWeightGraph<>(); + graph.putEdge("A", "B", 14); + graph.putEdge("A", "C", 8); + graph.putEdge("A", "D", 12); + +// graph.putEdge("B", "A", -14); + graph.putEdge("B", "E", 4); + + graph.putEdge("C", "B", 3); + graph.putEdge("C", "D", 5); + graph.putEdge("C", "E", 5); + graph.putEdge("C", "F", 6); + + graph.putEdge("D", "F", 7); + +// graph.putEdge("E", "B", -14); + graph.putEdge("E", "G", 4); + + graph.putEdge("G", "B", -9); + + graph.putEdge("F", "G", 2); + + graph.putEdge("X", "Y", 2); + + graph.removePoint("X"); + + System.out.println(graph); + Map> map = null; + try { + map = graph.bestPathMap("A"); + map.forEach((k,v) -> { + System.out.println(v); + }); + } catch (DirectedWeightGraph.NegativeRingException e) { + e.printStackTrace(); + } + + } +}