Skip to content

Latest commit

 

History

History
1449 lines (1183 loc) · 41.6 KB

5_图.md

File metadata and controls

1449 lines (1183 loc) · 41.6 KB

(一)基础

图论基本概念

图的表示

// 图的接口
public interface Graph {

    public int V();
    public int E();
    public void addEdge(int v, int w);
    boolean hasEdge(int v, int w);
    void show();
    public Iterable<Integer> adj(int v);
}

1. 邻接矩阵

  • 邻接矩阵表示无向图
  • 邻接矩阵表示有向图
/**
 * 稠密图-邻接矩阵
 */
public class DenseGraph {

    private int n;  // 节点数
    private int m;  // 边数
    private boolean directed;   // 是否为有向图
    private boolean[][] g;      // 图的具体数据

    // 构造函数
    public DenseGraph( int n , boolean directed ){
        assert n >= 0;
        this.n = n;
        this.m = 0;    // 初始化没有任何边
        this.directed = directed;
        // g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
        // false为boolean型变量的默认值
        g = new boolean[n][n];
    }

    public int V(){ return n;} // 返回节点个数
    public int E(){ return m;} // 返回边的个数

    // 向图中添加一个边
    public void addEdge( int v , int w ){

        assert v >= 0 && v < n ;
        assert w >= 0 && w < n ;

        if( hasEdge( v , w ) ){
            return; 
        }
           

        g[v][w] = true;
        if( !directed ){
            g[w][v] = true;
        }
        m ++;
    }

    // 验证图中是否有从v到w的边
    boolean hasEdge( int v , int w ){
        assert v >= 0 && v < n ;
        assert w >= 0 && w < n ;
        return g[v][w];
    }
    
    //遍历邻边
    //返回图中一个顶点的所有相邻顶点
    public Iterable<Integer> adj(int v) {
        assert v >= 0 && v < n;
        Vector<Integer> adjV = new Vector<Integer>();
        for(int i = 0 ; i < n ; i ++ )
            if( g[v][i] )
                adjV.add(i);
        return adjV;
    }
}

2. 邻接表

  • 邻接表表示无向图
  • 邻接表表示有向图
/**
* 稀松图-领接表
*/
public class SparseGraph {

    private int n;  // 节点数
    private int m;  // 边数
    private boolean directed;    // 是否为有向图
    private Vector<Integer>[] g; // 图的具体数据

    // 构造函数
    public SparseGraph( int n , boolean directed ){
        assert n >= 0;
        this.n = n;
        this.m = 0;    // 初始化没有任何边
        this.directed = directed;
        // g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
        g = (Vector<Integer>[])new Vector[n];
        for(int i = 0 ; i < n ; i ++){
            g[i] = new Vector<Integer>();
        }
    }

    public int V(){ return n;} // 返回节点个数
    public int E(){ return m;} // 返回边的个数

    // 向图中添加一个边
    public void addEdge( int v, int w ){

        assert v >= 0 && v < n ;
        assert w >= 0 && w < n ;

        g[v].add(w);
        if( v != w && !directed ){
            g[w].add(v);
        }
        m ++;
    }

    // 验证图中是否有从v到w的边 -->时间复杂度:O(n)
    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 ){
                return true;
            }
        }
        return false;
    }
    
    //遍历邻边
    //返回图中一个顶点的所有相邻顶点
    public Iterable<Integer> adj(int v){
    	assert v>=0 && v<n;
    	return g[v];
	}
}

图的深度优先遍历(DFS)

/**
 * 图的深度优先遍历
 */
public class DepthFirstPaths {
    //标记顶点是否被访问过
    private boolean[] visited;
    //记录路径
    private int[] edgeTo;
    //起点
    private int s;

    public DepthFirstPaths(Graph graph,int s){
        visited=new boolean[graph.V()];
        edgeTo=new int[graph.V()];
        for(int i=0;i<graph.V();i++){
            edgeTo[i]=-1;
        }
        this.s=s;
        dfs(graph,this.s);
    }

    //深度优先搜索
    private void dfs(Graph graph,int v){
        visited[v]=true;
        for(int w:graph.adj(v)){
            if(!visited[w]){
                edgeTo[w]=v; //w--->v的路径
                dfs(graph,w);
            }
        }
    }

    //是否有到顶点v的路径
    private boolean hasPathTo(int v){
        return visited[v];
    }

