数据结构 -- 加权无向图

一、概述

加权无向图是一种为每条边关联一个权重值或是成本的图模型。

二、邻接矩阵实现(todo)

三、邻接表实现

3.1 加权无向图边

加权无向图中的边我们就不能简单的使用v-w两个顶点表示了,而必须要给边关联一个权重值,因此我们可以使用对象来描述一条边。

3.1.1 API设计

类名 Digraph
构造方法 Digraph(int V):创建一个包含V个顶点但不包含边的有向图
成员方法 1.public int V():获取图中顶点的数量
2.public int E():获取图中边的数量
3.public void addEdge(int v,int w):向有向图中添加一条边 v->w
4.public Queue adj(int v):获取由v指出的边所连接的所有顶点
5.private Digraph reverse():该图的反向图
成员变量 1.private final int V: 记录顶点数量
2.private int E: 记录边数量
3.private Queue[] adj: 邻接表

3.1.2 实现

/**
 * 加权无向图的边
 * @date 2021/7/13 16:49
 */
public class Edge implements Comparable<Edge> {
    // 顶点一
    private final int v;
    // 顶点二
    private final int w;
    // 当前边的权重
    private final double weight;

    //通过顶点v和w,以及权重weight值构造一个边对象
    public Edge(int v, int w, double weight) {
        this.v = v;
        this.w = w;
        this.weight = weight;
    }

    // 获取边的权重值
    public double weight() {
        return weight;
    }

    // 获取边上的一个点
    public int either() {
        return v;
    }

    // 获取边上除了顶点vertex外的另一个顶点
    public int other(int vertex) {
        if (vertex == v) {
            //如果传入的顶点vertext是v,则返回另外一个顶点w
            return w;
        } else {
            //如果传入的顶点vertext不是v,则返回v即可
            return v;
        }
    }

    @Override
    public int compareTo(Edge that) {
        int cmp;
        if (this.weight() > that.weight()) {
            //如果当前边的权重大于参数边that的权重,返回1
            cmp = 1;
        } else if (this.weight() < that.weight()) {
            //如果当前边的权重小于参数边that的权重,返回-1
            cmp = -1;
        } else {
            //如果当前边的权重等于参数边that的权重,返回0
            cmp = 0;
        }
        return cmp;
    }
}

3.2 加权无向图

3.2.1 API设计

类名 EdgeWeightedGraph
构造方法 EdgeWeightedGraph(int V):创建一个含有V个顶点的空加权无向图
成员方法 1.public int V():获取图中顶点的数量
2.public int E():获取图中边的数量
3.public void addEdge(Edge e):向加权无向图中添加一条边e
4.public Queue adj(int v):获取和顶点v关联的所有边
5.public Queue edges():获取加权无向图的所有边
成员变量 1.private final int V: 记录顶点数量
2.private int E: 记录边数量
3.private Queue[] adj: 邻接表

3.2.2 实现

/**
 * 加权无向图
 *
 * @date 2021/7/13 18:48
 */
public class EdgeWeightedGraph {
    // 顶点总数
    private final int V;

    // 边的总数
    private int E;

    // 邻接表
    private Queue<Edge>[] adj;

    // 创建一个含有V个顶点的空加权无向图
    public EdgeWeightedGraph(int V) {
        this.V = V;
        this.E = 0;
        this.adj = new Queue[V];
        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<Edge>();
        }
    }

    // 获取图中顶点的数量
    public int V() {
        return V;
    }

    // 获取图中边的数量
    public int E() {
        return E;
    }

    // 向加权无向图中添加一条边e
    public void addEdge(Edge e) {
        // 获取边中的一个顶点
        int v = e.either();

        // 获取边中的另一个顶点w
        int w = e.other(v);

        // 因为是无向图,所以边e需要同时出现在两个顶点的邻接表中
        adj[v].enqueue(e);
        adj[w].enqueue(e);

        // 边的数量+1
        E++;
    }

    // 获取和顶点v关联的所有边
    public Queue<Edge> adj(int v) {
        return adj[v];
    }

    // 获取加权无向图的所有边
    public Queue<Edge> edges() {
        Queue<Edge> allEdge = new Queue<>();
        for (int v = 0; v < this.V; v++) {
            for (Edge e : adj(v)) {
                if (e.other(v) < v) {
                    allEdge.enqueue(e);
                }
            }
        }
        return allEdge;
    }
}

