带权图和最小生成树

带权图

一言以蔽之,我们的图中,边上带有权值。 如果我们的图带上权值的话,存储图的结构就不能用 TreeSet 了,这是因为 set只能帮助我们存储当前顶点的邻接点, 不能存储与这个邻接点 之间 边的权值。 因此,为了可以方便地存储 两个数据,准确说是一对数据,我们很容易想到用 映射,也就是TreeMap


import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;

public class WeightedGraph {
     
    private int V;
    private int E;
    private TreeMap<Integer, Integer>[] adj; // key 为邻接点 value 为  权值

    public WeightedGraph(String filename) {
     
        File file = new File(filename);
        try (Scanner scanner = new Scanner(file)) {
     
            V = scanner.nextInt();
            if (V < 0) throw new IllegalArgumentException("V must be non-negative");
            adj = new TreeMap[V];
            for (int i = 0; i < V; i++)
                adj[i] = new TreeMap<Integer, Integer>();
            E = scanner.nextInt();
            if (E < 0) throw new IllegalArgumentException("E must be non-negetive");

            for (int i = 0; i < E; i++) {
     
                int a = scanner.nextInt();
                validateVertex(a);
                int b = scanner.nextInt();
                validateVertex(b);
                int weight = scanner.nextInt();
                if (a == b)
                    throw new IllegalArgumentException("self loop is detected");
                // 如果有平行边,我们处理平行边,多半是保留权值最小的
                if (adj[a].containsKey(b))
                    throw new IllegalArgumentException("parallel edges  are detected");
                adj[a].put(b, weight);
                adj[b].put(a, weight);


            }


        } catch (IOException e) {
     

            e.printStackTrace();
        }


    }

    public void validateVertex(int v) {
     
        if (v < 0 || v >= V)
            throw new IllegalArgumentException("vertex " + v + "is invalid");
    }

    public boolean hasEdge(int v, int w) {
     
        validateVertex(v);
        validateVertex(w);
        return adj[v].containsKey(w);
    }

    public int V() {
     
        return V;
    }

    public Iterable<Integer> adj(int v) {
     
        validateVertex(v);
        return adj[v].keySet(); //  我们返回所有的键的集合 也就是返回了顶点v的所有邻接点
    }

    // 返回顶点v邻接的顶点w之间的权值
    public int getWeight(int v, int w) {
     
        if (hasEdge(v, w))
            return adj[v].get(w);
        throw new IllegalArgumentException(String.format("No edge %d--%d", v, w));
    }


    public int degree(int v) {
     
        validateVertex(v);
        return adj[v].size();
    }

    // 删除一条边 如果用户传来的v和w之间根本不存在边,remove做不了任何事情
    public void removeEdge(int v, int w) {
     
        validateVertex(w);
        validateVertex(v);
        adj[v].remove(w);
        adj[w].remove(v);

    }

    //做一次深拷贝 也就是做一个副本
    @Override
    public Object clone() {
     
        try {
     
            WeightedGraph cloned = (WeightedGraph) super.clone();
            cloned.adj = new TreeMap[V];
            for (int v = 0; v < V; v++) {
     
                cloned.adj[v] = new TreeMap<>();
                // 对map进行遍历
                for (Map.Entry<Integer, Integer> entry : adj[v].entrySet()) {
     
                    cloned.adj[v].put(entry.getKey(), entry.getValue());
                }
            }
            return cloned;

        } catch (CloneNotSupportedException e) {
     
            e.printStackTrace();
        }

        return null;
    }

    @Override
    public String toString() {
     
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("V = %d E = %d\n", V, E));
        for (int v = 0; v < V; v++) {
     
            stringBuilder.append(String.format("%d :", v));
            for (Map.Entry<Integer,Integer>  entry  :  adj[v].entrySet())
                stringBuilder.append(String.format("(%d: %d)", entry.getKey(),  entry.getValue()));
            stringBuilder.append("\n");
        }
        return stringBuilder.toString();
    }

    public static void main(String[] args) {
     
        WeightedGraph g =new WeightedGraph("g7.txt");
        System.out.println(g);

    }
}

最小生成树算法

我们之前已经接触过生成树的概念: 广度优先遍历生成树,深度优先遍历生成树。对于带权图来说,不同的生成树,权值的和不同。应用价值在哪?布线设计,找到最小的耗费,把所有的点连接在一起。 或者保证图连通,并且费用和最小。

在图中找v-1条边,使得费用和最小。一个很自然的想法,我们尽量选择短边。但是我们在找边的过程中,不能任意选择权值小的边,我们当前选择的边不能和已经有的边形成环。这样进行选择其实是一种贪心思想。 而这个算法本身就是大名鼎鼎的 Krusal 算法

切分定理

