圖論(中)有權圖之最小生成樹問題

有權圖的設計

增長一個邊的類,鄰接表和鄰接矩陣中保存的都是類的對象java

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 |ide

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

最小生成樹

針對帶權無向圖 針對連通圖優化

找v-1條邊,鏈接v個節點,使總權值最小ui

切分定理

把圖中的節點分爲兩部分,成爲一個切分(Cut).
若是一個邊的兩個端點,屬於切分(Cut)不一樣的兩邊,這個邊稱爲橫切邊。
defaultthis

切分定理:
給定任意切分,橫切邊中權值最小的邊必然屬於最小生成樹。
defaultspa

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實現code

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();
}
相關文章
相關標籤/搜索