7-加权无向图的遍历(DFS、BFS)和最小生成树(Prim、Kruskal)

无向图的遍历

DFS
  • 算法思想

深度优先搜索思想:假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

  • 算法特点:栈

深度优先搜索是一个递归的过程。首先,选定一个出发点后进行遍历,如果有邻接的未被访问过的节点则继续前进。若不能继续前进,则回退一步再前进,若回退一步仍然不能前进,则连续回退至可以前进的位置为止。重复此过程,直到所有与选定点相通的所有顶点都被遍历。

深度优先搜索是递归过程,带有回退操作,因此需要使用存储访问的路径信息。当访问到的当前顶点没有可以前进的邻接顶点时,需要进行出栈操作,将当前位置回退至出栈元素位置,直到栈为空。

BFS
  • 算法思想

广度优先搜索思想:从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

  • 算法特点: 队列

广度优先搜索类似于树的层次遍历,是按照一种由近及远的方式访问图的顶点。在进行广度优先搜索时需要使用队列存储顶点信息,每出队一个顶点,则将它的所有邻接的未入队的点放入队列,直到队列为空。

最小生成树

  • 生成树是加权图中包含所有顶点的无环连通子图,显然生成树只存在于连通图中,最小生成树(MST)是权值最小的生成树;
  • Prim 和 Kruskal 只能用于无向加权图中,用于生成MST,当存在相同权重的边时,MST可能不唯一。
Prim算法

Prim算法的基本思想是贪心算法,通过不断搜索最小横切边实现,算法实现的关键是用“优先队列”这种数据结构来表示横切边,其中又分为以下两种:

1. 延时实现 – 优先队列
2. 即时实现 – 索引优先队列
Kruskal算法

Kruskal算法的基本思想是先对边按照权值升序排序,每次取出最短的(借助于优先队列)然后借助UF算法不断加边并合并,直至形成生成树;E条边算法时间复杂度为ElogE

实现

/*
加权无向图,示例和代码不做特殊说明都是在连通图下,不适用于包含多个极大连通子图的图
 */

import java.util.*;

/**
 * 无向图的遍历
 */
public class UndirectedGraph {
    // 这里为了简单,用0开始连续的数字代表顶点(顶点ID),实际的实现中应该设计成类 Node 的形式
    private final int V;      // 顶点数目
    private int E;            // 边的数目
    private boolean[] marked; // 用于遍历时标记用
    private HashMap<Integer, TreeSet<Edge>> adj; // 邻接表用Hash表实现,key=Node_ID, value=相邻的边组成的链表(这里的实现是红黑二叉树)

    public UndirectedGraph(int v) {
        this.V = v;
        this.E = 0;
        this.adj = new HashMap<>();
        // 初始化哈希表
        for (int i = 0; i < V; i++) {
            adj.put(i,new TreeSet<>());
        }
    }

    public int getV() {
        return V;
    }

    public int getE() {
        return E;
    }

    public void addEdge(Edge e) {
        int from = e.from;
        int to = e.to;
        adj.get(from).add(e);
        adj.get(to).add(e);
        E++;
    }

    // 返回 v 点所有的连接边,Iterable<Edge>方便遍历
    Iterable<Edge> adj(int v) {
        return adj.get(v);
    }

    // 返回加权无向图中所有的边
    public Iterable<Edge> edges() {
        TreeSet<Edge> edges = new TreeSet<>();
        for (Map.Entry<Integer,TreeSet<Edge>> entry : adj.entrySet())
            edges.addAll(entry.getValue());
        return edges;
    }

    /*
    无向图的连通性
     */
    // DFS
    public Queue<Integer> depthFirstSearch() {
        marked = new boolean[V]; // 每次遍历之前懒惰初始化,每次都保证初始全是false
        LinkedList<Integer> outQueue = new LinkedList<>(); // 这个队列是为了方便输出

        dfs(0, outQueue);

        return outQueue;
    }

    private void dfs(int root, LinkedList<Integer> outQueue) {
        outQueue.offer(root); // offer <-> poll
        marked[root] = true;
        for (Edge edge : this.adj(root)) {
            int other = edge.getOther(root);
            if (!marked[other])
                dfs(other, outQueue);
        }
    }

