1

有权图的设计

增加一个边的类,邻接表和邻接矩阵中保存的都是类的对象

public class Edge<Weight extends Number & Comparable> implements Comparable<Edge>{
    
    private int 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.a = e.a;
        this.b = e.b;
        this.weight = e.weight;
    }

    public int v() {
        return a;
    }

    public int w() {
        return b;
    }

    public Weight wt() {
        return weight;
    } 

    public int other(int x) {
        if (x != a && x != b) {
            return -1;
        }

        return x == a ? b : a;
    }
    
    @Override
    public int compareTo(Edge that) {
        return 0;
    }

    @Override
    public String toString() {
        return "" + a + "-" + ": " + weight;
    }
}

加权图的接口

public interface WeightedGraph<Weight extends Number & Comparable>  {
    public int getNodesCount();
    public int getEdgesCount();
    public void addEdge(Edge<Weight> e);
    public boolean hasEdge(int v, int w);
    public void show();
    public Iterable<Edge<Weight>> adj(int v);
}

邻接表

import java.util.List;
import java.util.ArrayList;

public class SparseWeightedGraph<Weight extends Number & Comparable> implements WeightedGraph {
    private int n;
    private int m;
    private boolean directed;
    private List<Edge<Weight>>[] g;

    public SparseWeightedGraph(int n, boolean directed) {
        if (n < 0) {
            return;
        }
        this.n = n;
        this.directed = directed;
        this.g = (ArrayList<Edge<Weight>>)new ArrayList[n];

        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<Edge<Weight>>();
        }
    }

    @Override
    public int getNodesCount() {
        return n;
    }

    @Override
    public int getEdgesCount() {
        return m;
    }

    @Override
    public boolean hasEdge(int v, int w) {
        if (!isLegal(v) || !isLegal(w)) {
            return false;
        }

        for (Edge e : g.adj(v)) {
            if (e.other(v) == w) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void addEdge(Edge e) {
        if (e == null) {
            return;
        }

        int v = e.v();
        int w = e.w();

        if (!isLegal(v) || !isLegal(w)) {
            return;
        }

        g[v].add(new Edge(e));


        if (v != w && !directed) {
            g[w].add(new Edge(w, v, e.wt()));
        }

        m++;
    }

    @Override 
    public Iterable<Edge<Weight>> adj(int v) {
        List<Edge<Weight>> res = new ArrayList<>();
        if (!isLegal(v)) {
            return res;
        }

        return g[v];
    }

    @Override
    public void show() {
        for( int i = 0 ; i < n ; i ++ ){
            System.out.print("vertex " + i + ":\t");
            for( int j = 0 ; j < g[i].size() ; j ++ ){
                Edge e = g[i].get(j);
                System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
            }
            System.out.println();
        }
    }

    private boolean isLegal(int i) {
        return i >= 0 && i < n;
    }
}

在此处任性地增加一段知识点

| Summary of Queue methods |

Method Throws exception Returns special value
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()

最小生成树

针对带权无向图 针对连通图

找v-1条边,连接v个节点,使总权值最小

切分定理

把图中的节点分为两部分,成为一个切分(Cut).
如果一个边的两个端点,属于切分(Cut)不同的两边,这个边称为横切边。
default

切分定理:
给定任意切分,横切边中权值最小的边必然属于最小生成树。
default

Lazy Prim算法

Lazy prim的时间复杂度O(ElogE),E为边数
需要维护一个最小堆

import java.lang.reflect.Array;

public class MinHeap<T extends Comparable> {
    private T[] data;
    private int count;
    private int capacity;
    private Class<T> type;

    public MinHeap(int capacity, Class<T> type) {
        this.capacity = capacity;
        this.count = 0;
        this.type = type;
        data = (T[]) Array.newInstance(type, capacity + 1);
    }

    public int size() {
        return count;
    }

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

    public void insert(T item) {
        if (count + 1 >= capacity && capacity * 2 + 1 < Integer.MAX_VALUE) {
            this.capapcity *= 2;
            T[] newData = (T[]) Array.newInstance(this.type, this.capacity + 1);
            System.arraycopy(data, 0, newData, 0, count + 1);
            data = newData;
        }

        data[++count] = item;
        shiftUp(count);
    }

    private void shiftUp(int v) {
        while (v > 1 && data[v].compareTo(data[v / 2]) < 0) {
            swap(data, v, v / 2);
            v /= 2;
        }
    }

    public T extractMin() {
        if (count <= 0) {
            return null;
        }

        T res = data[1];
        swap(data, 1, count--);
        shiftDown(1);
        return res;
    }

    private void shiftDown(int k) {
        while (k * 2 <= count) {
            int j = k * 2;
            if (j + 1 < count && data[j + 1].compareTo(data[j]) < 0) {
                j++;
            }

            if (data[k].compareTo(data[j]) <= 0) {
                break;
            }
            swap(data, k, j);
            k = j;
        }
    }

    private void swap(T[] data, int i, int j) {
        T t = data[i];
        data[i] = data[j];
        data[j] = t;
    }
}

lazy prim实现

public class LazyPrimMST<Weight extends Number & Comparable> {
    private MinHeap<Edge<Weight>> minHeap;
    private List<Edge<Weight>> mst;
    private Number mstWeight;
    private Graph g;
    private boolean[] marked;

    public LazyPrimMST(Graph g) {
        this.g = g;
        int n = g.getNodesCount();
        int m = g.getEdgesCount();
        minHeap = new MinHeap<>(Edge.class, m);
        marked = new boolean[n];

        visit(0);

        while (!minHeap.isEmpty()) {
            Edge<Weight> e = minHeap.extractMin();

            //这条边已经不是横切边了
            if (marked[e.v()] == markded[e.w()]) {
                continue;
            }

            //不然的话,这条横切边应该在最小生成树中,将其加入list
            mst.add(e);

            if (marked[e.v()]) {
                visit(e.w());
            } else {
                visit(e.v());
            }
        }

        mstWeight = mst.get(0).wt();
        for (int i = 1; i < mst.size(); i++) {
            mstWeight += mst.get(i).wt();
        }
    } 

    //处于还未处于的结点,发现横切边,将横切边存入最小堆中
    private void visit(int k) {
        if (marked[k]) {
            return;
        }

        marked[k] = true;

        for (Edge<Weight> e : g.adj(v)) {
            //如果这是一条横切边
            if (!marked[e.other(v)]) {
                minHeap.insert(e);
            }
        }
    }
}

Kruskal算法

需要采用并查集的算法,用来判断是否存在环。

回顾并查集

并查集的归并过程,是将元素放入一个集合中,集合用一个数组id表示
先回顾quickUnion版本

public class QuickUnion {
    private int[] id;//元素分组
    private int count;//有count个元素

    public QuickUnion(int count) {
        this.count = count;
        this.id = new int[count];
        //初始化时,每个元素一个分组
        for (int i = 0; i < count; i++) {
            id[i] = i;
        }
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);

        if (qRoot == pRoot) {
            return;
        }

        //任意使一个结点指向另一个的父结点即可
        id[p] = qRoot;
    }

    //每个元素的分组是按根节点分组
    public int find(int p) {
        if (p < 0 || p >= count) {
            return -1;
        }

        //根结点自己指向自己
        while (p != id[p]) {
            p = id[p];
        }

        return p;
    }

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

优化的方向是使树的高度尽可能合理地低,每一步可以使结点数量少的移到数量多的根上,降低树的高度。但是可能出现的问题是,结点数多不见得树就高。因此,有基于rank的优化,其中rank[i]表示以i为根的集合所表示的树的层数

public class RankUF {
    private int[] parent;//用这样一个数组来表示元素所属的集合,表示元素指向的根结点
    private int count;
    private int[] rank;//rank[i]表示以i为根的集合所表示的树的层数

    public RankUF(int count) {
        this.count = count;
        this.parent = new int[count];
        this.rank = new int[count];

        for (int i = 0; i< count; i++) {
            parent[i] = i;
            rank[i] = i;
        }
    }

    public int find(int p) {
        if (p < 0 || p >= count) {
            return -1;
        }

        // 不断去查询自己的父亲节点, 直到到达根节点
        // 根节点的特点: parent[p] == p
        while (p != parent[p]) {
            p = parent[p];
        }
        return p;
    }

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

    /**
    * 关键在union的过程
    * 根据两个元素所在树的元素个数不同判断合并方向
    * 将元素个数少的集合合并到元素个数多的集合上
    */
    public void union(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的值
            rank[qRoot] += 1;
        }
    }
}

