Algorithm Learning - The Power of Graphs

foreword

In the last article, we talked about the basics of graphs. I believe that with the basis of unweighted graphs, we will soon be able to understand weighted graphs . Having the right map is actually in line with the actual situation of our life. The shortest distance does not mean that this route is the best route, because there may be traffic jams. At this time, the right map can help us solve the problem. Each road has a certain cost. When our cost is the smallest, this road is the best.

right map

adjacency matrix

We still use the adjacency matrix for dense graphs, but unlike unweighted graphs, we are not represented by 0 and 1, but instead use weights.
adjacency matrix

adjacency list

The sparse graph also uses an adjacency list in the same way, but instead of storing simple numbers, it should store a structure that indicates the direction and weight . So here we use a class Edge representation, which is convenient for us to operate.
adjacency list

minimum spanning tree

The minimum spanning tree MST (Minimum Spanning Tree) is the main problem we want to solve in this article.
Wikipedia definition:

Minimum spanning tree is actually short for minimum weight spanning tree.
A connected graph may have multiple spanning trees. When the edges in the graph have weights, there is always a spanning tree whose sum of the edge weights is less than or equal to the sum of the weights of the other spanning tree edges. Broadly speaking, for a non-connected undirected graph, each connected component also has a minimum spanning tree, and their union is called a minimum spanning forest.

In a weighted graph, we guarantee that all nodes in the graph are connected with minimum cost , of course, the premise is that the graph is a connected graph.

Segmentation Theorem

The segmentation theorem is the theoretical support for implementing minimum spanning trees.
Wiki definition:

In a connected weighted undirected graph, given any split, the edge with the smallest weight among its cross-cut edges must belong to the minimum spanning tree of the graph.

Proof by contradiction:

Let e ​​be the crosscut edge with the smallest weight, and T be the minimum spanning tree of the graph. Assuming that T does not contain e, then if e is added to T, the resulting graph must contain a ring passing through e, and this ring only contains a cross-cutting edge - set f, the weight of f must be greater than e, then replace f with e A spanning tree with a weight less than T can be formed, which contradicts the fact that T is the minimum spanning tree. So the assumption does not hold.

I was confused at first about how a ring was formed, but after re-reading the definition of minimum spanning tree, I was enlightened. Since all nodes are included in the minimum spanning tree, adding an edge must have a cycle.
Segmentation Theorem

LazyPrimMST

Here we enter the core code part. Prim is a person's name, why is it called Lazy? Because there is more optimized Prim algorithm than him. Closer to home, we first introduce the principle of this algorithm.
We start with 0 as a split, then find the smallest edge from its cross-cutting edge, and expand to 7; then 0 and 7 are used as a split, continue to search, expand to 1, and continue to expand until all nodes are connect.
Lazy

Code

Edge class

Edge mainly stores two points and weights of edges, where Weight is a template class.

private:
    int a,b;
    Weight weight;

dense graph

Here we can still use the vector representation, but the type inside we use the class pointer. It should be noted here that coercion is required during initialization (Edge<Weight>*)NULL. This is definitely a big pit, because I use code::blocks and get an error when compiling invalid conversion from int to 'std::vector<Edge<double>*, std::allocator<Edge<double>*> >::value_type {aka Edge<double>*}. And the location of the error report is in the system file, and I don't spend much time here, just a few hours. . . .

typedef vector<Edge<Weight>* > EDGE;
private:
    int n, m;
    bool directed;
    vector<EDGE> g;

public:
    DenseGraph( int n , bool directed){
        this->n = n;
        this->m = 0;
        this->directed = directed;
        g = vector< EDGE >(n, EDGE(n, (Edge<Weight>*)NULL));
    }

Others are very similar to the unauthorized pictures, so I won't post them here. If you need to go directly to the warehouse at the end of the article to download.

sparse graph

Sparse graphs also use vectors to represent two-dimensional matrices.

private:
    int n, m;
    bool directed;
    vector<vector<Edge<Weight> *> > g;
public:
    SparseGraph( int n , bool directed){
        this->n = n;
        this->m = 0;
        this->directed = directed;
        for( int i = 0 ; i < n ; i ++ )
            g.push_back( vector<Edge<Weight> *>() );
    }

LazyPrimMST

Since we want to find the minimum value of the edge, here we use the previously learned min-heap for fast search. Also need to mark whether the node has been visited, use the marked array. You also need to add a vector to store all the edges, as well as to store the final total weight.

private:
    Graph &G;                   // 图的引用
    MinHeap< Edge<Weight> > pq;   // 最小堆, 算法辅助数据结构
    bool *marked;               // 标记数组, 在算法运行过程中标记节点i是否被访问
    vector< Edge<Weight> > mst;   // 最小生成树所包含的所有边
    Weight mstWeight;           // 最小生成树的权值