    //获取从 s->v 的路径
    public Iterable<Integer> pathTo(int v){
        if(!hasPathTo(v)){
            return null;
        }
        Stack<Integer> path=new Stack<>();
        for(int i=v;i!=s;i=edgeTo[i]){
            path.push(i);
        }
        path.push(s);
        return path;
    }

    public void showPath(int v){
        Stack<Integer> path= (Stack<Integer>) pathTo(v);

        StringBuffer sb=new StringBuffer();
        sb.append("[");
        while(!path.isEmpty()){
            int w=path.peek();
            if(path.size()==1){
                sb.append(w);
                path.pop();
            }else{
                sb.append(w+"-->");
                path.pop();
            }
        }
        sb.append("]");
        System.out.println(sb.toString());
    }
}

时间复杂度:

  • 领接表:O(V+E)

  • 邻接矩阵:O(V^2)

统计图的连通分量

/**
 * 图的dfs应用之统计图的连通分量
 */
public class Component {
    private Graph graph;
    //连通分量的个数
    private int num;
    private boolean[] visited;

    public Component(Graph graph){
        this.graph=graph;
        visited=new boolean[graph.V()];
        num=0;
        for(int i=0;i<graph.V();i++){
            if(!visited[i]){
                dfs(i);
                num++;
            }
        }
    }

    private void dfs(int v){
        visited[v]=true;
        for(int w:graph.adj(v)){
            if(!visited[w]){
                dfs(w);
            }
        }
    }

    //获取连通分量个数
    public int getNum(){
        return num;
    }
}

图的广度优先遍历(BFS)

/**
 * 图的广度优先遍历
 */
public class BreadthFirstPaths {
    //标记顶点是否被访问过
    private boolean[] visited;
    //记录路径
    private int[] edgeTo;
    //起点
    private int s;

    public BreadthFirstPaths(Graph graph, int s){
        visited=new boolean[graph.V()];
        edgeTo=new int[graph.V()];
        for(int i=0;i<graph.V();i++){
            edgeTo[i]=-1;
        }
        this.s=s;
        bfs(graph,this.s);
    }

    //广度优先搜索
    private void bfs(Graph graph,int s){
        visited[s]=true;
        Queue<Integer> queue=new LinkedList<>();
        queue.add(s);
        while (!queue.isEmpty()){
            int v=queue.poll();
            for(int w:graph.adj(v)){
                if(!visited[w]){
                    visited[w]=true;
                    edgeTo[w]=v;
                    queue.add(w);
                }
            }
        }
    }

    //是否有到顶点v的路径
    private boolean hasPathTo(int v){
        return visited[v];
    }

    //获取从 s->v 的路径
    public Iterable<Integer> pathTo(int v){
        if(!hasPathTo(v)){
            return null;
        }
        Stack<Integer> path=new Stack<>();
        for(int i=v;i!=s;i=edgeTo[i]){
            path.push(i);
        }
        path.push(s);
        return path;
    }

    public void showPath(int v){
        Stack<Integer> path= (Stack<Integer>) pathTo(v);

        StringBuffer sb=new StringBuffer();
        sb.append("[");
        while(!path.isEmpty()){
            int w=path.peek();
            if(path.size()==1){
                sb.append(w);
                path.pop();
            }else{
                sb.append(w+"-->");
                path.pop();
            }
        }
        sb.append("]");
        System.out.println(sb.toString());
    }
}

无权图的最短路径

public class ShortestPath {
    //标记顶点是否被访问过
    private boolean[] visited;
    //记录路径
    private int[] edgeTo;
    //起点
    private int s;
    //从起点到某个节点的最短路径
    private int[] ord;

    public ShortestPath(Graph graph, int s){
        visited=new boolean[graph.V()];
        edgeTo=new int[graph.V()];
        ord=new int[graph.V()];
        for(int i=0;i<graph.V();i++){
            edgeTo[i]=-1;
        }
        this.s=s;
        bfs(graph,this.s);
    }

    //广度优先搜索
    private void bfs(Graph graph,int s){
        visited[s]=true;
        Queue<Integer> queue=new LinkedList<>();
        queue.add(s);
        while (!queue.isEmpty()){
            int v=queue.poll();
            for(int w:graph.adj(v)){
                if(!visited[w]){
                    visited[w]=true;
                    edgeTo[w]=v;
                    ord[w]=ord[v]+1;
                    queue.add(w);
                }
            }
        }
    }

