数据结构笔记_最小生成树

一. 有权图

  • 边上的权值不一定是数值,而可以是各种类型

  • 有权图的邻接矩阵和邻接表
  • 抽象出一个Edge类,存放边的信息

//Edge类, 表示边的信息
public class Edge<Weight extends Number & Comparable> implements Comparable<Edge> {
    private int a, b;         //边的两个节点, 表示边 a->b
    private Weight weight;    //边的权重,不一定是数字类型

    public Edge(int a,int b, Weight weight){
        this.a = a;
        this.b = b;
        this.weight = weight;
    }
    public Edge(Edge<Weight> e){
        this(e.a, e.b, e.weight);
    }

    //返回第一个顶点
    public int v(){
        return this.a;
    }


    //返回第二个顶点
    public int w(){
        return this.b;
    }


    //返回权重
    public Weight weight(){
        return this.weight;
    }


    //给定一个顶点返回另一个顶点
    public int otherV(int v){
        assert v == a || v == b;
        return v == a? b:a;
    }

    //输出边的信息
    public String toString(){
        return ""+a+"-"+b+":"+weight;
    }


    //边之间的比较
    @Override
    public int compareTo(Edge other){
        if(weight.compareTo(other.weight())>0){
            return 1;
        }else if(weight.compareTo(other.weight())<0){
            return -1;
        }else{
            return 0;
        }
    }
}
//有权稠密图 邻接矩阵表示
public class DenseGraph<Weight extends Number & Comparable> implements WeightGraph<Weight> {
    private int n;               //节点数
    private int m;               //边数
    private boolean isDirected;  //是否为有向图
    private Edge<Weight>[][] g;  //图的具体数据

    public DenseGraph(int n, boolean isDirected){
        assert n>=0;
        this.n = n;
        this.m = 0;
        this.isDirected = isDirected;

        // g初始化为n*n的矩阵, 每一个g[i][j]均为null, 表示没有任和边
        this.g = new Edge[n][n];
        for(int i=0; i<n; i++){
            for(int j=0; j<n; j++){
                g[i][j] = null;
            }
        }
    }

    @Override
    public int V(){
        return this.n;
    }

    @Override
    public int E(){
        return this.m;
    }


    //检查边 v——>w 是否存在
    @Override
    public boolean hasEdge(int v, int w){
        assert v >= 0 && v < n ;
        assert w >= 0 && w < n ;
        return g[v][w] != null;
    }

    @Override
    public void addEdge(Edge<Weight> e){
        assert e.v()>=0 && e.v()<n;
        assert e.w()>=0 && e.w()<n;

        //去除重复边
        //if(e.compareTo(g[e.v()][e.w()]) == 0){
        //    return;
        //}

        //去重平行边
        if(hasEdge(e.v(), e.w())){
            return;
        }

        g[e.v()][e.w()] = e;
        if(e.v()!=e.w() && !isDirected){
            g[e.w()][e.v()] = e;
        }

        m++;
    }


    // 返回图中一个顶点的所有邻边
    // 由于java使用引用机制,返回一个Vector不会带来额外开销,
    @Override
    public Iterable<Edge<Weight>> adjIterator(int v){
        assert v >= 0 && v < n;

        Vector<Edge<Weight>> vector = new Vector<>();
        for(int i=0; i<n; i++){
            if(g[v][i]!=null){
                vector.add(g[v][i]);
            }
        }

        return vector;
    }

    @Override
    public void show(){
        for(int i=0; i<n; i++){
            for(int j=0; j<n; j++){
                if(g[i][j]!=null){
                    System.out.printf("%.2f\t",g[i][j].weight());
                }else{
                    System.out.print("null\t");
                }

            }
            System.out.println();
        }
    }
}
//有权稀疏图 邻接表的表示
public class SpraseGraph<Weight extends Number & Comparable> implements WeightGraph<Weight> {
    private int n;                      //节点数
    private int m;                      //边数
    private boolean isDirected;         //是否为有向图
    private Vector<Edge<Weight>>[] g;   //图的具体数据

    public SpraseGraph(int n, boolean isDirected){
        assert n>=0;
        this.n = n;
        this.m = 0;
        this.isDirected = isDirected;
        this.g = (Vector<Edge<Weight>>[]) new Vector[n];
        //初始化数组中的每个Vetor
        for(int i=0; i<n; i++){
            g[i] = new Vector<Edge<Weight>>();
        }
    }