3.2.3 测试

/**
 * @date 2021/7/13 21:01
 */
public class EdgeWeightedGraphTest {
    public static void main(String[] args) {
        EdgeWeightedGraph edgeWeightedGraph = new EdgeWeightedGraph(10);
        Edge edge01 = new Edge(1, 2, 1);
        Edge edge02 = new Edge(2, 3, 1);
        Edge edge03 = new Edge(3, 4, 1);
        edgeWeightedGraph.addEdge(edge01);
        edgeWeightedGraph.addEdge(edge02);
        edgeWeightedGraph.addEdge(edge03);

        Queue<Edge> adj = edgeWeightedGraph.adj(2);
        System.out.print("顶点2关联的所有边:");
        for (Edge edge : adj) {
            System.out.print(edge.toString()+" ");
        }
        System.out.println();

        Queue<Edge> edges = edgeWeightedGraph.edges();
        System.out.print("所有边:");
        for (Edge edge : edges) {
            System.out.print(edge.toString()+" ");
        }
        System.out.println();

        System.out.println("顶点的个数:" + edgeWeightedGraph.V());
        System.out.println("边的个数:" + edgeWeightedGraph.E());
    }
}

四、最小生成树

4.1 概述

(1)之前学习的加权图,我们发现它的边关联了一个权重,那么我们就可以根据这个权重解决最小成本问题,但如何才能找到最小成本对应的顶点和边呢?最小生成树相关算法可以解决。
(2)图的生成树是它的一棵含有其所有顶点的无环连通子图,一副加权无向图的最小生成树它的一棵权值(树中所有边的权重之和)最小的生成树

4.2 切分定理

要从一副连通图中找出该图的最小生成树,需要通过切分定理完成。
切分: 将图的所有顶点按照某些规则分为两个非空且没有交集的集合。
横切边: 连接两个属于不同集合的顶点的边称之为横切边。
切分定理: 在一副加权图中,给定任意的切分,它的横切边中的权重最小者必然属于图中的最小生成树。
注意: :一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图的最小生成树的边。

五、贪心算法

5.1 定义

贪心算法或贪心思想采用贪心的策略,保证每次操作都是局部最优解的,从而使最后得到的结果是全局最优解。贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关。)所以,对所采用的贪心策略一定要仔细分析其是否满足无后效性。

5.2 解题步骤

1)将问题分为若干子问题
2)找到合适的贪心策略
3)求解每一个子集的最优解
4)将局部最优解迭代成全局最优解

六、Prim算法

普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。

6.0 贪心策略

切分定理

6.1 API设计

类名 PrimMST
构造方法 PrimMST(EdgeWeightedGraph G):根据一副加权无向图,创建最小生成树计算对象;
成员方法 1.private void visit(EdgeWeightedGraph G, int v):将顶点v添加到最小生成树中,并且更新数据
2.public Queue edges():获取最小生成树的所有边
成员变量 1.private Edge[] edgeTo: 索引代表顶点,值表示当前顶点和最小生成树之间的最短边
2.private double[] distTo: 索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重
3.private boolean[] marked:索引代表顶点,如果当前顶点已经在树中,则值为true,否则为false
4.private IndexMinPriorityQueue pq:存放树中顶点与非树中顶点之间的有效横切边

6.2 实现

public class PrimMST {
    // 索引代表顶点,值代表当前顶点和最小生成树之间的最短边
    private Edge[] edgeTo;
    // 索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重
    private double[] distTo;
    // 索引代表顶点,如果当前顶点已经在树中,则值为true,否则为false
    private boolean[] marked;
    // 存放树中顶点与非树中顶点之间的有效横切边
    private IndexMinPriorityQueue<Double> pq;