    //是否有到顶点v的路径
    private boolean hasPathTo(int v){
        return visited[v];
    }

    //获取从 s->v的路径
    public Iterable<Integer> pathTo(int v){
        if(!hasPathTo(v)){
            return null;
        }
        Stack<Integer> path=new Stack<>();
        for(int i=v;i!=s;i=edgeTo[i]){
            path.push(i);
        }
        path.push(s);
        return path;
    }

    public void showPath(int v){
        Stack<Integer> path= (Stack<Integer>) pathTo(v);

        StringBuffer sb=new StringBuffer();
        sb.append("[");
        while(!path.isEmpty()){
            int w=path.peek();
            if(path.size()==1){
                sb.append(w);
                path.pop();
            }else{
                sb.append(w+"-->");
                path.pop();
            }
        }
        sb.append("]");
        System.out.println(sb.toString());
    }

    //s-->v 的最短距离
    public int getLength(int v){
        return ord[v];
    }
}

(二)最小生成树

有向边

public class Edge<E extends Number & Comparable> implements Comparable<Edge>{
    //定义改变的起始节点和终止节点
    private int from;
    private int to;
    //该边的权值
    private E weight;

    public Edge(int v, int w, E weight)
    {
        this.from = v;
        this.to = w;
        this.weight = weight;
    }

    public Edge(Edge<E> e)
    {
        this.from = e.from;
        this.to = e.to;
        this.weight = e.weight;
    }

    //获取起点
    public int v(){
        return from;
    }

    //获取终点
    public int w(){
        return to;
    }

    //获取权值
    public E wt(){
        return weight;
    }

    // 给定一个顶点, 返回另一个顶点
    public int other(int x){
        assert x == from || x == to;
        return x == from ? to : from;
    }

    //输出边的具体信息
    @Override
    public String toString() {
        return "" + from + "-" + to + ": " + weight;
    }

    @Override
    public int compareTo(Edge that) {
        int num=weight.compareTo(that.wt());
        return num;
    }
}

带权图

// 带权图的接口
public interface WeightedGraph<E extends Number & Comparable> {
    public int V();
    public int E();
    public void addEdge(Edge<E> edge);
    boolean hasEdge(int v, int w);
    void show();
    public Iterable<Edge<E>> adj(int v);
}

稠密图(带权图)

public class DenseWeightedGraph<E extends Number & Comparable> implements WeightedGraph<E>{

    private int n;  // 节点数
    private int m;  // 边数
    private boolean directed;   // 是否为有向图
    private Edge[][] g;      // 图的具体数据

    // 构造函数
    public DenseWeightedGraph(int n , boolean directed ){
        assert n >= 0;
        this.n = n;
        this.m = 0;    // 初始化没有任何边
        //false表示无向图
        this.directed = directed;
        // g初始化为n*n的有向边矩阵
        g = new Edge[n][n];
    }

    @Override
    public int V(){ return n;} // 返回节点个数

    @Override
    public int E(){ return m;} // 返回边的个数

    // 向图中添加一个边
    @Override
    public void addEdge(Edge edge){
        assert edge.v() >= 0 && edge.v() < n ;
        assert edge.w() >= 0 && edge.w() < n ;

        if( hasEdge( edge.v() , edge.w() ) ){
            return;
        }
        g[edge.v()][edge.w()] = new Edge(edge);
        if( edge.v() != edge.w() && !directed ){
            g[edge.w()][edge.v()] = new Edge(edge.w(), edge.v(), edge.wt());
        }
        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 show() {
        for(int i=0;i<n;i++){
            for(int j=0;j<n;j++){
                if(g[i][j]!=null){
                    System.out.print(g[i][j].wt()+"\t");
                }else{
                    System.out.print("NULL\t");
                }
            }
            System.out.println();
        }
    }

    // 返回图中一个顶点的所有邻边
    // 由于java使用引用机制,返回一个Vector不会带来额外开销
    @Override
    public Iterable<Edge<E>> adj(int v) {
        assert v >= 0 && v < n;
        Vector<Edge<E>> adjV = new Vector<>();
        for (int i = 0; i < n; i++) {
            if (g[v][i]!=null) {
                adjV.add(g[v][i]);
            }
        }
        return adjV;
    }
}

稀疏图(带权图)

public class SparseWeightedGraph<E extends Number & Comparable> implements WeightedGraph<E>{

