【算法】有权图相关操作

157 阅读3分钟

1. 有权图的表示方式

  • 有权图需要自定义以下Edge类
public class Edge {
    private int a;
    private int b;
    // 权值,可用泛型来扩展
    private int weight;
    public Edge(int a, int b, int weight) {
        this.a = a;
        this.b = b;
        this.weight = weight;
    }
    // 得到与v相连的另外一个顶点
    public int getOther(int v) {
        return a == v ? b : a;
    }
    // 所有属性的get方法……
}
  • 有权图一般用邻接表的形式表示
// graph.get(i) = [{"a":i, "b":1, "weight":10}, 
//                 {"a":i, "b":2, "weight":20}, 
//                 {"a":i, "b":3, "weight":30}]
// 表示顶点i和顶点1,2,3相连,权值分别是10,20,30
List<List<Edge>> graph;

2. 有权图的相关操作

2.1 最小生成树

  • 切分:把图中的节点分成两部分

  • 横切边:如果一条边的两端顶点属于切分的不同部分,把这种边称为横切边。如下图的蓝色边 036-横切边.png

  • 切分定理:给定任何切分横切边中权值最小的边必定属于最小生成树

2.1.2 Prim算法

  • 算法思路:以某顶点作为起始顶点,标记它(即把它加入到标记阵营中),把该顶点和其他顶点看成两部分(通过是否标记来区分两个阵营),从所有横切边中选出权值最小的边,并标记该边的相邻顶点,把它加入到标记阵营中。再从新的所有横切边中选出权值最小的边,把该边的相邻顶点加入到标记阵营中。不断重复上述步骤

  • 时间复杂度:O(E㏒V)

  • 代码逻辑如下图所示,具体代码需要使用索引堆数据结构 036-Prim算法逻辑.png

public class PrimMST {

    public List<Edge> getMST(List<List<Edge>> graph) {
        if (graph == null || graph.size() == 0) {
            return new ArrayList<>();
        }
        int size = graph.size();
        // 值为true的顶点理解成被标记了,与没有被标记的顶点看成两部分
        boolean[] marked = new boolean[size];
        // 索引堆
        IndexHeap<Edge> indexHeap = new IndexHeap<>(size, (o1, o2) -> o2.getWeight() - o1.getWeight());

        List<Edge> mst = new ArrayList<>();

        visit(0, graph, marked, indexHeap);
        while (!indexHeap.isEmpty()) {
            int nextV = indexHeap.peekIndex();
            mst.add(indexHeap.poll());

            visit(nextV, graph, marked, indexHeap);
        }

        return mst;
    }

    private void visit(int v, List<List<Edge>> graph, boolean[] marked, IndexHeap<Edge> indexHeap) {
        if (marked[v]) {
            return;
        }
        marked[v] = true;
        for (Edge adjEdge : graph.get(v)) {
            int otherV = adjEdge.getOther(v);
            if (marked[otherV]) {
                continue;
            }
            if (indexHeap.indexOf(otherV) == null) {
                indexHeap.offer(otherV, adjEdge);
            } else if (adjEdge.getWeight() < indexHeap.indexOf(otherV).getWeight()) {
                // 如果对应位置已有元素,并且新元素的权值更小,则替换
                indexHeap.replace(otherV, adjEdge);
            }
        }
    }
}

2.1.3 Kruskal算法

  • 算法思路:把所有边按权值从小到大排序,从权值小的边开始遍历,若遍历到的边不构成环,则放入到结果集中,直到拿到 v - 1 条边为止
  • 时间复杂度:O(E㏒E),效率比Prim算法低
  • 代码实现如下,判断是否构成环需要使用并查集数据结构
public class KruskalMST {

    public List<Edge> getMST(List<List<Edge>> graph) {
        if (graph == null || graph.size() == 0) {
            return new ArrayList<>();
        }
        // 收集所有边,按权值从小到大排序
        List<Edge> allEdge = new ArrayList<>();
        graph.forEach(item -> item.forEach(e -> {
            // 无向图的时候,不收集重复的边
            if (e.getA() < e.getB()) {
                allEdge.add(e);
            }
        }));
        allEdge.sort(Comparator.comparing(Edge::getWeight));

        // 并查集
        UnionFind unionFind = new UnionFind(graph.size());

        List<Edge> mst = new ArrayList<>();
        int a, b;
        for (Edge edge : allEdge) {
            a = edge.getA();
            b = edge.getB();
            // 如果新加入的边会构成环,则跳过
            if (unionFind.isConnected(a, b)) {
                continue;
            }
            unionFind.union(a, b);
            mst.add(edge);
        }
        return mst;
    }
}

2.2 最短路径

  • 最短路径树:又称为单源最短路径树,是指从图中一个顶点出发,到其他任意顶点的最短路径构成树的集合
  • 下面介绍的算法都是得到最短路径树

2.2.1 Dijkstra算法

  • 该算法的前提:图中不能有负权值的边

  • 时间复杂度:O(E㏒V)

  • 该算法主要是得出从某个起始顶点到达其他顶点的最短路径的权值,有向图与无向图的逻辑通用

  • 代码逻辑如下图所示,实际得到的是一棵最短路径树,具体代码需要使用索引堆数据结构 036-Dijkstra算法逻辑.png

