算法学习笔记(六)——最小生成树

最小生成树

1.最小生成树(minimum spanning tree)

定义:给定一幅加权无向图,找到一棵权值最小的生成树

生成树:图的生成树是一棵含有其所有结点的无环连通子图

image-20210922195659215

橙色部分即为该图的最小生成树

2.加权无向图的数据结构

加权边

API for a weighted edge

代码实现

class Edge
{
private:
    int vertax_v, vertax_w; //相邻的两个顶点
    double weight;          //加权值

public:
    Edge(int v, int w, double weight) //构造函数
        : vertax_v(v), vertax_w(w), weight(weight)
    {
    }

    double getWeight() const { return weight; }
    
    int either() const { return vertax_v; }
    int other(int v)
    {
        if (v == vertax_v)
            return vertax_w;
        else if (v == vertax_w)
            return vertax_v;
        else
            return 0;
    }

    int compareTo(const Edge &that) const //与that比较
    {
        if (this->weight > that.getWeight())
            return +1;
        else if (this->weight < that.getWeight())
            return -1;
        else
            return 0;
    }

    string toString() //将边转换为字符串输出
    {
        string s = "[ " + to_string(vertax_v) + "-" + to_string(vertax_w) + ", " + to_string(weight) + " ]";
        return s;
    }

    //重载比较运算符,方便调用greater<>以及less<>
    friend bool operator>(const Edge &a, const Edge &b)		
    {
        return a.compareTo(b) > 0;
    }

    friend bool operator<(const Edge &a, const Edge &b)
    {
        return a.compareTo(b) < 0;
    }
};

加权无向图

API for an edge-weighted graph

代码实现

class EdgeWeightGraph
{
private:
    int vertax;                 //顶点数
    int edge;                   //边数
    vector<list<Edge>> adjList; //邻接表
    vector<Edge> edges;         //所有边的集合
public:
    EdgeWeightGraph(int V) //构造一幅含有V个顶点的空图
    {
        vertax = V;
        edge = 0;
        adjList.resize(vertax, list<Edge>());
    }

    EdgeWeightGraph(string in) //从文件读取图
    {
        ifstream file(in);
        //文件读取失败
        if (!file)
        {
            printf("can't open this file.\n");
            return;
        }
        string ch;
        int i = 0, e;
        while (getline(file, ch))
        {
            istringstream iss(ch);
            if (i == 0)
            {
                iss >> vertax;
                edge = 0;
                adjList.resize(vertax, list<Edge>());
            }
            else if (i == 1)
                iss >> e;
            else if (i < e + 2)
            {
                int v, w;
                double weight;
                iss >> v >> w >> weight;
                addEdge(Edge(v, w, weight));
            }
            else
                break;
            i++;
        }
        file.close();
        //初始化边数组
        for (int v = 0; v < vertax; v++)
        {
            for (auto w : adjList[v])
                if (w.other(v) > v)
                    edges.push_back(w);
        }
    }

    int V() { return vertax; }
    int E() { return edge; }

    void addEdge(Edge e)
    {
        adjList[e.either()].push_front(e);
        adjList[e.other(e.either())].push_front(e);
        edge++;
    }

    list<Edge> &adj(int v)
    {
        return adjList[v];
    }

    vector<Edge> &getEdges()
    {
        return edges;
    }
    
	//转换为字符串输出
    string toString()
    {
        string s = to_string(vertax) + " vertices, " + to_string(edge) + " egdes\n";
        for (int v = 0; v < vertax; v++)
        {
            s += to_string(v) + ": ";
            for (auto w : adj(v))
                s += w.toString();
            s += "\n";
        }
        return s;
    }
};

3.Prim算法

Prim算法将图的顶点分为两类,一类是已经加入最小生成树的顶点,另一类是未加入最小生成树的顶点。先将一个顶点放入最小生成树,然后每次将下一条连接树中的顶点与不在树中的顶点且权值最小的边加入树中。

在Prim算法中,我们需要维护一个连接两类顶点的边的集合,并且选中权值最小的加入最小生成树中。我们采用优先队列存放这些边,并且按照是否在优先队列中删除失效边来分为延时Prim和即时Prim两种不同的实现

延时Prim算法

笔者使用了priority_queue来存放横切边(即连接两类顶点的边),marked数组和mst数组来分别存储最小生成树的顶点和边

代码实现:

//延时Prim算法
class LazyPrim
{
private:
    vector<bool> marked;                                  //最小生成树的节点
    queue<Edge> mst;                                      //最小生成树的边
    priority_queue<Edge, vector<Edge>, greater<Edge>> pq; //横切边
    double weight = 0;                                    //权值和