    private int n;  // 节点数
    private int m;  // 边数
    private boolean directed;    // 是否为有向图
    private Vector<Edge<E>>[] g; // 图的具体数据

    // 构造函数
    public SparseWeightedGraph(int n , boolean directed ){
        assert n >= 0;
        this.n = n;
        this.m = 0;    // 初始化没有任何边
        this.directed = directed;
        // g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
        g = (Vector<Edge<E>>[])new Vector[n];
        for(int i = 0 ; i < n ; i ++){
            g[i] = new Vector<Edge<E>>();
        }
    }

    @Override
    public int V(){ return n;} // 返回节点个数

    @Override
    public int E(){ return m;} // 返回边的个数

    // 向图中添加一个边
    @Override
    public void addEdge(Edge edge){

        assert edge.v() >= 0 && edge.v() < n ;
        assert edge.w() >= 0 && edge.w() < n ;

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

    // 验证图中是否有从v到w的边 -->时间复杂度:O(n)
    @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).other(v) == w ){
                return true;
            }
        }
        return false;
    }

    @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].elementAt(j);
                System.out.print( "( to:" + e.other(i) + ",wt:" + e.wt() + ")\t");
            }
            System.out.println();
        }
    }

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

最小生成树

最小生成树问题针对的是带权无向图,并且该图是一个连通图

切分定理

1.切分

把图中的的顶点分成两部分,成为一个切分

其中图中顶点被分为蓝色部分和红色部分,这就是一个切分。

2.横切边

如果一个边的两个端点,分别属于不同的切分,则这个边就被成为横切边

其中使用绿色标出了横切边。

3.切分定理

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

在这个切分中横切边中权值最小的是0.16,已使用红色标出。

在这个切分中横切边中权值最小的是0.4,已使用红色标出。

Lazy Prim

public class LazyPrimMST<E extends Number & Comparable> {
    private WeightedGraph<E> G;    // 图的引用
    private PriorityQueue<Edge<E>> pq;   // 最小堆, -->Java默认使用小根堆
    private boolean[] marked;           // 标记数组, 在算法运行过程中标记节点i是否被访问
    private Vector<Edge<E>> mst;   // 最小生成树所包含的所有边
    private Number mstWeight;           // 最小生成树的权值