    @Override
    public int V(){
        return this.n;
    }

    @Override
    public int E(){
        return this.m;
    }


    //检查边 v——>w 是否存在
    @Override
    public boolean hasEdge(int v, int w){
        assert v>=0 && v<n;
        assert w>=0 && w<n;

        for(int i=0; i<g[v].size(); i++){
            if(g[v].elementAt(i).w() == w){
                return true;
            }
        }

        return false;
    }

    @Override
    public void addEdge(Edge<Weight> e){
        assert e.v()>=0 && e.w()<n;
        assert e.w()>=0 && e.w()<n;

        //去除平行边
        // 注意, 由于在邻接表的情况, 查找是否有重边需要遍历整个链表
        // 我们的程序允许重边的出现
        //if(hasEdge(e.v(), e.w())){
        //   return ;
        //}

        g[e.v()].add(e);
        if(e.v() != e.w() && !isDirected){
            g[e.w()].add(new Edge(e.w(), e.v(), e.weight()));
        }
        m++;
    }

    @Override
    public Iterable<Edge<Weight>> adjIterator(int v){
        assert v >= 0 && v < n;
        return g[v];
    }

    @Override
    public void show(){
        for(int i = 0; i<n; i++){
            System.out.print("Vertex "+i+" : ");
            for(int j=0; j<g[i].size(); j++){
                System.out.print("(to: "+g[i].elementAt(j).w()+", weight: "+g[i].elementAt(j).weight()+")\t");
            }
            System.out.println();
        }
    }
}

 

二. 有权图的最小生成树问题

1.最小生成树

  • 生成树:一个连通图中,能连通所有顶点而又不产生回路的任何子图都是它的生成树(n个顶点+n-1条边)
  • 最小生成树:所有生成树中,各边的权重和最小的生成树
  • 最小生成树针对的是:带权无向图,连通图
  • 求最小生成树就是找v-1条边连接v个顶点,使得总权值最小

2.切分定理(Cut Property)

  • 把图中的节点分为两个部分,成为一个切分(Cut)
  • 如果一个边的两个端点,属于切分(Cut)不同的两边,这个边称为横切边(Crossing Edge)
  • 切分定理:给定任意切分,横切边中权值最小的边必然属于最小生成树
  • 若图中有多个相等的"横切边",则该图的最小生成树不唯一

3. lazy-prim算法求最小生成树

  • 缺点:所有的边都要进入最小堆
  • 时间复杂度为:O(logE)
//lazy-prim算法带权无向图最小生成树
public class LazyPrimMST<Weight extends Number & Comparable> {
    private WeightGraph<Weight> graph;           //用来生成最小生成树的图
    private boolean[] isVisited;         //标记节点是否被访问
    private Vector<Edge<Weight>> mst;    //最小生成树中的边
    private Number mstWeight;            //最小生成树的权重和
    private PriorityQueue<Edge<Weight>> pq;            //算法的辅助数据结构,用来选出权重最小的边

    public LazyPrimMST(WeightGraph<Weight> graph){
        this.graph = graph;
        this.isVisited = new boolean[graph.V()];
        this.mst = new Vector<>();
        this.mstWeight = 0;
        this.pq = new PriorityQueue<>(graph.E(), new Comparator<Edge<Weight>>() {
            @Override
            public int compare(Edge<Weight> o1, Edge<Weight> o2) {
                return o1.compareTo(o2);
            }
        });
    }


    //辅助方法,访问节点,并挑选该节点未被访问过的邻边加入优先队列
    private void visit(int v){
        //判断传入的节点v是否被访问过
        assert !isVisited[v];

        //访问节点v
        isVisited[v] = true;
        for(Edge<Weight> e: graph.adjIterator(v)){
            if(!isVisited[e.otherV(v)]){
                pq.add(e);
            }
        }
    }


