图算法
图结构的表示方法
图的节点表示:
import java.util.ArrayList;
public class Node {
public int value;
public int in;
public int out;
public ArrayList<Node> nexts;
public ArrayList<Edge> edges;
public Node(int value){
this.value=value;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
图的边结构表示:
public class Edge {
public int weight;
public Node from;
public Node to;
public Edge(int weight, Node from, Node to) {
this.weight = weight;
this.from = from;
this.to = to;
}
}
图结构表示:
import java.util.HashMap;
import java.util.HashSet;
public class Graph {
public HashMap<Integer, Node> nodes;
public HashSet<Edge> edges;
public Graph() {
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
图的生成:
public class GraphGenerator {
public static Graph createGraph(Integer[][] matrix) {
Graph graph = new Graph();
for (int i = 0; i < matrix.length; i++) {
Integer from = matrix[i][0];
Integer to = matrix[i][1];
Integer weight = matrix[i][2];
/**
* 如果点不存在与图中,则添加该点
*/
if (!graph.nodes.containsKey(from)) {
graph.nodes.put(from, new Node(from));
}
if (!graph.nodes.containsKey(to)) {
graph.nodes.put(to, new Node(to));
}
Node fromNode = graph.nodes.get(from);
Node toNode = graph.nodes.get(to);
Edge newEdge = new Edge(weight, fromNode, toNode);
fromNode.nexts.add(toNode);
fromNode.out++;
toNode.in++;
fromNode.edges.add(newEdge);
graph.edges.add(newEdge);
}
return graph;
}
}
图的深度优先遍历与宽度优先遍历
图的广度优先遍历:
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
public class BFS {
public static void bfs(Node node) {
if (node == null) {
return;
}
Queue<Node> queue = new LinkedList<>();
HashSet<Node> map = new HashSet<>();
queue.add(node);
map.add(node);
while (!queue.isEmpty()) {
Node current = queue.poll();
System.out.println(current.value);
for (Node next : current.nexts) {
if (!map.contains(next)) {
map.add(next);
queue.add(next);
}
}
}
}
}
图的深度优先遍历:
import java.util.HashSet;
import java.util.Stack;
public class DFS {
public static void dfs(Node node) {
if (node == null) {
return;
}
Stack<Node> stack = new Stack<>();
HashSet<Node> set = new HashSet<>();
stack.add(node);
set.add(node);
System.out.println(node.value);
while (!stack.isEmpty()) {
Node current = stack.pop();
for (Node next : current.nexts) {
if (!set.contains(next)) {
stack.push(current);
stack.push(next);
set.add(next);
System.out.println(next.value);
break;
}
}
}
}
}
拓扑排序问题
什么是拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
- 每个顶点出现且只出现一次。
- 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。
例如,下面这个图:
它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:
- 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
- 从图中删除该顶点和所有以它为起点的有向边。
- 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
于是,得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。
通常,一个有向无环图可以有一个或多个拓扑排序序列。
拓扑排序的应用
拓扑排序通常用来“排序”具有依赖关系的任务。
比如,如果用一个DAG图来表示一个工程,其中每个顶点表示工程中的一个任务,用有向边<A,B><A,B>表示在做任务 B 之前必须先完成任务 A。故在这个工程中,任意两个任务要么具有确定的先后关系,要么是没有关系,绝对不存在互相矛盾的关系(即环路)。
拓扑排序的实现
关键是要维护一个入度为0的顶点的集合。
import java.util.*;
public class TopologySort {
public static List<Node> sortedTopology(Graph graph) {
HashMap<Node, Integer> inMap = new HashMap<>();
Queue<Node> zeroInQueue = new LinkedList<>();
for (Node node : graph.nodes.values()) {
inMap.put(node, node.in);
if (node.in == 0) {//找出入度为0的节点
zeroInQueue.add(node);
}
}
List<Node> result = new ArrayList<>();
while (!zeroInQueue.isEmpty()) {
//依次弹出入度为0的节点
Node current = zeroInQueue.poll();
result.add(current);
//删除入度为0的节点所属的边
for (Node next : current.nexts) {
inMap.put(next, inMap.get(next) - 1);
//将新产生的入度为0的节点放入队列中
if (inMap.get(next) == 0) {
zeroInQueue.add(next);
}
}
}
return result;
}
}
最小生成树问题
图的几个概念定义:
- 连通图:在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。
- 强连通图:在有向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该有向图为强连通图。
- 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
- 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
- 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
1. 把图中的所有边按代价从小到大排序;
2. 把图中的n个顶点看成独立的n棵树组成的森林;
3. 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
代码:
import java.util.*;
public class Kruskal {
public static class UnionFind {
private HashMap<Node, Node> fatherMap;
private HashMap<Node, Integer> rankMap;
public UnionFind() {
fatherMap = new HashMap<>();
rankMap = new HashMap<>();
}
private Node findFather(Node n) {
Node father = fatherMap.get(n);
if (father != n) {
father = findFather(father);
}
fatherMap.put(n, father);
return father;
}
//每个节点建立自己的集合
public void makeSets(Collection<Node> nodes) {
fatherMap.clear();
rankMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
rankMap.put(node, 1);
}
}
//两个节点是否属于同一集合
public boolean isSameSet(Node a, Node b) {
return findFather(a) == findFather(b);
}
//两个节点所在的集合合并
public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aFather = findFather(a);
Node bFather = findFather(b);
if (aFather != bFather) {
int aFrank = rankMap.get(aFather);
int bFrank = rankMap.get(bFather);
if (aFrank <= bFrank) {
fatherMap.put(aFather, bFather);
rankMap.put(bFather, aFrank + bFrank);
} else {
fatherMap.put(bFather, aFather);
rankMap.put(aFather, aFrank + bFrank);
}
}
}
public static class EdgeComparator implements Comparator<Edge> {
@Override
public int compare(Edge o1, Edge o2) {
return o1.weight - o2.weight;
}
}
public static Set<Edge> kruskalMST(Graph graph) {
UnionFind unionFind = new UnionFind();
unionFind.makeSets(graph.nodes.values());
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());//小根堆,权重低的在上边
for (Edge edge : graph.edges) {
priorityQueue.add(edge);
}
Set<Edge> result = new HashSet<>();
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
if (!unionFind.isSameSet(edge.from, edge.to)) {
result.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return result;
}
}
}
Prim算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
- 图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
- 在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
- 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息,:
struct
{
char vertexData //表示u中顶点信息
UINT lowestcost //最小代价
}closedge[vexCounts]
代码:
import java.util.Comparator;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
public class Prim {
public static class EdgeComparator implements Comparator<Edge> {
@Override
public int compare(Edge o1, Edge o2) {
return o1.weight - o2.weight;
}
}
public static Set<Edge> primMST(Graph graph) {
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
HashSet<Node> set = new HashSet<>();
Set<Edge> result = new HashSet<>();
for (Node node : graph.nodes.values()) {
if (!set.contains(node)) {
set.add(node);
for (Edge edge : node.edges) {
priorityQueue.add(edge);
}
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
Node toNode = edge.to;
if (!set.contains(toNode)) {
set.add(toNode);
result.add(edge);
for (Edge nextEdge : node.edges) {
priorityQueue.add(nextEdge);
}
}
}
}
}
return result;
}
}
单源最短路径问题
问题解释:
从图中的某个顶点出发到达另外一个顶点的所经过的边的权重和最小的一条路径,称为最短路径
解决问题的算法:
- 迪杰斯特拉算法(Dijkstra算法)
- 弗洛伊德算法(Floyd算法)
- SPFA算法
Dijkstra算法介绍
-
算法特点:
迪科斯彻算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。
-
算法的思路
Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,
然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。
然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。
代码:
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class Dijkstra {
public static HashMap<Node, Integer> dijkstral(Node head) {
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(head, 0);
HashSet<Node> selectedNodes = new HashSet<>(); //已经选择过的节点
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
while (minNode != null) {
int distance = distanceMap.get(minNode);
for (Edge edge : minNode.edges) {
Node toNode = edge.to;
if (!distanceMap.containsKey(toNode)) {
distanceMap.put(toNode, distance + edge.weight);
}
distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
}
selectedNodes.add(minNode);
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
}
return distanceMap;
}
private static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> touchedNodes) {
Node minNode = null;
int minDistance = Integer.MAX_VALUE;
for (Map.Entry<Node, Integer> entry : distanceMap.entrySet()) {
Node node = entry.getKey();
int distance = entry.getValue();
if (!touchedNodes.contains(node) && distance < minDistance) {
minNode = node;
minDistance = distance;
}
}
return minNode;
}
public static class NodeRecord {
public Node node;
public int distance;
public NodeRecord(Node node, int distance) {
this.node = node;
this.distance = distance;
}
}
public static class NodeHeap {
private Node[] nodes;
private HashMap<Node, Integer> heapIndexMap;
private HashMap<Node, Integer> distanceMap;
private int size;
public NodeHeap(int size) {
nodes = new Node[size];
heapIndexMap = new HashMap<>();
distanceMap = new HashMap<>();
this.size = 0;
}
public boolean isEmpty() {
return size == 0;
}
public void addOrUpdateOrIgnore(Node node, int distance) {
if (inHeap(node)) {
distanceMap.put(node, Math.min(distanceMap.get(node), distance));
insertHeapify(node, heapIndexMap.get(node));
}
if (!isEntered(node)) {
nodes[size] = node;
heapIndexMap.put(node, size);
distanceMap.put(node, distance);
insertHeapify(node, size++);
}
}
public NodeRecord pop() {
NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
swap(0, size - 1);
heapIndexMap.put(nodes[size - 1], -1);
distanceMap.remove(nodes[size - 1]);
nodes[size - 1] = null;
heapify(0, --size);
return nodeRecord;
}
private void insertHeapify(Node node, int index) {
while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left]) ? left + 1 : left;
smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;
if (smallest == index) {
break;
}
swap(smallest, index);
index = smallest;
left = index * 2 + 1;
}
}
private boolean isEntered(Node node) {
return heapIndexMap.containsKey(node);
}
private boolean inHeap(Node node) {
return isEntered(node) && heapIndexMap.get(node) != -1;
}
private void swap(int index1, int index2) {
heapIndexMap.put(nodes[index1], index2);
heapIndexMap.put(nodes[index2], index2);
Node temp = nodes[index1];
nodes[index1] = nodes[index2];
nodes[index2] = temp;
}
}
public static HashMap<Node, Integer> dijkstra2(Node head, int size) {
NodeHeap nodeHeap = new NodeHeap(size);
nodeHeap.addOrUpdateOrIgnore(head, 0);
HashMap<Node, Integer> result = new HashMap<>();
while (!nodeHeap.isEmpty()) {
NodeRecord record = nodeHeap.pop();
Node curr = record.node;
int distance = record.distance;
for (Edge edge : curr.edges) {
nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
}
result.put(curr, distance);
}
return result;
}
}
代码2:
public class DijkstraCode {
static int MAX = Integer.MAX_VALUE;
public static void main(String[] args) {
//邻接矩阵
int[][] weight = {
{0, 3, 2000, 7, MAX},
{3, 0, 4, 2, MAX},
{MAX, 4, 0, 5, 2},
{7, 2, 5, 0, 6},
{MAX, MAX, 4, 6, 0}
};
int start = 0;
int[] shortPath = DijkstraProcess(weight, start);
for (int i = 0; i < shortPath.length; i++) {
System.out.println("从" + start + "出发到" + i + "的最短距离为:" + shortPath[i]);
}
}
/**
* 接受一个有向图的权重矩阵,和一个起点编号start(从0编号,顶点存在数组中)
* 返回一个int[]数组,表示从start到它的最短路径长度
*
* @param weight
* @param start
* @return
*/
private static int[] DijkstraProcess(int[][] weight, int start) {
int length = weight.length;
int[] shortPath = new int[length];//存放从start到其他各点的最短距离
shortPath[start] = 0;//start到它本身的距离为0
String[] path = new String[length];//存放从start到其他割点的最短路径的字符串表示
for (int i = 0; i < length; i++) {
path[i] = start + "-->" + i;
}
int[] visited = new int[length];//标记当前该顶点的最短路径是否已经求出,1表示已求出
visited[start] = 1;//start点的最短距离已经求出
for (int count = 1; count < length; count++) {
//选出一个距离初始顶点start最近的未标记顶点
int k = -1;
int dmin = Integer.MAX_VALUE;
for (int i = 0; i < length; i++) {
if (visited[i] == 0 && weight[start][i] < dmin) {
dmin = weight[start][i];
k = i;
}
}
//将新选出的顶点标记为已求出最短路径,且到start的最短路径就是dmin
shortPath[k] = dmin;
visited[k] = 1;
//以k为中间点,修正从start到未访问各点的距离
for (int i = 0; i < length; i++) {
if (visited[i] == 0 && weight[start][k] + weight[k][i] < weight[start][i]) {
weight[start][i] = weight[start][k] + weight[k][i];
path[i] = path[k] + "-->" + i;
}
}
}
for (int i = 0; i < length; i++) {
System.out.println("从" + start + "出发到" + i + "的最短路径为:" + path[i]);
}
System.out.println("==============================================");
return shortPath;
}
}
弗洛伊德算法介绍
弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。
基本思想
通过Floyd计算图G=(V,E)中各个顶点的最短路径时,需要引入一个矩阵S,矩阵S中的元素a[i][j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。
假设图G中顶点个数为N,则需要对矩阵S进行N次更新。初始时,矩阵S中顶点a[i][j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则a[i][j]=∞。 接下来开始,对矩阵S进行N次更新。第1次更新时,如果"a[i][j]的距离" > "a[i][0]+a[0][j]"(a[i][0]+a[0][j]表示"i与j之间经过第1个顶点的距离"),则更新a[i][j]为"a[i][0]+a[0][j]"。 同理,第k次更新时,如果"a[i][j]的距离" > "a[i][k]+a[k][j]",则更新a[i][j]为"a[i][k]+a[k][j]"。更新N次之后,操作完成!
初始状态:S是记录各个顶点间最短路径的矩阵。
第1步:初始化S。
矩阵S中顶点a[i][j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则a[i][j]=∞。实际上,就是将图的原始矩阵复制到S中。
注:a[i][j]表示矩阵S中顶点i(第i个顶点)到顶点j(第j个顶点)的距离。
第2步:以顶点A(第1个顶点)为中介点,若a[i][j] > a[i][0]+a[0][j],则设置a[i][j]=a[i][0]+a[0][j]。 同理,依次将顶点B,C,D,E,F,G作为中介点,并更新a[i][j]的大小。
代码:
public class Floyd {
/**
* 邻接矩阵
*/
private int[][] matrix;
/**
* 表示正无穷
*/
private int MAX_WEIGHT = Integer.MAX_VALUE;
/**
* 路径矩阵
*/
private int[][] pathMatirx;
/**
* 前驱表
*/
private int[][] preTable;
/**
* 创建图1
*/
private void createGraph1(int index) {
matrix = new int[index][index];
int[] v0 = {0, 1, MAX_WEIGHT, MAX_WEIGHT, 2, MAX_WEIGHT};
int[] v1 = {1, 0, 1, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT};
int[] v2 = {MAX_WEIGHT, 1, 0, 1, MAX_WEIGHT, MAX_WEIGHT};
int[] v3 = {MAX_WEIGHT, MAX_WEIGHT, 1, 0, 1, MAX_WEIGHT};
int[] v4 = {MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 1, 0, 1};
int[] v5 = {MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 1, 1, 0};
matrix[0] = v0;
matrix[1] = v1;
matrix[2] = v2;
matrix[3] = v3;
matrix[4] = v4;
matrix[5] = v5;
}
/**
* 创建图2
*/
private void createGraph2(int index) {
matrix = new int[index][index];
int[] v0 = {0, 1, 5, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT};
int[] v1 = {1, 0, 3, 7, 5, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT};
int[] v2 = {5, 3, 0, MAX_WEIGHT, 1, 7, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT};
int[] v3 = {MAX_WEIGHT, 7, MAX_WEIGHT, 0, 2, MAX_WEIGHT, 3, MAX_WEIGHT, MAX_WEIGHT};
int[] v4 = {MAX_WEIGHT, 5, 1, 2, 0, 3, 6, 9, MAX_WEIGHT};
int[] v5 = {MAX_WEIGHT, MAX_WEIGHT, 7, MAX_WEIGHT, 3, 0, MAX_WEIGHT, 5, MAX_WEIGHT};
int[] v6 = {MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 3, 6, MAX_WEIGHT, 0, 2, 7};
int[] v7 = {MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 9, 5, 2, 0, 4};
int[] v8 = {MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, MAX_WEIGHT, 7, 4, 0};
matrix[0] = v0;
matrix[1] = v1;
matrix[2] = v2;
matrix[3] = v3;
matrix[4] = v4;
matrix[5] = v5;
matrix[6] = v6;
matrix[7] = v7;
matrix[8] = v8;
}
/**
* 打印 所有最短路径
*/
public void print() {
for (int m = 0; m < matrix.length; m++) {
for (int n = m + 1; n < matrix.length; n++) {
int k = preTable[m][n];
System.out.print("(" + m + "," + n + ")" + pathMatirx[m][n] + ": ");
System.out.print(m);
while (k != n) {
System.out.print("->" + k);
k = preTable[k][n];
}
System.out.println("->" + n);
}
System.out.println();
}
}
/**
* 弗洛伊德算法
*/
private void floyd() {
//路径矩阵(D),表示顶点到顶点的最短路径权值之和的矩阵,初始时,就是图的邻接矩阵
pathMatirx = new int[matrix.length][matrix.length];
//前驱表(P),P[m][n]的值为m到n的最短路径的前驱顶点,如果是直连,值为n,也就是初始值
preTable = new int[matrix.length][matrix.length];
//初始化D,P
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix.length; j++) {
pathMatirx[i][j] = matrix[i][j];
preTable[i][j] = j;
}
}
//循环,中间经过顶点
for (int k = 0; k < matrix.length; k++) {
for (int m = 0; m < matrix.length; m++) {
for (int n = 0; n < matrix.length; n++) {
int mn = pathMatirx[m][n];
int mk = pathMatirx[m][k];
int kn = pathMatirx[k][n];
int addedPath = (mk == MAX_WEIGHT || kn == MAX_WEIGHT) ? MAX_WEIGHT : mk + kn;
if (mn > addedPath) {
//如果经过k顶点路径比原两点路径更短,将两点间权值设为更小的一个
pathMatirx[m][n] = addedPath;
//前驱设置为经过下标为k的顶点
preTable[m][n] = preTable[m][k];
}
}
}
}
}
public static void main(String[] args) {
Floyd floyd = new Floyd();
floyd.createGraph1(6);
// floyd.createGraph2(9);
floyd.floyd();
floyd.print();
}
}
SPFA(Shortest Path Faster Algorithm)算法
spfa算法功能:给定一个加权连通图,选取一个顶点,称为起点,求取起点到其它所有顶点之间的最短距离,其显著特点是可以求含负权图的单源最短路径,且效率较高。在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。
spfa算法思想:spfa就是BellmanFord的一种实现方式,其具体不同在于,对于处理松弛操作时,采用了队列(先进先出方式)操作,从而大大提高了时间复杂度。
SPFA算法有两个优化算法 SLF 和 LLL: SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。 LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。
问题描述
给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。
输入格式:
第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。
输出格式:
共n-1行,第i行表示1号点到i+1号点的最短路。
样例输入
3 3
1 2 -1
2 3 -1
3 1 2
样例输出
-1
-2
数据规模与约定
对于10%的数据,n = 2,m = 2。
对于30%的数据,n <= 5,m <= 10。
对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。
题目分析:带负权边的单源最短路问题
算法分析:
题目的锦囊提示可以使用带堆优化的Dijkstra算法,因为此图的边数比点数的平方要少很多。但是根据题意,含负权边并且不含负环,这里我选用的是SPFA算法(Shortest Path Faster Algorithm)。SPFA是队列优化的Bellman-Ford算法,利用了队列的先进先出性质。按照本题的要求提一下思路:引入数组d [ ] ,d [ i ] 表示始点到 i 的最短路,初始赋值为无穷大,只有 始点的对应值为 0;引入数组 first [ ] ,记录前驱,初始赋值为-1,表示还没有知道前驱。接着将始点入队,每次读取一个入队元素并将其赋值给x,然后出队,并对所有相邻点进行松弛操作,松弛成功的点将其入队。 重复这个步骤,直到队列为空时算法结束。另外需要注意的是需要引入 boolean数组 vis [ ] ,用来标记点的状态,在读取、赋值、出队操作后要清除标记,表示该点已不在队列中,在入队操作后要添加标记。
关于松弛操作不太清楚的建议看一看算法书,或者根据给出的思路结合下面的代码再仔细思考一下。原理就是,依次枚举从u出发的边 u —> v,边的长度为len(题目中为l,这里为了看的清楚先写成len),判断d [ u ] + len 是否小于 d [ v ],如果小于则改进 d [ v ] 。
另外提一下的是,SPFA无法处理带负环的图,如果某个点进入队列的次数超过 n 次则存在负环。
代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
public class SPFA {
static int n, m;//n个顶点,m条边
static int[] start;//起始点
static int[] end;//终止点
static int[] weight;//start到end有一条长度为len的边
static int[] path;//path[i]表示起始点到i的最短路径
static int[] pre;//记录前驱
static int[] next;//赋值为-1,作为循环终止
static boolean[] isVisited;//标记状态
static Queue<Integer> queue = new LinkedList<>();//队列
public static void main(String[] args) throws IOException {
BufferedReader bfr = new BufferedReader(new InputStreamReader(System.in));
String str = bfr.readLine();
String[] s = str.split(" ");
n = Integer.parseInt(s[0]);
m = Integer.parseInt(s[1]);
start = new int[m + 1];
end = new int[m + 1];
weight = new int[m + 1];
pre = new int[n + 1];
next = new int[m + 1];
path = new int[n + 1];
isVisited = new boolean[n + 1];
for (int i = 1; i <= n; i++) {
pre[i] = -1;//前驱全部赋值为-1,表示还没有知道前驱
}
for (int i = 1; i <= m; i++) {
str = bfr.readLine();
s = str.split(" ");
start[i] = Integer.parseInt(s[0]);
end[i] = Integer.parseInt(s[1]);
weight[i] = Integer.parseInt(s[2]);
next[i] = pre[start[i]];//next[] 全部赋值为-1
pre[start[i]] = i;//存放n个节点
}
spfa(1);
for (int i = 2; i <= n; i++) {
System.out.println(path[i]);
}
}
private static void spfa(int s) {
for (int i=2;i <= n;i++) {
path[i] = Integer.MAX_VALUE;//除初始点外,赋值为无穷大
}
queue.offer(s);//添加队头节点
while (!queue.isEmpty()) {
int x = queue.poll();//读取队头节点s,赋值给x,并将s出队
isVisited[x] = false;//清除标记,表示不在队列中
for (int i = pre[x]; i != -1; i = next[i]) {
//对相邻的点做松弛操作
if (path[end[i]] > path[x] + weight[i]) {
path[end[i]] = path[x] + weight[i];
if (!isVisited[end[i]]) {
isVisited[end[i]] = true;//标记,表示在队列中
queue.offer(end[i]);//入队
}
}
}
}/**/
}
}