最后的优化为路径压缩
此时有两种方法,第一种是每隔一层,压缩一层,修改find方法即可,如下

public int find(int p) {
    if (p < 0 || p >= count) {
        return -1;
    }

    while (p != parent[p]) {
        parent[p] = parent[parent[p]];//增加这一行即可
        p = parent[p];
    }
    return p;
}

第二种方法是压缩到底,这里效率不见得最高,因为用到了递归,会有一定的损失,所以上述方法已经很好了。正则压缩到底的优化

public int find(int p) {
    if (p < 0 || p >= count) {
        return -1;
    }

    if (p != parent[p]) {
        parent[p] = find(parent[p]);
    }
    return parent[p];
}

kruskal算法思想

每次找最短边,只要不构成环,那么这条边就是最小生成树中的边

import java.util.List;
import java.util.ArrayList;

public class KruskalMST<Weight extends Number & Comparable> {
    
    private List<Edge<Weight>> mst;
    private Number mstWeight;

    public KruskalMST(WeightedGraph g) {
        int n = g.getNodesCount();
        int m = g.getEdgesCount();
        MinHeap<Edge> minHeap = new MinHeap<>(m, Edge.class);
        for (int i = 0; i < n; i++) {
            for (Object o : g.adj(i)) {
                Edge<Weight> e = (Edge<Weight>) o;
                //防止重复放入边
                if (e.v() <= e.w()) {
                    minHeap.insert(e);
                }
            }
        }

        RankUF uf = new RankUF(n);
        while (!minHeap.isEmpty() && mst.size() < n - 1) {
            Edge<Weight> e = minHeap.extractMin();

            if (uf.isConnected(e.v(), e.w())) {
                continue;
            }

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

        mstWeight = mst.get(0).wt();
        for (int i = 1; i < mst.size(); i++) {
             mstWeight = mstWeight.doubleValue() + mst.get(i).wt().doubleValue();
        }
    }

    public List<Edge<Weight>> getMSTEdges() {
        return mst;
    }

    public Number getWeight() {
        return mstWeight;
    }
}

总结:此时有泛型擦除问题

public interface Interface1<Weight extends Number & Comparable> {
    public Iterable<Edge<Weight>> adj();
}
public class Test {
    public Test(Interface1 t) {
        for (Edge<Weight> e : t.adj()) {
            //此时编译会报错,因为Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
            //此时在Test眼中,是看不到Interface1的返回值中的泛型的,只能看到Iterable,在编写java代码时,要时刻提醒自己,“这不是个泛型,这只是个Object”
        }
    }
}

有两种解决方法
第一种,即代码中,用Object接收,然后强制类型转换
第二种,泛型在当前类中,因此编译时可以识别到

public class Test {
    public Test(Interface2 t) {
        for (Edge<Weight> e : t.adj()) {
            //此时不会报错
        }
    }
}

interface Interface2 {
    public Iterable<Edge<Weight>> adj();
}

菟潞寺沙弥
303 声望55 粉丝

引用和评论

0 条评论