Visit a node, put all its crosscut edges into the heap.

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

        // 将和节点v相连接的所有未访问的边放入最小堆中
        typename Graph::adjIterator adj(G,v);
        for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
            if( !marked[e->other(v)] )
                pq.insert(*e);
    }

The constructor is as follows:

    // 构造函数, 使用Prim算法求图的最小生成树
    LazyPrimMST(Graph &graph):G(graph), pq(MinHeap< Edge<Weight> >(graph.E())){

        // 算法初始化
        marked = new bool[G.V()];
        for( int i = 0 ; i < G.V() ; i ++ )
            marked[i] = false;
        mst.clear();

        // Lazy Prim
        visit(0);
        while( !pq.isEmpty() ){
            // 使用最小堆找出已经访问的边中权值最小的边
            Edge<Weight> e = pq.extractMin();
            // 如果这条边的两端都已经访问过了, 则扔掉这条边
            if( marked[e.v()] == marked[e.w()] )
                continue;
            // 否则, 这条边则应该存在在最小生成树中
            mst.push_back( e );

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

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

Optimize Prim

In the previous LazyPrimMST, there are several things that were not considered:
1. After many edges become part of the minimum spanning tree, there is no need to add them to the minimum heap
. 2. We only need to focus on the shortest crosscut edge,
so we only need to keep updating the crosscut The shortest edge in the edge is enough, and a previously learned data structure-minimum index heap is also used here, which can be well modified in the heap.
We put the weights of the edges from 0 to adjacent nodes into the index heap, find the edge 0-7 is the smallest, and expand to 7; find the weights of the edges from 7 to each node except 0, which is the same as the original If it is smaller than the original index heap, update the index heap, and then find the shortest.
Prim1
The updated index heap:
Prim

Code

Of course, what we store in the index heap should be Edges, not simple values.

private:
    Graph &G;                     // 图的引用
    IndexMinHeap<Weight> ipq;     // 最小索引堆, 算法辅助数据结构
    vector<Edge<Weight>*> edgeTo; // 访问的点所对应的边, 算法辅助数据结构
    bool* marked;                 // 标记数组, 在算法运行过程中标记节点i是否被访问
    vector<Edge<Weight>> mst;     // 最小生成树所包含的所有边
    Weight mstWeight;             // 最小生成树的权值

According to the previous operation, visit node v and maintain the minimum index heap:

    // 访问节点v
    void visit(int v){

        assert( !marked[v] );
        marked[v] = true;

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

Constructor, complete minimum spanning tree:

    // 构造函数, 使用Prim算法求图的最小生成树
    PrimMST(Graph &graph):G(graph), ipq(IndexMinHeap<double>(graph.V())){

        assert( graph.E() >= 1 );

        // 算法初始化
        marked = new bool[G.V()];
        for( int i = 0 ; i < G.V() ; i ++ ){
            marked[i] = false;
            edgeTo.push_back(NULL);
        }
        mst.clear();

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

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

After testing, Prim algorithm will be two to three times faster than LazyPrim in the case of more nodes and edges.

Kruskal's algorithm

This is also the name of the inventor. .
We choose the shortest edge each time. If this edge does not form a cycle after joining, it can become part of the minimum spanning tree. When all nodes are joined, the minimum spanning tree is complete.
We can sort the edges first; to see if a cycle is formed we can use UnionFind to check the set.
Kruskal

Code

We first use the min heap to add all the edges; then create and check the set to see if the two nodes are connected, if they are connected, give up, if not, add the edge to the minimum spanning tree.

private:
    vector< Edge<Weight> > mst;   // 最小生成树所包含的所有边
    Weight mstWeight;           // 最小生成树的权值

public:
    // 构造函数, 使用Kruskal算法计算graph的最小生成树
    KruskalMST(Graph &graph){

        // 将图中的所有边存放到一个最小堆中
        MinHeap< Edge<Weight> > pq( graph.E() );
        for( int i = 0 ; i < graph.V() ; i ++ ){
            typename Graph::adjIterator adj(graph,i);
            for( Edge<Weight> *e = adj.begin() ; !adj.end() ; e = adj.next() )
                if( e->v() < e->w() )
                    pq.insert(*e);
        }

        // 创建一个并查集, 来查看已经访问的节点的联通情况
        UnionFind uf = UnionFind(graph.V());
        while( !pq.isEmpty() && mst.size() < graph.V() - 1 ){

            // 从最小堆中依次从小到大取出所有的边
            Edge<Weight> e = pq.extractMin();
            // 如果该边的两个端点是联通的, 说明加入这条边将产生环, 扔掉这条边
            if( uf.isConnected( e.v() , e.w() ) )
                continue;

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

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

Compared

Compared
Prim algorithm is obviously much faster, but Kruskal plus sorting and searching time is not very efficient


Picture reference Baidu picture
Code implementation reference liuyubobobo MOOC tutorial

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325567202&siteId=291194637