    // 根据一副加权无向图,创建最小生成树计算对象
    public PrimMST(EdgeWeightedGraph G) {
        // 创建一个和图的顶点数一样大小的Edge数组,表示边
        this.edgeTo = new Edge[G.V()];
        // 创建一个和图的顶点数一样大小的double数组,表示权重,并且初始化数组中的内容为无穷大,无穷大表示不存在这样的边
        this.distTo = new double[G.V()];
        for (int i = 0; i < distTo.length; i++) {
            distTo[i] = Double.POSITIVE_INFINITY;
        }
        // 创建一个和图的顶点数一样大小的boolean数组,表示当前顶点是否已经在树中
        this.marked = new boolean[G.V()];
        // 创建一个和图的顶点数一样大小的索引优先队列,存储有效横切边
        this.pq = new IndexMinPriorityQueue<>(G.V());
        // 默认让顶点0进入树中,但0顶点目前没有与树中其他的顶点相连接,因此初始化distTo[0]=0.0
        distTo[0] = 0.0;
        // 使用顶点0和权重0初始化pq
        pq.insert(0, 0.0);
        // 遍历有效边队列
        while (!pq.isEmpty()) {
            // 找到权重最小的横切边对应的顶点,加入到最小生成树中
            visit(G, pq.delMin());
        }
    }

    // 将顶点v添加到最小生成树中,并且更新数据
    private void visit(EdgeWeightedGraph G, int v) {
        // 把顶点v添加到树中
        marked[v] = true;
        // 遍历顶点v的邻接表,得到每一条边Edge e,
        for (Edge e : G.adj(v)) {
            // 边e的一个顶点是v,找到另外一个顶点w;
            int w = e.other(v);
            //检测是否已经在树中,如果在,则继续下一次循环,如果不在,则需要修正当前顶点w距离最小生成树的最小边edgeTo[w]以及它的权重distTo[w],还有有效横切边也需要修正
            if (marked[w]) {
                continue;
            }
            // 如果v-w边e的权重比目前distTo[w]权重小,则需要修正数据
            if (e.weight() < distTo[w]) {
                // 把顶点w距离最小生成树的边修改为e
                edgeTo[w] = e;
                // 把顶点w距离最小生成树的边的权重修改为e.weight()
                distTo[w] = e.weight();
                // 如果pq中存储的有效横切边已经包含了w顶点,则需要修正最小索引优先队列w索引关联的权 重值
                if (pq.contains(w)) {
                    pq.changeItem(w, e.weight());
                } else {
                    //如果pq中存储的有效横切边不包含w顶点,则需要向最小索引优先队列中添加v-w和其 权重值
                    pq.insert(w, e.weight());
                }
            }
        }
    }

    // 获取最小生成树的所有边
    public Queue<Edge> edges() {
        // 创建队列
        Queue<Edge> edges = new Queue<>();
        // 遍历edgeTo数组,找到每一条边,添加到队列中
        for (int i = 0; i < marked.length; i++) {
            if (edgeTo[i] != null) {
                edges.enqueue(edgeTo[i]);
            }
        }
        return edges;
    }

}

6.3 测试

public class PrimMSTTest {
    public static void main(String[] args) {
        // 创建加权无向图
        EdgeWeightedGraph edgeWeightedGraph = new EdgeWeightedGraph(10);
        Edge edge00 = new Edge(0, 2, 5);
        Edge edge01 = new Edge(1, 2, 1);
        Edge edge02 = new Edge(2, 3, 2);
        Edge edge03 = new Edge(3, 4, 4);
        Edge edge04 = new Edge(1, 4, 6);
        edgeWeightedGraph.addEdge(edge00);
        edgeWeightedGraph.addEdge(edge04);
        edgeWeightedGraph.addEdge(edge01);
        edgeWeightedGraph.addEdge(edge02);
        edgeWeightedGraph.addEdge(edge03);

        // 构建PrimMST对象
        PrimMST primMST = new PrimMST(edgeWeightedGraph);
        // 获取最小生成树的边
        Queue<Edge> edges = primMST.edges();
        // 打印输出
        for (Edge edge : edges) {
            if(edge!=null){
                System.out.println(edge.either() + "-" + edge.other(edge.either()) + "::" + edge.weight());
            }
        }
    }
}