图中的顶点分为两个部分,就称为一个切分。这个划分是任意的。只要把图中的顶点分为两个部分就行。

如果一个边的两个端点,属于切分不同的两边,这个边就称为横切边。其实用这个方法,我们可以重新定义二分图。对于二分图,就是我们可以找到一种划分,将图中的顶点分为两个部分,使得图中的所有的边都是横切边。

切分定理:横切边中的最短边或者说权重最小的边,一定属于最小生成树。Krusal算法其实一直在应用切分定理。

实现krusal 算法

Krusal 算法让我们每次都从还没有进行选择边中选择权值最小的。同时当前选择的边 不能和已经选择的边构成环。 因此, 因此,我们需要两步, 第一步,对所有的带权边进行按照权重进行一次从小到大排序。 第二步:环检测判断。 第一步比较容易,我们直接排序就可以了。 第二步环检测稍微复杂一点。

由于我们在不断向 最后的结构边序列添加边, 这个环检测其实是动态的。对于这种环检测的动态判断,其实是判断连通性,如果新加入一条边,就形成了一个环,那么之前没有加入这条边的时候,这条边的两端点都是可以到达的。也就是连通的。 于是我们可以使用dfs 或者 bfs 。 但是这里是动态判断。—对于动态判断两个点是否可达,或者说是不是在同一个集合中,使用并查集

// 复习并查集,首先初始化parent 数组,初始时,每个顶点的父顶点都是自己
public class UF {
     
    private int[] parent;

    public UF(int n) {
      // 初始化顶点数组,每个顶点的初始父顶点都是自己
        parent = new int[n];
        for (int i = 0; i < n; i++)
            parent[i] = i;

    }

    public int Find_(int p) {
      // 不带路径压缩的循环查找
        int cur = p;
        while (parent[cur] != cur) {
     
            cur = parent[cur];
        }
        return cur;
    }

    public int Find(int p) {
      // 带路径压缩的递归查找
        if (parent[p] == p)
            return p;
        parent[p] = Find(parent[p]);
        return parent[p];
    }

    public boolean isConnected(int p, int q) {
     // 判断两个顶点是否在同一个集合中
        return Find(p) == Find(q);
    }

    public void UnionElements(int p, int q) {
     //把两个顶点所在的集合合并
        int proot = Find(p);
        int qroot = Find(q);
        parent[proot] = qroot;
    }
}

在进行具体的求解之前,我们需要定义一下 边这个类,因为我们最后需要得到的是一条一条的边

public class WeightedEdge implements Comparable<WeightedEdge> {
     
    private int v, w, weight;

    public WeightedEdge(int v, int w, int weight) {
     
        this.v = v;
        this.w = w;
        this.weight = weight;
    }

    public int getV() {
      return v; }

    public int getW() {
      return w; }


    @Override
    public String toString() {
     

        return String.format("(%d -%d :%d)", v, w, weight);
    }

    @Override
    public int compareTo(WeightedEdge other) {
     
        // 权值越小越靠前
        return this.weight - other.weight;
    }
}

下面我们就可以进行最小生成树的求解:


import java.util.ArrayList;
import java.util.Collections;

public class Krusal {
     
    private WeightedGraph g;
    private ArrayList<WeightedEdge> mst;

    public Krusal(WeightedGraph g) {
      //首先要判断是否连通
        this.g = g;
        mst = new ArrayList<WeightedEdge>();
        CCadv cc = new CCadv(g);
        if (cc.count() > 1) return; //不连通 也就是没有最小生成树
        // Kruskal
        ArrayList<WeightedEdge> edges = new ArrayList<>();
        for (int v = 0; v < g.V(); v++)
            for (int w : g.adj(v))
                if (v < w)     // 这是无向图,为了避免重复
                    edges.add(new WeightedEdge(v, w, g.getWeight(v, w)));
        Collections.sort(edges); // 排序
        // 环判断

        UF uf = new UF(g.V());
        // 核心算法部分
        for (WeightedEdge edge : edges) {
      // 依次遍历每一条边
            int v = edge.getV(); //取出边的两个顶点
            int w = edge.getW();
            if (!uf.isConnected(v, w)) {
      // 如果这两个顶点在已经收集的边集合中不可达
                mst.add(edge);  //那么这条边就是我们要的边
                uf.UnionElements(v, w);// 把这条边添加到结果中之后还要 在并查集中把顶点v,w合并
            }
        }


    }

    public ArrayList<WeightedEdge> result() {
      // 返回最后的结果
        return mst;
    }

	public  static  void  main(String[] args){
     

        WeightedGraph g = new WeightedGraph("g7.txt");
        Krusal krusal = new Krusal(g) ;

        System.out.println(krusal.result());
   }
}

时间复杂度是 O(E log E) 时间复杂度主要在 排序这里。

图的文件数据如下:

7 12
0 1 2
0 3 7
0 5 2
1 2 1
1 3 4
1 4 3
1 5 5
2 4 4
2 5 4
3 4 1
3 6 5
4 6 7

prime算法

操作切分,从1 : V-1 开始,每次找当前切分的最短横切边,扩展切分,直到没有切分。

import java.util.ArrayList;

public class Prime {
     
    private WeightedGraph g;
    private ArrayList<WeightedEdge> mst;

    public Prime(WeightedGraph g) {
     
        this.g = g;
        mst = new ArrayList<>();
        CCadv cc = new CCadv(g);
        if (cc.count() > 1)
            return;

        // prime 核心算法
        boolean[] visited = new boolean[g.V()]; // 生成树的顶点值用 true标记,非生成树的顶点值用 false 标记
        visited[0] = true; // 从0开始进行切分
        for (int i = 1; i < g.V(); i++) {
      // 循环v-1 次找出v-1 条边
            WeightedEdge minEdge = new WeightedEdge(-1, -1, Integer.MAX_VALUE);
            for (int v = 0; v < g.V(); v++) {
      // 遍历每一个顶点
                if (visited[v]) {
      // 如果v已经是生成树的顶点 那么遍历v的所有邻接点
                    for (int w : g.adj(v)) {
     
                        if (!visited[w] && g.getWeight(v, w) < minEdge.getWeight())
                            minEdge = new WeightedEdge(v, w, g.getWeight(v, w));
                    }
                }
            }
            mst.add(minEdge); // 简单粗暴 把收录进行MST的边上的顶点都划为生成树的顶点
            visited[minEdge.getV()] = true;
            visited[minEdge.getW()] = true;

        }


    }

    public ArrayList<WeightedEdge> result() {
     
        return mst;
    }

    public  static  void main(String[] args){
     
        WeightedGraph g = new WeightedGraph("g7.txt");
        Prime prime =new Prime(g)  ;
        System.out.println(prime.result());

    }

}

时间复杂度是 : O((v-1) *(V+E)) = O(v * E) 时间复杂度比较高。

Prime 算法的优化

我们之前的prime算法实现是这样的: 遍历每一个顶点 —> 判断这个顶点是否是当前生成树的顶点。如果是生成树的顶点—> 遍历这个顶点的每一条横切边。 然后经过一次一次的比较,找出当前权重最小的横切边。 但是这样做,会存在重复遍历问题。

我们每一轮产生的横切边只不过是在前一轮的基础上改变了一些边。我们可以使用优先队列帮我们动态找到当前最小的横切边。

优先队列中存储的边不一定是合法的边。当我们的最小生成树 新收录一个顶点,会导致一些之前的横切边变得不再是横切边。如果我们要在优先队列中删除这些边,会比较麻烦。 一个处理方式是我们依然存储这些边,只是在获取最小权值的边的时候,对边的合法性进行判断。

import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.Queue;

public class Prime {
     
    private WeightedGraph g;
    private ArrayList<WeightedEdge> mst;

    public Prime(WeightedGraph g) {
     
        this.g = g;
        mst = new ArrayList<>();
        CCadv cc = new CCadv(g);
        if (cc.count() > 1)
            return;

        // prime 核心算法
        boolean[] visited = new boolean[g.V()]; // 生成树的顶点值用 true标记,非生成树的顶点值用 false 标记
        visited[0] = true; // 从0开始进行切分
        Queue<WeightedEdge> pq = new PriorityQueue<>();
        //添加初始状态下的横切边
        for (int w : g.adj(0))
            pq.add(new WeightedEdge(0, w, g.getWeight(0, w)));


        while (!pq.isEmpty()) {
     
            WeightedEdge minEdge = pq.remove();// 有可能是最小边
            if (visited[minEdge.getV()] && visited[minEdge.getW()])// 如果这条边的两个端点都已经被访问过的话,那么这条边不是合法的边
                continue;
            mst.add(minEdge);
            //  生成树新拓展的顶点
            int newV = visited[minEdge.getV()] ? minEdge.getW() : minEdge.getV();
            visited[newV]  = true;
            for(int w :  g.adj(newV))
                if(!visited[w]) //  找新的横切边
                    pq.add(new WeightedEdge(newV,w, g.getWeight(newV,w))) ;
        }
    }

    public ArrayList<WeightedEdge> result() {
     
        return mst;
    }

    public static void main(String[] args) {
     
        WeightedGraph g = new WeightedGraph("g7.txt");
        Prime prime = new Prime(g);
        System.out.println(prime.result());
    }
}

时间复杂度 : O(E log E) 手写kursal 需要自己写并查集, 但是手写 prime 我们一般不用自己手写优先队列。其实Kruskal 算法是早于并查集的。

你可能感兴趣的:(图论)