public class Dijkstra {

    public List<String> getShortestPath(List<List<Edge>> graph, int source) {
        if (graph == null || graph.size() == 0) {
            return new ArrayList<>();
        }
        int size = graph.size();

        // 若marked[i] = true,则说明找到了从顶点source到顶点i的最短路径
        boolean[] marked = new boolean[size];
        // path[i]表示从顶点source到顶点i的最短路径的权值
        int[] path = new int[size];
        // from[i] = j,表示在最短路径树中,i的上一个节点是j
        int[] from = new int[size];
        // source是最短路径树的根节点
        from[source] = -1;

        // 索引堆
        // 索引:顶点编号
        // 具体元素:从source到该顶点的最短路径的权值
        IndexHeap<Integer> indexHeap = new IndexHeap<>(size, (w1, w2) -> w2 - w1);
        indexHeap.offer(source, 0);
        
        while (!indexHeap.isEmpty()) {
            // 当前权值最小的顶点,每次出堆都说明找到了从source到curV的最短路径
            int curV = indexHeap.peekIndex();
            Integer curWeight = indexHeap.poll();

            marked[curV] = true;
            path[curV] = curWeight;

            for (Edge adjE : graph.get(curV)) {
                int nextV = adjE.getOther(curV);
                if (marked[nextV]) {
                    continue;
                }
                // 累加后新的权值
                int newWeight = curWeight + adjE.getWeight();
                if (indexHeap.indexOf(nextV) == null) {
                    indexHeap.offer(nextV, newWeight);
                } else if (newWeight < indexHeap.indexOf(nextV)) {
                    indexHeap.replace(nextV, newWeight);
                }
                from[nextV] = curV;
            }
        }

        // 汇总最短路径树
        return translateShortestPathTree(source, marked, path, from);
    }

    private List<String> translateShortestPathTree(int source, boolean[] marked, int[] path, int[] from) {
        List<String> ret = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        int temp;
        for (int v = 0; v < marked.length; v++) {
            if (v == source) {
                continue;
            }
            if (!marked[v]) {
                ret.add("不存在从" + source + "到" + v + "的路径");
                continue;
            }
            temp = v;
            while (temp != -1) {
                sb.insert(0, temp);
                temp = from[temp];
                if (temp != -1) {
                    sb.insert(0, " -> ");
                }
            }
            ret.add(source + "到" + v + "的最短路径:" + sb.toString() + "  总权值为" + path[v]);
            sb.setLength(0);
        }
        return ret;
    }
}

2.2.2 SPFA算法

  • 该算法是Bellman-Ford算法的优化,支持负权边,但是图里不能有负权环:假设图有负权环,那么每绕一圈,路径总权值就会更小,这样永远也找不到最短路径

  • 该算法一般用于有向图中,需要用上队列数据结构。在无向图中,假设存在负权边,由于无向图的特性就相当于拥有负权环了

  • 时间复杂度:O(kE),k是每个顶点的平均入队次数,一般在区间[2, 3]中

  • 算法逻辑如下,可参考这篇文章 036-SPFA算法逻辑.png

public class SPFA {

    public List<String> getShortestPath(List<List<Edge>> graph, int source) {
        if (graph == null || graph.size() == 0) {
            return new ArrayList<>();
        }
        int size = graph.size();

        // path[i]表示从顶点source到顶点i的最短路径的权值
        int[] path = new int[size];
        Arrays.fill(path, Integer.MAX_VALUE);
        path[source] = 0;

        // from[i] = j,表示在最短路径树中,i的上一个节点是j
        int[] from = new int[size];
        Arrays.fill(from, -1);

        // 顶点i是否在队列中
        boolean[] inQueue = new boolean[size];
        inQueue[source] = true;

        Queue<Integer> queue = new ArrayDeque<>();
        queue.offer(source);

        while (!queue.isEmpty()) {
            Integer curV = queue.poll();
            inQueue[curV] = false;

            for (Edge adjE : graph.get(curV)) {
                int nextV = adjE.getOther(curV);
                int newWeight = path[curV] + adjE.getWeight();

                if (newWeight < path[nextV]) {
                    path[nextV] = newWeight;
                    from[nextV] = curV;
                    if (!inQueue[nextV]) {
                        queue.offer(nextV);
                        inQueue[nextV] = true;
                    }
                }
            }
        }
        // 汇总最短路径树
        return translateShortestPathTree(source, path, from);
    }

    private List<String> translateShortestPathTree(int source, int[] path, int[] from) {
        List<String> ret = new ArrayList<>();
        StringBuilder sb = new StringBuilder();
        int temp;
        for (int v = 0; v < path.length; v++) {
            if (v == source) {
                continue;
            }
            if (from[v] == -1) {
                ret.add("不存在从" + source + "到" + v + "的路径");
                continue;
            }
            temp = v;
            while (temp != -1) {
                sb.insert(0, temp);
                temp = from[temp];
                if (temp != -1) {
                    sb.insert(0, " -> ");
                }
            }
            ret.add(source + "到" + v + "的最短路径:" + sb.toString() + "  总权值为" + path[v]);
            sb.setLength(0);
        }
        return ret;
    }
}

3. 相关链接