    // BFS
    public Queue<Integer> breadthFirstSearch() {
        marked = new boolean[V]; // 每次遍历之前懒惰初始化,每次都保证初始全是false
        LinkedList<Integer> outQueue = new LinkedList<>();   // 这个队列是为了方便输出
        LinkedList<Integer> levelQueue = new LinkedList<>(); // 这个队列为了执行层次遍历逻辑

        levelQueue.offer(0); // 加入起点
        marked[0] = true;       // 标记起点为已入队点

        while (!levelQueue.isEmpty()) {
            Integer node = levelQueue.pop();
            outQueue.offer(node);      // 从层次队列中删除,并添加到输出队列中
            for (Edge edge : this.adj(node)) {
                int other = edge.getOther(node);
                if (!marked[other]) {
                    levelQueue.offer(other); // 加入层次队列
                    marked[other] = true;    // 标记已入队点
                }
            }
        }

        return outQueue;
    }

    @Override
    public String toString() {
        return "UndirectedGraph{" +
                "V=" + V +
                ", E=" + E +
                ", adj=" + adj +
                '}';
    }
}

class Edge implements Comparable<Edge> {
    int from;
    int to;
    double weight;
    public Edge(int from, int to, double weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }

    public int getFrom() {
        return from;
    }

    public int getTo() {
        return to;
    }

    public double getWeight() {
        return weight;
    }

    public int getOther(int vertex) {
        if (vertex == from)
            return to;
        else if (vertex == to)
            return from;
        else
            throw new RuntimeException("Inconsistent edge");
    }

    // 这里定义大小关系,平行边定义为相等,因为前面用的set存邻接边,即不允许添加平行边
    @Override
    public int compareTo(Edge that) {
        if (this.from == that.from && this.to == that.to)
            return 0;
        else if (this.weight < that.weight)
            return -1;
        else if (this.weight > that.weight)
            return 1;
        else
            return 1;
    }

    @Override
    public String toString() {
        return "Edge{" +
                "from=" + from +
                ", to=" + to +
                ", weight=" + weight +
                '}';
    }
}

/**
 * 最小生成树
 */
class GeneratMST {
    /*
    Prim算法的基本思想是贪心算法,通过不断搜索最小横切边实现,算法实现的关键是用“优先队列”这种数据结构来表示横切边,
    其中又分为两种,<1>优先队列 --> 延时实现,
                <2>索引优先队列 --> 即时实现。
     */

    // 1. 延时实现:E条边的时间复杂度为ElogE
    private static boolean[] marked;        // 顶点标记
    private static PriorityQueue<Edge> pq;  // 优先队列

    public static UndirectedGraph lazyPrimMST(UndirectedGraph graph) {
        UndirectedGraph mst = new UndirectedGraph(graph.getV());
        marked = new boolean[graph.getV()];
        pq = new PriorityQueue<>();

        lazyVisit(graph,0); // 从0开始,将0邻接边加入优先队列
        while (!pq.isEmpty()) {
            Edge minEdge = pq.remove(); // 得到最小边并从pq中删除
            int from = minEdge.from;
            int to = minEdge.to;
            if (marked[from] && marked[to])
                continue;         // 边已失效,跳过
            mst.addEdge(minEdge); // 否则将边加入到mst中
            if (!marked[from])    // 并且将没被访问过顶点的邻接边加入pq
                lazyVisit(graph,from);
            if (!marked[to])
                lazyVisit(graph,to);
        }

        return mst;
    }

    // 工具函数:标记顶点v,并将所有连接v的横切边且未访问的边加入优先队列
    private static void lazyVisit(UndirectedGraph graph, int v) {
        marked[v] = true;
        for (Edge edge : graph.adj(v))
            if (!marked[edge.getOther(v)])
                pq.add(edge);
    }

    // 2. 即时实现:关键在于对邻接边的处理,我们不需要将所有邻接边都存进pq,只需要存最短的那条,那就需要在遍历邻接边时不断地对优先队列进行插入和更新;
    // 而优先队列只能对堆顶元素进行删除,索引优先队列才支持对任意元素的更新,所以要用到索引优先队列;V个顶点,E条边,时间复杂度为ElogV
    private static Edge[] edgeTo;   // 如果v不在树中但至少有一条边和树相连,那么edgeTo[v]代表将v和树相连的最短边
    private static double[] distTo; // distTo[v]是edgeTo[v]的权重,即树到v之间最短边的权值
    // marked 同上
    private static IndexMinPriorityQueue<Double> indexPq; // 索引优先队列,里面存储的是树 到 非树节点 最短边的权重