    //最小生成树实现  lazy-prim算法
    public void mst(){

        //初始化,先访问节点0
        visit(0);

        //lazy-prim
        while(!pq.isEmpty()){
            //从优先队列中取得当前队列中权重最小的边
            Edge<Weight> e = pq.remove();

            //若边的两个端点均被访问过,则抛弃这条边,在从优先队列中取得下一个权重最小的边
            if(isVisited[e.v()] == isVisited[e.w()]){
                continue;
            }

            //若边的另一个端点没有被访问过,则该边为最小生成树中的一条边
            mst.add(e);

            //顺着这条边接着访问边中那个为被访问过的节点
            if(!isVisited[e.v()]){
                visit(e.v());
            }else{
                visit(e.w());
            }
        }

        //计算最小生成树中的权重和
        for(int i=0; i<mst.size(); i++){
            mstWeight = mstWeight.doubleValue() + mst.elementAt(i).weight().doubleValue();
        }
    }


    // 返回最小生成树的所有边
    public Vector<Edge<Weight>> mstEdges(){
        return this.mst;
    }


    // 返回最小生成树的权值
    public Number minWeight(){
        return this.mstWeight;
    }
}

4. prim算法

  • prim算法是贪婪算法的一个典型例子,有点类似于dijkstra算法。
  • 算法思想:
  1. 横切边:若一条边中有且只有一个节点被访问过(被标记过),则该边为一条横切边
  2. 根据切分定理,将节点切分已加入最小生成树的部分和未加入最小生成树的部分,从树节点(已标记的节点)出发寻找最短横切边,添加该最短横切边直到所有结点都加入到最小生成树。
  3. 从任意一个点开始选择,找出这个点连接的所有的边,然后找出最短的,选中这条边加入到生成树中,枚举每一个树顶点到每一个非树顶点的所有的边,然后找最短的边加入到生成树,一直加边n-1次, 直到所有的顶点都被加入到生成树中。
  • 辅助数据结构:利用一个最小索引堆,开辟V个空间,索引为节点,索引下存储的值是当前节点到树区的权重(距离)
  • 时间复杂度:O(logV)
//索引堆(构造时,可根据参数选择最大或者最小), 底层基于数组实现
public class IndexMinHeap<E extends Comparable> {
    private boolean isMin; //是否为最小索引堆
    private E[] items;      //存放具体数据
    private int[] indexes; //索引堆的底层数组,数组内存储的是具体数据的索引,保持堆的排序
    private int[] reverse; //索引堆中的反向索引  reverse[indexes[i]]=i;
    private int size;      //堆中数据的个数
    private int capacity;  //堆的容量

    public IndexMinHeap(int capacity, boolean isMin){
        this.isMin = isMin;
        this.capacity = capacity;
        this.size = 0;
        this.indexes = new int[capacity];
        this.items = (E[])new Comparable[capacity];
        this.reverse = new int[capacity];
        for(int i=0; i<capacity; i++){
            reverse[i] = -1;
        }
    }

    // 返回索引堆中的元素个数
    public int size(){
        return this.size;
    }

    // 返回一个布尔值, 表示索引堆中是否为空
    public boolean isEmpty(){
        return this.size == 0;
    }


    // 向索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
    public void add(int i, E item){
        assert i>=0 && i<capacity;
        assert size<capacity;
        assert !contain(i);

        items[i] = item;
        indexes[size] = i;
        reverse[indexes[size]] = size;
        shiftUp(size);
        size++;
    }


    // 从索引堆中取出堆顶元素, 同时维护索引堆
    public E remove(){
        assert size>0;

        E ret = items[indexes[0]];
        indexes[0] = indexes[size-1];
        reverse[indexes[0]] = 0;
        size--;
        reverse[indexes[size]] = -1;
        shiftDown(0);

        return ret;
    }


    // 从索引堆中取出堆顶元素的索引
    public int removeIndex(){
        assert size>0;

        int ret = indexes[0];
        indexes[0] = indexes[size-1];
        reverse[indexes[0]] = 0;
        size--;
        reverse[indexes[size]] = -1;
        shiftDown(0);

        return ret;
    }


    // 获取索引堆中的堆顶元素
    public E get(){
        assert size>0;
        return items[indexes[0]];
    }


    // 获取索引堆中的堆顶元素的索引
    public int getIndex(){
        assert size>0;
        return indexes[0];
    }


    // 看索引i所在的位置是否存在元素
    public boolean contain(int i){
        assert i>=0 && i<capacity;

        return reverse[i]!=-1;
        //return items[i]!=null;
    }