    //标记顶点v,并且将所有连接v并且未被标记顶点的边加入堆
    void visit(EdgeWeightGraph G, int v)
    {
        marked[v] = true;
        for (auto e : G.adj(v))
            if (!marked[e.other(v)])
                pq.push(e);
    }

public:
    LazyPrim(EdgeWeightGraph G)
    {
        marked.resize(G.V(), false);
        visit(G, 0);
        while (!pq.empty())
        {
            //获取堆中权重最小的边
            Edge e = pq.top(); 
            pq.pop();

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

            //跳过失效的边
            if (marked[v] && marked[w])
                continue;

            //将边加入最小生成树
            mst.push(e);
            weight += e.getWeight();

            //将顶点v或者w加入树种
            if (!marked[v])
                visit(G, v);
            if (!marked[w])
                visit(G, w);
        }
    }

    queue<Edge> &edges()
    {
        return mst;
    }

    double getWeight()
    {
        return weight;
    }
};

即时Prim算法

即时Prim算法中我们不需要将每一条从非树顶点到树顶点的边都放入优先队列,只需要在顶点加入树中之后更新数据后,将权重最小的放入即可。

因为priority_queue不支持随机访问的操作,因此我采用了map来存放,并且自定义了getMin()delMin()来满足使用的要求。

代码实现:

//即时Prim算法
class Prim
{
private:
    vector<bool> marked;	//最小生成树的节点
    vector<Edge> edgeTo;	//距离最小生成树最近的边
    vector<double> distTo;	//distTo[i] = edgeTo[i].getWeight()
    map<int, double> pq;	//有效横切边
    double weight;			//权值和
	
    //获取map的最小值
    int getMin()
    {
        double minWeight = numeric_limits<double>::max();
        int minKey = 0;
        for (auto v : pq)
        {
            if (v.second < minWeight)
            {
                minWeight = v.second;
                minKey = v.first;
            }
        }
        return minKey;
    }

    //删除map的最小值
    void delMin(int v)
    {
        auto it = pq.find(v);
        pq.erase(it);
    }

    //将顶点v添加到树中,更新数据
    void visit(EdgeWeightGraph G, int v)
    {
        marked[v] = true;
        for (Edge e : G.adj(v))
        {
            int w = e.other(v);

            if (marked[w])
                continue;

            if (e.getWeight() < distTo[w])
            {
                edgeTo[w] = e;
                distTo[w] = e.getWeight();
                pq[w] = distTo[w];
            }
        }
    }

public:
    Prim(EdgeWeightGraph G)
    {
        //初始化数据
        edgeTo.resize(G.V(), Edge(0, 0, 0.0));
        distTo.resize(G.V(), 0.0);
        marked.resize(G.V(), false);
        for (int v = 0; v < G.V(); v++)
            distTo[v] = numeric_limits<double>::max();	
        distTo[0] = 0.0;
        pq[0] = 0.0;
        
        //将最近的顶点添加进树中
        while (!pq.empty())
        {
            int v = getMin();
            weight += pq[v];
            delMin(v);
            visit(G, v);
        }
    }

    vector<Edge> &getMST()
    {
        return edgeTo;
    }

    double getWeight()
    {
        return weight;
    }
};

Prim算法的遍历过程

Prim's algorithm (lazy implementation) for the minimum spanning tree problem

4.Kruskal算法

Kruskal算法主要将所有边按照权重顺序(从小到大)处理他们,将边加入最小生成树中,并且加入的边不会与已经加入的边形成环,直到树中还有V-1条边为止。

通过Kruskal算法的思想我们很容易就可以实现他,用优先队列存储所有的边,并且在每一次添加边的时候用union-find算法来判断新加入的边会不会形成一个环。

代码实现:

class Kruskal
{
private:
    queue<Edge> mst;
    priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
    double weight;

public:
    Kruskal(EdgeWeightGraph G)
    {
        weight = 0.0;
        UF uf(G.V());
        for (auto e : G.getEdges())
            pq.push(e);

        while (!pq.empty() && mst.size() < G.V() - 1)
        {
            Edge e = pq.top();
            pq.pop();
            int v = e.either(), w = e.other(v);

            if (uf.Connected(v, w))
                continue;
            else
            {
                uf.Union(v, w);
                mst.push(e);
                weight += e.getWeight();
            }
        }
    }

    queue<Edge> &getMST()
    {
        return mst;
    }

    double getWeight()
    {
        return weight;
    }
};

Kruskal算法的遍历过程

Kruskal's algorithm for the minimum spanning tree problem

posted @ 2021-09-22 21:18  Astray_M  阅读(145)  评论(0)    收藏  举报