七、kruskal算法

7.0 贪心策略

切分定理

7.1 API设计

类名 KruskalMST
构造方法 KruskalMST(EdgeWeightedGraph G):根据一副加权无向图,创建最小生成树计算对象;
成员方法 1.public Queue edges():获取最小生成树的所有边
成员变量 1.private Queue mst:保存最小生成树的所有边
2.private UF_Tree_Weighted uf: 索引代表顶点,使用uf.connect(v,w)可以判断顶点v和顶点w是否在同一颗树中,使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并
3.private MinPriorityQueue pq: 存储图中所有的边,使用最小优先队列,对边按照权重进行排序

7.2 实现

public class KruskalMST {
    // 保存最小生成树的所有边
    private Queue<Edge> mst;
    // 索引代表顶点,使用uf.connect(v.w)可以判断顶点v和顶点w是否在同一颗树中,使用uf.union(v,w)可以把顶点v所在的树和顶点w所在的树合并
    private UF_Tree_Weighted uf;
    // 存储图中所有的边,使用最小优先队列,对边按照权重进行排序
    private MinPriorityQueue<Edge> pq;

    // 根据一副甲醛无向图,创建最小生成树计算对象

    public KruskalMST(EdgeWeightedGraph G) {
        // 初始化mst
        this.mst = new Queue<Edge>();
        // 初始化uf
        this.uf = new UF_Tree_Weighted(G.V());
        // 初始化pq
        this.pq = new MinPriorityQueue<>(G.E()+1);
        // 把图中所有的边存储到pq中
        for (Edge edge : G.edges()) {
            pq.insert(edge);
        }

        // 遍历pq队列,拿到最小权重的边,进行处理
        while (!pq.isEmpty() && mst.size()<G.V()-1){
            // 找到权重最小的边
            Edge e = pq.delMin();
            // 找到该边的两个顶点
            int v = e.either();
            int w = e.other(v);
            // 判断这两个顶点是否已经在同一颗树中,如果在同一颗树中,则不对该边做处理,如果不在同一颗树中,则让这两个顶点属于的两颗树合并成一棵树
            if (uf.connected(v,w)){
                continue;
            }
            uf.union(v,w);
            // 让边e进入到mst队列中
            mst.enqueue(e);
        }
    }

    // 获取最小生成树的所有边
    public Queue<Edge> edges(){
        return mst;
    }
}

7.3 测试

/**
 * @date 2021/7/15 14:42
 */
public class KruskalMSTTest {
    public static void main(String[] args) {
        // 创建加权无向图
        EdgeWeightedGraph edgeWeightedGraph = new EdgeWeightedGraph(10);
        Edge edge00 = new Edge(0, 2, 5);
        Edge edge01 = new Edge(1, 2, 1);
        Edge edge02 = new Edge(2, 3, 2);
        Edge edge03 = new Edge(3, 4, 4);
        Edge edge04 = new Edge(1, 4, 6);
        edgeWeightedGraph.addEdge(edge00);
        edgeWeightedGraph.addEdge(edge04);
        edgeWeightedGraph.addEdge(edge01);
        edgeWeightedGraph.addEdge(edge02);
        edgeWeightedGraph.addEdge(edge03);

        // 构建PrimMST对象
        KruskalMST kruskalMST = new KruskalMST(edgeWeightedGraph);
        // 获取最小生成树的边
        Queue<Edge> edges = kruskalMST.edges();
        // 打印输出
        for (Edge edge : edges) {
            if(edge!=null){
                System.out.println(edge.either() + "-" + edge.other(edge.either()) + "::" + edge.weight());
            }
        }
    }
}

你可能感兴趣的:(数据结构,数据结构,算法)