    public LazyPrimMST(WeightedGraph graph){
        this.G=graph;
        pq=new PriorityQueue<>();
        marked=new boolean[graph.V()];
        mst=new Vector<>();

        //从0节点开始访问
        visit(0);
        while(!pq.isEmpty()){
            //获取最小边
            Edge<E> e=pq.poll();
            //看看这条最小边是否是横切边
            if(marked[e.v()]==marked[e.w()]){
                continue;
            }
            //是横切边,说明就是MST的边
            mst.add(e);

            // 访问和这条边连接的还没有被访问过的节点
            if(! marked[e.v()]){
                visit(e.v());
            }else{
                visit(e.w());
            }

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

    //v顶点是未访问过的
    private void visit(int v){
        assert !marked[v];
        marked[v]=true;
        //将与v节点相连的顶点边加入到堆中
        for(Edge e:G.adj(v)){
            if(!marked[e.other(v)]){
                pq.add(e);
            }
        }
    }

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

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

Prim 算法的优化

/**
 * 优化的Prim算法求图的最小生成树
 */
public class PrimMST<E extends Number & Comparable> {
    private WeightedGraph G;              // 图的引用
    private IndexMinHeap<E> ipq;     // 最小索引堆, 算法辅助数据结构
    private Edge<E>[] edgeTo;        // 访问的点所对应的边, 算法辅助数据结构
    private boolean[] marked;             // 标记数组, 在算法运行过程中标记节点i是否被访问
    private Vector<Edge<E>> mst;     // 最小生成树所包含的所有边
    private Number mstWeight;             // 最小生成树的权值

    public PrimMST(WeightedGraph graph){
        G = graph;
        assert( graph.E() >= 1 );
        ipq = new IndexMinHeap<E>(graph.V());

        // 算法初始化
        marked = new boolean[G.V()];
        edgeTo = new Edge[G.V()];
        mst=new Vector<>();

        //Lazy Prim算法的改进
        visit(0);
        while (!ipq.isEmpty()){
            // 使用最小索引堆找出已经访问的边中权值最小的边
            // 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边
            int v=ipq.extractMinIndex();
            assert( edgeTo[v] != null );
            mst.add(edgeTo[v]);
            visit(v);
        }

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

    private void visit(int v){
        assert !marked[v];
        marked[v] = true;

        // 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
        for(Object edge:G.adj(v)){
            Edge<E> e=(Edge<E>)edge;
            int w=e.other(v);
            // 如果边的另一端点未被访问
            if(!marked[w]){
                if(edgeTo[w]==null){
                    //如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆
                    //edgeTo[w] 表示访问w定点所对应的边<v,w>
                    edgeTo[w]=e;
                    ipq.insert(w,e.wt());
                }else if(e.wt().compareTo(edgeTo[w].wt())<0){
                    //如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换
                    edgeTo[w]=e;
                    ipq.change(w,e.wt());
                }
            }
        }
    }

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

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

Kruskal

public class KruskalMST<E extends Number & Comparable> {
    private Vector<Edge<E>> mst;   // 最小生成树所包含的所有边
    private Number mstWeight;           // 最小生成树的权值

    public KruskalMST(WeightedGraph graph){
        mst=new Vector<>();

        // 将图中的所有边存放到一个最小堆中
        PriorityQueue<Edge<E>> pq=new PriorityQueue<>(graph.E());
        for(int i=0;i<graph.V();i++){
           for(Object item:graph.adj(i)){
               Edge<E> e=(Edge<E>)item;
               if(e.w() <e.v()){ //由于是无向图,只要加入一条边就好了
                   pq.add(e);
               }
           }
        }

        //创建一个并查集, 来查看已经访问的节点的联通情况
        UnionFind uf = new UnionFind(graph.V());
        while (!pq.isEmpty() && mst.size()<graph.V()-1){
            // 从最小堆中依次从小到大取出所有的边
            Edge<E> e = pq.poll();

            //如果该边的两个端点是联通的, 说明加入这条边将产生环, 扔掉这条边
            if(uf.isConnected(e.w(),e.v())){
                continue;
            }

            // 否则, 将这条边添加进最小生成树, 同时标记边的两个端点联通
            mst.add(e);
            uf.unionElements(e.w(),e.v());
        }

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

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

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

时间复杂度

算法 时间复杂度
Lazy Prim O(ElogE)
Prim O(ElogV)
Kruskal O(ElogE)

(三)最短路径

Dijkstra 单源最短路径算法

前提:图中不能有负权边

图中的蓝色顶点表示已加入最短路径树。红色的边就是最短路径。

图的右方左部分是索引堆,右部分是distTo[v] (表示从源点到v的已知最短路径长度)。

Dijkstra算法思路 :

将0顶点开始,将访问顶点1、2、3

到没有访问过的顶点的最短路径是2,也就是顶点2

从2开始,将访问1、3、4。

先访问顶点 1 ,则0->2->1的路径就为3,此时不需要考虑0->1权为5的边了。

访问 顶点 4 ,则 0->2->4的路径长为7。

访问 顶点 3,则 0->2->3的路径长为5,此时就不需要考虑0->3权为6的边了。

此时所能到达的最短路径是顶点1。

考虑顶点1的邻边,1->4的路径更小。

此时所能到达的最短路径是顶点4。

此时所能到达的最短路径是顶点3.

public class DijkstraSP<E extends Number & Comparable>{
    private WeightedGraph G;
    // 图的引用
    private int s;
    // 起始点
    private Number[] distTo;
    // distTo[i]存储从起始点s到顶点i的最短路径长度
    private boolean[] marked;
    // 标记数组, 在算法运行过程中标记节点i是否被访问
    private Edge<E>[] from;
    // from[i]记录最短路径中, 到达i点的边是哪一条
    // 可以用来恢复整个最短路径

    public DijkstraSP(WeightedGraph graph,int s){
        this.G=graph;
        this.s=s;
        distTo=new Number[G.V()];
        marked=new boolean[G.V()];
        from=new Edge[G.V()];

        //使用索引堆记录当前找到的到达每个顶点的最短距离 ---> Number类型
        IndexMinHeap<E> indexMinHeap=new IndexMinHeap<>(G.V());
        //初始化起始点
        distTo[s] = 0.0;
        from[s]=new Edge<E>(s,s,(E)((Number)0.0));
        marked[s]=true;

        indexMinHeap.insert(s,(E)distTo[s]);
        while (!indexMinHeap.isEmpty()){
            int v=indexMinHeap.extractMinIndex();

            // distTo[v]就是s到v的最短距离
            marked[v] = true;

            for(Object item:G.adj(v)){
                Edge<E> edge=(Edge<E>)item;
                int w=edge.other(v);
                //如果从s点到w点的最短路径还没有找到
                if(!marked[w]){
                    // 如果w点以前没有访问过,
                    // 或者访问过, 但是通过当前的v点到w点距离更短, 则进行更新
                    if(from[w]==null ||
                            (distTo[v].doubleValue()+edge.wt().doubleValue() < distTo[w].doubleValue())){
                        distTo[w]=distTo[v].doubleValue()+edge.wt().doubleValue();
                        from[w]=edge;
                        if(indexMinHeap.contain(w)){
                            indexMinHeap.change(w,(E)distTo[w]);
                        }else{
                            indexMinHeap.insert(w,(E)distTo[w]);
                        }
                    }
                }
            }
        }
    }

    //获取从s点到w点的最短路径长度
    public Number shortestPathTo(int w){
        assert hasPathTo(w);
        return distTo[w];
    }

    // 判断从s点到w点是否联通
    public boolean hasPathTo(int w){
        assert w>=0 && w<G.V();
        return marked[w];
    }

    //寻找从s到w的最短路径, 将整个路径经过的边存放在res中
    public Vector<Edge<E>> shortestPath(int w){
        assert hasPathTo(w);

        //通过from数组逆向查找到从s到w的路径, 存放到栈中
        Stack<Edge<E>> stack=new Stack<>();
        Edge<E> e=from[w];
        while (e.v()!=s){
            stack.push(e);
            e=from[e.v()];
        }
        //最后e.v()就是s,那么e这条边入栈
        stack.push(e);

        //从栈中依次取出元素, 获得顺序的从s到w的路径
        Vector<Edge<E>> res=new Vector<>();
        while (!stack.isEmpty()){
            Edge<E> edge=stack.pop();
            res.add(edge);
        }
        return res;
    }


    // 打印出从s点到w点的路径
    public void showPath(int w){
        assert hasPathTo(w);

        Vector<Edge<E>> path =  shortestPath(w);
        for( int i = 0 ; i < path.size() ; i ++ ){
            System.out.print( path.elementAt(i).v() + " -> ");
            if( i == path.size()-1 ){
                System.out.println(path.elementAt(i).w());
            }
        }
    }
}

Bellman-Ford 单源最短路径算法

1. 负权边问题

顶点0、1、2构成了一个负权环。

显然,有负权环的图,没有最短路径。

如果一个图没有负权环,从一点到另外一点的最短路径, 最多经过所有的V个顶点,有(V-1)条边。 否则,存在顶点经过了两次,也就是说存在负权环。

2. 思路

  • 对一个顶点的一次松弛操作,就是找到经过这个点的另外一条路径,多一条边,权值更小。
  • 如果一个图没有负权环,从一个顶点到另外一个顶点的最短路径,最多经过所有的V个顶点,有(V-1)条边。
  • 对所有的点进行(V-1)次松弛操作,理论上可以找到到其他所有点的最短路径
  • 如果还可以继续松弛,说明原图中有负权环。

3. 实践

public BellmanFordSP(WeightedGraph graph,int s){
    this.G=graph;
    this.s=s;
    distTo=new Number[G.V()];
    marked=new boolean[G.V()];
    from=new Edge[G.V()];

    // // 设置distTo[s] = 0, 并且让from[s]不为NULL, 表示初始s节点可达且距离为0
    distTo[s] = 0.0;
    from[s] = new Edge<E>(s, s, (E)(Number)(0.0));

    // Bellman-Ford的过程
    // 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
    for(int pass=1;pass<G.V();pass++){
        // 每次循环中对所有的边进行一遍松弛操作
        // 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
        for(int i=0;i<G.V();i++){
            for(Object item:G.adj(i)){
                Edge<E> e=(Edge<E>)item;
                // 对于每一个边首先判断e->v()可达
                // 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
                // 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离,
                // 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
                if(from[e.v()]!=null &&
                        (from[e.w()]==null || distTo[e.v()].doubleValue()+e.wt().doubleValue()<distTo[e.w()].doubleValue())){
                    distTo[e.w()]=distTo[e.v()].doubleValue()+e.wt().doubleValue();
                    from[e.w()]=e;
                }
            }
        }
    }
}

4. 查看图中是否有负权环

public class BellmanFordSP<E extends Number & Comparable> {
    private WeightedGraph G;
    // 图的引用
    private int s;
    // 起始点
    private Number[] distTo;
    // distTo[i]存储从起始点s到顶点i的最短路径长度
    private boolean[] marked;
    // 标记数组, 在算法运行过程中标记节点i是否被访问
    private Edge<E>[] from;
    // from[i]记录最短路径中, 到达i点的边是哪一条
    // 可以用来恢复整个最短路径

    private boolean hasNegativeCycle;
    // 标记图中是否有负权环

    public BellmanFordSP(WeightedGraph graph,int s){
        this.G=graph;
        this.s=s;
        distTo=new Number[G.V()];
        marked=new boolean[G.V()];
        from=new Edge[G.V()];

        // // 设置distTo[s] = 0, 并且让from[s]不为NULL, 表示初始s节点可达且距离为0
        distTo[s] = 0.0;
        from[s] = new Edge<E>(s, s, (E)(Number)(0.0));

        // Bellman-Ford的过程
        // 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
        for(int pass=1;pass<G.V();pass++){
            // 每次循环中对所有的边进行一遍松弛操作
            // 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
            for(int i=0;i<G.V();i++){
                for(Object item:G.adj(i)){
                    Edge<E> e=(Edge<E>)item;
                    // 对于每一个边首先判断e->v()可达
                    // 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
                    // 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离,
                    // 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
                    if(from[e.v()]!=null &&
                            (from[e.w()]==null || distTo[e.v()].doubleValue()+e.wt().doubleValue()<distTo[e.w()].doubleValue())){
                        distTo[e.w()]=distTo[e.v()].doubleValue()+e.wt().doubleValue();
                        from[e.w()]=e;
                    }
                }
            }
        }

        hasNegativeCycle = detectNegativeCycle();
    }

    // 判断图中是否有负权环
    boolean detectNegativeCycle(){

        for( int i = 0 ; i < G.V() ; i ++ ){
            for( Object item : G.adj(i) ){
                Edge<E> e = (Edge<E>)item;
                if( from[e.v()] != null && distTo[e.v()].doubleValue() + e.wt().doubleValue() < distTo[e.w()].doubleValue() ){
                    return true;
                }

            }
        }
        return false;
    }
    // 返回图中是否有负权环
    boolean negativeCycle(){
        return hasNegativeCycle;
    }

    // 返回从s点到w点的最短路径长度
    Number shortestPathTo( int w ){
        assert w >= 0 && w < G.V();
        assert !hasNegativeCycle;
        assert hasPathTo(w);
        return distTo[w];
    }

    // 判断从s点到w点是否联通
    boolean hasPathTo( int w ){
        assert( w >= 0 && w < G.V() );
        return from[w] != null;
    }

    // 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
    Vector<Edge<E>> shortestPath(int w){

        assert w >= 0 && w < G.V() ;
        assert !hasNegativeCycle ;
        assert hasPathTo(w) ;

        // 通过from数组逆向查找到从s到w的路径, 存放到栈中
        Stack<Edge<E>> s = new Stack<Edge<E>>();
        Edge<E> e = from[w];
        while( e.v() != this.s ){
            s.push(e);
            e = from[e.v()];
        }
        s.push(e);

        // 从栈中依次取出元素, 获得顺序的从s到w的路径
        Vector<Edge<E>> res = new Vector<Edge<E>>();
        while( !s.empty() ){
            e = s.pop();
            res.add(e);
        }

        return res;
    }

    // 打印出从s点到w点的路径
    void showPath(int w){

        assert( w >= 0 && w < G.V() );
        assert( !hasNegativeCycle );
        assert( hasPathTo(w) );

        Vector<Edge<E>> res = shortestPath(w);
        for( int i = 0 ; i < res.size() ; i ++ ){
            System.out.print(res.elementAt(i).v() + " -> ");
            if( i == res.size()-1 ){
                System.out.println(res.elementAt(i).w());
            }
        }
    }
}

(四)拓扑排序

拓扑排序,是指将有向无环图的所有顶点以线性的方式进行排序。使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。 通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。

例如下图所示,其拓扑序列的一种为:(5, 4, 0, 1, 2, 3)

基于Kahn 算法的拓扑排序

思路

每次从该集合中取出(没有特殊的取出规则,随机取出也行,使用队列/栈也行,下同)一个顶点,将该顶点放入保存结果的序列中。

紧接着循环遍历由该顶点引出的所有边,从图中移除这条边,同时获取该边的另外一个顶点,如果该顶点的入度在减去本条边之后为0,那么也将这个顶点放到入度为0的集合中。然后继续从集合中取出一个顶点,直到该集合为空。

当集合为空之后,检查图中是否还存在任何边,如果存在的话,说明图中至少存在一条环路。不存在的话则返回结果序列,此时就是对图进行拓扑排序的结果。

实现

public class KahnTopologicalSort {

    //拓扑序列
    private List<Integer> order;
    //入度为0的集合
    private Queue<Integer> setOfZeroIndegree;
    //存储每个节点的入度
    private int[] indegrees;
    private int edges;
    private Digraph graph;

    public KahnTopologicalSort(Digraph graph) {
        this.graph = graph;
        this.edges = graph.E();
        this.indegrees = new int[graph.V()];
        order = new ArrayList<>();
        setOfZeroIndegree = new LinkedList<>();

        //对入度为0的集合进行初始化
        Vector<Integer>[] g = graph.getG();
        //将邻接表中的每一条[a->b]中的b添加入度
        for (int i = 0; i < g.length; i++) {
            for (int j:g[i]){
                indegrees[j]++;
            }
        }
        for (int i = 0; i < indegrees.length; i++) {
            if (indegrees[i]==0){
                setOfZeroIndegree.offer(i);
            }
        }
        generateOrder();
    }

    //生成拓扑序列
    private void generateOrder(){
        while (!setOfZeroIndegree.isEmpty()){
            //从入度为0的集合中取出一个节点
            Integer curNode = setOfZeroIndegree.poll();
            order.add(curNode);
            //遍历该节点的邻居节点
            for (int i:graph.adj(curNode)){
                edges--;
                indegrees[i]--;
                if (indegrees[i]==0){
                    setOfZeroIndegree.offer(i);
                }
            }
        }
        //说明没有遍历完所有节点,证明有环
        if (edges!=0){
            order=new ArrayList<>();
        }
    }

    public List<Integer> getOrder(){
        return order;
    }
}

基于 DFS 的拓扑排序

思路

使用DFS(深度优先遍历),需要使用到栈,用来存在遍历过的节点,因为越早遍历到的节点其前序依赖越少,作为拓扑序列其位置应该越靠前。对于任何先序关系:v->w,DFS可以保证 w 先进入栈中,因此栈的逆序结果中 v 会在 w 之前。

实现

public class DfsTopologicalSort {

    //拓扑序列
    private List<Integer> order;

    //入队序列需要逆序才是拓扑序列
    private Stack<Integer> stack;

    //标记各节点的状态
    // -1:visited;0:none-visited;1:visiting
    private int[] flag;

    private Digraph graph;

    public DfsTopologicalSort(Digraph graph) {
        this.graph = graph;
        order=new ArrayList<>();
        stack=new Stack<>();
        flag=new int[graph.V()];

        //生成拓扑序列
        for (int i = 0; i < graph.V(); i++) {
            if (dfs(graph,i)){
                order.clear();
            }
        }
        while (!stack.empty()){
            order.add(stack.pop());
        }
    }

    /**
     * 深度优先遍历
     * @param graph
     * @param curNode
     * @return 是否存在环路
     */
    private boolean dfs(Digraph graph,int curNode){
        //此时说明存在环路
        if (flag[curNode]==1){
            return true;
        }
        //当前的节点已被遍历过
        if (flag[curNode]==-1){
            return false;
        }
        flag[curNode]=1;
        //继续遍历当前节点的邻居节点
        for (int i:graph.adj(curNode)){
            if (dfs(graph,i)){
                return true;
            }
        }
        stack.push(curNode);
        flag[curNode]=-1;
        return false;
    }

    public List<Integer> getOrder(){
        return order;
    }
}

对比

Kahn算法不需要检测图为DAG,如果图为DAG,那么在出度为0的集合为空之后,图中还存在没有被移除的边,这就说明了图中存在环路。而基于DFS的算法需要首先确定图为DAG,当然也能够做出适当调整,让环路的检测和拓扑排序同时进行,毕竟环路检测也能够在DFS的基础上进行。