    public static UndirectedGraph primMST(UndirectedGraph graph) {
        UndirectedGraph mst = new UndirectedGraph(graph.getV());
        edgeTo = new Edge[graph.getV()];
        distTo = new double[graph.getV()];
        marked = new boolean[graph.getV()];
        indexPq = new IndexMinPriorityQueue<>(graph.getV());
        for (int v = 0; v < graph.getV(); v++)
            distTo[v] = Double.MAX_VALUE;  // 先将最短距离置为最大值

        distTo[0] = 0.0; // 设置树的起点,即第0点一直在树上
        indexPq.insert(0 ,0.0); // 用顶点0和权重0初始化indexPq
        while (!indexPq.isEmpty())
            visit(graph,indexPq.delMin());

        // 最后edgeTo数组(索引0不存储任何东西)存储的就是mst上的所有边
        for (int i = 1; i < graph.getV(); i++)
            mst.addEdge(edgeTo[i]);

        return mst;
    }

    // 工具函数:标记顶点v,并更新索引优先队列
    private static void visit(UndirectedGraph graph, int v) {
        marked[v] = true; // 标记为已访问

        for (Edge edge : graph.adj(v)) { // 遍历所有邻接边
            int w = edge.getOther(v);
            if (marked[w])
                continue; // 跳过失效边
            if (edge.getWeight() < distTo[w]) { // 找到树 到 非树上的点 w 更短的边,更新或添加
                edgeTo[w] = edge;
                distTo[w] = edge.getWeight();
                if (indexPq.contains(w))
                    indexPq.change(w,distTo[w]); // 更新
                else
                    indexPq.insert(w,distTo[w]); // 添加
            }
        }
    }




    /*
    Kruskal算法的基本思想是先对边按照权值升序排序,每次取出最短的(借助于优先队列)然后不断加边并合并,直至形成生成树;E条边算法时间复杂度为ElogE
     */
    // kruskal
    public static UndirectedGraph kruskalMST(UndirectedGraph graph) {
        UndirectedGraph mst = new UndirectedGraph(graph.getV());
        PriorityQueue<Edge> pq = new PriorityQueue<>();
        for (Edge edge : graph.edges())
            pq.add(edge);
        UnionFind uf = new UnionFind(graph.getV());

        while (mst.getE() < (graph.getV()-1) && !pq.isEmpty()) {
            Edge e = pq.remove(); // 从pq中得到权值最小的边
            int from = e.from; int to = e.to;

            if (uf.connected(from,to)) // 同根,即已经在同一棵树上
                continue;
            uf.union(from,to); // 合并两个子树
            mst.addEdge(e); // 添加到mst
        }

        return mst;
    }
}



// 测试类
class TestUndirectedGraph {
    // test UndirectedGraph
    public static void main(String[] args) {
        // 测试用例参考:https://zhuanlan.zhihu.com/p/33162490
        UndirectedGraph undirectedGraph = new UndirectedGraph(6); // 添加6个点,ID:0-5
        undirectedGraph.addEdge(new Edge(0,1,10));
        undirectedGraph.addEdge(new Edge(0,3,30));
        undirectedGraph.addEdge(new Edge(0,4,100));
        undirectedGraph.addEdge(new Edge(1,2,50));
        undirectedGraph.addEdge(new Edge(2,3,20));
        undirectedGraph.addEdge(new Edge(2,4,10));
        undirectedGraph.addEdge(new Edge(3,4,60));
        undirectedGraph.addEdge(new Edge(2,5,10));

//        System.out.println(undirectedGraph);

        // test dfs and bfs
        System.out.println(undirectedGraph.depthFirstSearch());
        System.out.println(undirectedGraph.breadthFirstSearch());

        // test prim
        UndirectedGraph lazyPrimMST = GeneratMST.lazyPrimMST(undirectedGraph);
        System.out.println(lazyPrimMST);
        UndirectedGraph MST = GeneratMST.primMST(undirectedGraph);
        System.out.println(MST);

        // test kruskal
        UndirectedGraph kruskalMST = GeneratMST.kruskalMST(undirectedGraph);
        System.out.println(kruskalMST);
    }
}




// 工具类

/**
 * 索引优先队列
 * @param <T>
 */
class IndexMinPriorityQueue<T extends Comparable<T>> {
    private T[] elements;
    private int[] indexPq;
    private int[] reIndexQp;
    private int N = 0;

    public IndexMinPriorityQueue(int maxN) {
        elements = (T[]) new Comparable[maxN + 1]; // elements这里可以不加1,没影响,但可能有其他用途
        indexPq = new int[maxN + 1];
        reIndexQp = new int[maxN +1];
        for (int i = 0; i <= maxN; i++)
            reIndexQp[i] = -1;
    }

    public boolean isEmpty() {
        return N == 0;
    }

    public int size() {
        return N;
    }

    public boolean contains(int k) {
        return reIndexQp[k] != -1;
    }