    // 获取索引堆中索引为i的元素
    public E get(int i){
        assert reverse[i] != -1;
        return items[i];
    }


    // 将索引堆中索引为i的元素修改为newItem
    public void set(int i, E newItem){
        assert contain(i);

        items[i] = newItem;
        shiftUp(reverse[i]);
        shiftDown(reverse[i]);
    }


    //********************
    //* 最小索引堆核心辅助函数
    //********************

    // 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
    private void shiftUp(int i){
        E e = items[indexes[i]];
        int ret = indexes[i];
        int j;
        
        //选择最小索引堆或者是最大索引堆
        if(isMin){
            for(j=i; e.compareTo(items[indexes[(j-1)/2]])<0 && j>0; j=(j-1)/2){
                indexes[j] = indexes[(j-1)/2];
                reverse[indexes[j]] = j;
            }

        }else{
            for(j=i; e.compareTo(items[indexes[(j-1)/2]])>0 && j>0; j=(j-1)/2){
                indexes[j] = indexes[(j-1)/2];
                reverse[indexes[j]] = j;
            }
        }

        indexes[j] = ret;
        reverse[indexes[j]] = j;
    }



    // 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
    private void shiftDown(int i){
        while (2*i+1<size){
            int j = 2*i+1;

            if(isMin){
                if(j+1<size && items[indexes[j+1]].compareTo(items[indexes[j]])<0){
                    j = j+1;
                }

                if(items[indexes[i]].compareTo(items[indexes[j]])<0){
                    break;
                }

            }else{
                if(j+1<size && items[indexes[j+1]].compareTo(items[indexes[j]])>0){
                    j = j+1;
                }

                if(items[indexes[i]].compareTo(items[indexes[j]])>0){
                    break;
                }
            }
            
            int temp = indexes[i];
            indexes[i] = indexes[j];
            reverse[indexes[i]] = i;
            indexes[j] = temp;
            reverse[indexes[j]] = j;

            i = j;
        }
    }
}
//prim算法 求带权无向图的最小生成树
public class PrimMST<Weight extends Number & Comparable> {
    private WeightGraph<Weight> graph;    //待求最小生成树的图
    private Vector<Edge<Weight>> mst;     //最小生成树的所有边
    private Number mstWeight;             //最小生成树的权重和
    private boolean[] isVisited;          //辅助数据结构,记录节点是否被访问过
    private Edge<Weight>[] edgeTo;        //辅助数据结构,记录“横切边”, edgeTo[w] 表示已标记区域中某个节点到未标记节点w的边
    private IndexMinHeap<Weight> imheap;  //辅助数据结构,最小索引堆,索引表示节点,对应的数据表示该节点到已标记区域的权重

    public PrimMST(WeightGraph graph){
        this.graph = graph;
        assert( graph.E() >= 1 );
        this.imheap = new IndexMinHeap<>(graph.V(), true);
        this.isVisited = new boolean[graph.V()];
        this.mst = new Vector<Edge<Weight>>();
        this.mstWeight = 0;
        this.edgeTo = (Edge<Weight>[]) new Edge[graph.V()];

        for(int i=0; i<graph.V(); i++){
            edgeTo[i] = null;
            isVisited[i] = false;
        }
    }

    //访问节点v
    private void visit(int v){
        assert !isVisited[v];
        isVisited[v] = true;

        for(Edge<Weight> e: graph.adjIterator(v)){
            if(!isVisited[e.otherV(v)]){

                //判断边 v——otherV(v) 是否被标记为"横切边"
                //若未标记过,则进行标记,并加入最小堆
                //若标记过,则判断之前标记的“横切边”与当前由节点v发出的“横切边”v——otherV(v)那个更短,更新最小堆
                if(edgeTo[e.otherV(v)] == null){
                    edgeTo[e.otherV(v)] = e;
                    imheap.add(e.otherV(v), e.weight());
                }else if(edgeTo[e.otherV(v)].weight().compareTo(e.weight())>0){
                    //更新到节点e.otherV(v)的“横切边”
                    edgeTo[e.otherV(v)] = e;
                    imheap.set(e.otherV(v), e.weight());
                }
            }
        }
    }

    //prim算法, 生成最小生成树
    public void prim(){

        //prim
        visit(0);
        while (!imheap.isEmpty()){
            // 使用最小索引堆找出已经访问的边中权值最小的边
            // 最小索引堆中,索引表示节点,索引对应的值表示该节点到已标记区域的权重(距离)
            int v = imheap.removeIndex();
            //assert( edgeTo[v] != null );
            mst.add(edgeTo[v]);
            visit(v);
        }

        // 计算最小生成树的权值
        for (int i=0; i<mst.size(); i++){
            mstWeight = mstWeight.doubleValue()+mst.elementAt(i).weight().doubleValue();
        }
    }

    //返回最小生成树的所有边
    public Vector<Edge<Weight>> mstEdges(){
        return this.mst;
    }

    //返回最小生成树的权重和
    public Number mstWeight(){
        return this.mstWeight;
    }
}

5. Kruslal算法

  • 算法思想:将图中所右边按照权重排序,从小到大依次试着将边加入到节点之间,若加入后不构成环,则该条边为最小生成树中的一条边,直至加构V-1条边;对于环的判断可以使用并查集实现
//并查集
public class UnionFind {
    private int[] parent;
    private int capacity;
    private int[] rank;

    public UnionFind(int capacity){
        this.capacity = capacity;
        this.parent = new int[capacity];
        this.rank = new int[capacity];
        for(int i=0; i<capacity; i++){
            parent[i] = i;
            rank[i] = 1;
        }
    }

    // 查找过程, 查找元素p所对应的集合编号
    // O(h)复杂度, h为树的高度
    private int find(int p){
        while (p!=parent[p]){
            parent[p]=parent[parent[p]];
            p = parent[p];
        }
        return p;
    }

    // 查看元素p和元素q是否所属一个集合
    // O(h)复杂度, h为树的高度
    public boolean isConnected(int p, int q){
        return find(p) == find(q);
    }

    // 合并元素p和元素q所属的集合
    // O(h)复杂度, h为树的高度
    public void unionElements(int p, int q){
        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot){
            return;
        }

        // 根据两个元素所在树的元素个数不同判断合并方向
        // 将元素个数少的集合合并到元素个数多的集合上
        if (rank[pRoot]<rank[qRoot]){
            parent[pRoot] = qRoot;
        } else if (rank[pRoot] > rank[qRoot]){
            parent[qRoot] = pRoot;
        }else{
            parent[pRoot] = qRoot;
            rank[qRoot]++;
        }
    }
}
//kruskal算法
public class KruskalMST<Weight extends Number & Comparable> {
    private WeightGraph<Weight> graph;
    private Vector<Edge<Weight>> mst;
    private Number mstWeight;

    public KruskalMST(WeightGraph graph){
        this.graph = graph;
        this.mst = new Vector<>();
        this.mstWeight = 0;
    }

    public void kruskal(){

        //利用优先队列对图中各条边按照权重从小到大排序
        PriorityQueue<Edge<Weight>> pq = new PriorityQueue<>(graph.E());
        for(int i=0; i<graph.V(); i++){
            for(Edge<Weight> e: graph.adjIterator(i)){
                //防止无向图中,同一条边两次入队
                if(e.v()<=e.w()){
                    pq.add(e);
                }
            }
        }


        //利用并查集检查加入一条边之前,该边的两个节点是否已经连接
        UnionFind uf = new UnionFind(graph.V());
        while(!pq.isEmpty() && mst.size()<graph.V()-1){
            Edge<Weight> e = pq.remove();
            if(uf.isConnected(e.v(), e.w())){
                continue;
            }

            mst.add(e);
            uf.unionElements(e.v(), e.w());
        }

        //计算最小生成树权重
        for(int i=0; i<mst.size(); i++){
            mstWeight = mstWeight.doubleValue() + mst.elementAt(i).weight().doubleValue();
        }
    }

    // 返回最小生成树的所有边
    public Vector<Edge<Weight>> mstEdges(){
        return mst;
    }

    // 返回最小生成树的权值
    public Number mstWeight(){
        return mstWeight;
    }
}

6. 三种算法的时间复杂度

7. Vyssotsky算法

  • 将边逐渐添加到生成树中,一旦形成环,删除环中权值最大的边

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值