    // 插入:在k位置插入元素,位置k并不代表任何含义,只是存储在elements数组的索引位置;注意:这里k可以从索引0开始,没有任何影响
    public void insert(int k, T value) {
        N++;
        elements[k] = value; // 放入索引k
        indexPq[N] = k;      // 记录此元素所在索引位置(k)
        reIndexQp[k] = N;    // 记录indexPq数组中哪个位置(N)存储着此元素的索引
        swim(N);             // 从堆底加入并上浮,维护indexPq 和 reIndexQp
    }

    public T min() {
        return elements[indexPq[1]];
    }

    public int minIndex() {
        return indexPq[1];
    }

    // 删除最小值,并返回其索引
    public int delMin() {
        int indexOfMax = indexPq[1];
        if (elements[indexOfMax] == null) // 已为空,返回-1
            return -1;
        exch(1,N--);                 // 把最后一个元素(最小元素)放在顶端,然后N--(堆的大小-1)
        sink(1);                    // 让“最后”一个元素下沉
        elements[indexPq[N+1]] = null; // 将垃圾(删除的最小值)清空
        reIndexQp[indexPq[N+1]] = -1;  // 更新对应reIndexQp为-1
        indexPq[N+1] = 0;              // 更新最后一位删除的indexPq为0
        return indexOfMax;
    }

    // 删除索引k位置的元素,与删除最小值类似
    public void delete(int k) {
        int indexOfPq = reIndexQp[k];
        exch(indexOfPq,N--);
        swim(indexOfPq);
        sink(indexOfPq);
        elements[k] = null;
        reIndexQp[k] = -1;
        indexPq[N+1] = 0;
    }

    // 更新值
    public void change(int k, T newValue) {
        elements[k] = newValue;
        // 更新值后,可能出现三种情况:
        //    1. 比父节点小:需要上浮
        //    2. 比子节点大:需要下沉
        //    3. 大小在父节点和子节点之间:不执行任何操作
        // 所以此处采取的策略是先上浮在下沉(或先下沉再上浮)
        swim(reIndexQp[k]); // 上浮
        sink(reIndexQp[k]); // 下沉
    }

    // 用于堆实现的比较方法:这里怎么设计关乎着是大堆顶(<0)还是小堆顶(>0)
    private boolean greater(int i, int j) {
        return elements[indexPq[i]].compareTo(elements[indexPq[j]]) > 0;
    }

    // 用于堆实现的交换方法:交换indexPq[i]、indexPq[j] 和 reIndexPq[i]、reIndexPq[j]
    private void exch(int i, int j) {
        int tempPq = indexPq[i];
        indexPq[i] = indexPq[j];
        indexPq[j] = tempPq;
        reIndexQp[indexPq[i]] = i;
        reIndexQp[indexPq[j]] = j;
    }

    // 上浮
    private void swim(int k) {
        while(k > 1 && greater(k/2,k)) {
            exch(k/2,k); // k/2默认向下取整
            k = k/2;
        }
    }

    // 下沉
    private void sink(int k) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && greater(j,j+1)) // 找到较小的子节点,并将j指向它
                j++;
            if (!greater(k,j)) // 此时j一定指向较小的子节点,如果elements[indexPq[k]] <= elements[indexPq[j]],则下沉结束
                break;
            exch(k,j); // 如果没有break则说明elements[indexPq[k]] > elements[indexPq[j]],交换indexPq 和 reIndexQp
            k = j;     // 交换k、j,让k始终指向下沉的元素
        }
    }

    @Override
    public String toString() {
        return "      indexPq " +
                Arrays.toString(indexPq) + "\n" +
                "    reIndexQp " +
                Arrays.toString(reIndexQp) + "\n" +
                "PriorityQueue " +
                Arrays.toString(elements);
    }
}


/**
 * Union-find算法之加权quick-union的变种
 */
class UnionFind {
    private int[] parent;
    private byte[] rank;
    private int count;

    public UnionFind(int n) {
        if (n < 0) throw new IllegalArgumentException();
        count = n;
        parent = new int[n];
        rank = new byte[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            rank[i] = 0;
        }
    }

    public int find(int p) {
        validate(p);
        while (p != parent[p]) {
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        return p;
    }

    public int count() {
        return count;
    }

    public boolean connected(int p, int q) {
        return find(p) == find(q);
    }

    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP == rootQ) return;

        if      (rank[rootP] < rank[rootQ]) parent[rootP] = rootQ;
        else if (rank[rootP] > rank[rootQ]) parent[rootQ] = rootP;
        else {
            parent[rootQ] = rootP;
            rank[rootP]++;
        }
        count--;
    }

    private void validate(int p) {
        int n = parent.length;
        if (p < 0 || p >= n) {
            throw new IllegalArgumentException("index " + p + " is not between 0 and " + (n-1));
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值