求无向连通网的所有不同构的最小生成树

前言

此为西南交通大学数据结构祖传课程设计,可惜网上似乎并没有相关资料。故在此公开个人题解。

本资料仅供交流学习,抄袭所造成的后果请自负

题目

用字符文件提供数据建立连通带权无向网络邻接矩阵存储结构。编写程序,求所有不同构的最小生成树。要求输出每棵最小生成树的各条边(用顶点无序偶表示)、最小生成树所有边上的权值之和;输出所有不同构的生成树的数目

程序设计报告

总体设计

总体上利用Kruskal算法以及回溯算法的思想,使用逐步添加边的方式进行最小生成树的寻找,同时使用并查集辅助搜索。
具体编程方面使用C++,遵循C++11标准。

详细数据结构设计

程序使用数组记录所有的输入边,如下所示:

struct Edge{
    int first;
    int second;
    int weight;
};
vector<Edge> edges;// {u, v, weight}

其中first和second表示一条边的两个端点,weight表示该无向边的权值。
因为程序不需要使用顶点的集合,故无须使用邻接表存储各点之间的关系。

对于寻找到的路径,使用二维数组进行存储:

vector<vector<Edge>>

最后根据该数组输出找到的所有最小生成树。

并查集的设计参考通用模板,采用数组进行集合归属的记录,同时使用秩模糊地记录各个集合的个数,在合并集合时产生查找效率更高的树。

vector<int> parent;
vector<int> rank;

详细算法设计

关于并查集的设计,每次union操作会将秩小的集合合并至秩大的集合。每次find操作在执行完寻找后会尝试将自身节点提高至根节点,实现路径压缩,same操作提供检查两个元素是否在同一集合的方法。
通过并查集,我们可以很快地确定一条边的两点是否连通,以确定添加该边是否会造成环的出现,保证寻找到的为最小生成树。

搜索方面采用深度优先搜索以及回溯的思想进行最小生成树的寻找,并对过程进行剪枝,提高运行效率。

核心代码如下(删去调试代码以及剪枝代码):

    for (size_t i = index; i < edges.size(); i++){
        if (!uf.Same(edges[i].first, edges[i].second)){
            UnionFind ufCopy = uf;//保存并查集的状态
            //更新状态
            uf.Union(edges[i].first, edges[i].second);
            currTree.push_back(edges[i]);
            currWeights += edges[i].weight;
      
            Search(uf, currTree, currWeights, minWeights, i + 1, edges, n, MSTs);//进入下一层搜索

            //回溯
            uf = ufCopy;
            currTree.pop_back();
            currWeights -= edges[i].weight;
       	 }
}

其中,uf为并查集,currTree为当前搜索产生的树,currWeight为当前搜索产生的权值和,edges为边集,MSTs为记录所有最小生成树的输出集合,index为进入此搜索层的初始下标。
在进入搜索前,我们保证边集已经按照权值从小到大的顺序进行了排序。
正如注释所述,在进入下一搜索层时,我们先保存并查集的状态,然后更新并查集,当前搜索产生的树,当前搜索产生的权值和,使i + 1,然后进入下一层搜索。
当搜索结束后,回溯状态,然后使i加1,从边集的下一条边开始搜索。

对搜索的边界条件可以考虑以下几种情况:

  1. currTree中有n-1条边。
  2. 剩余的边加上currTree中的边不足n-1。
  3. 当前权值和超过已知的最小生成树的权值和。

对于一颗最小生成树,顶点数为n,则边数一定为n-1。所以第一种情况是容易想到的。
而其余两种情况是对搜索的剪枝。当我们发现当前的搜索已经不可能生成一颗最小生成树,则我们应该放弃当前的搜索,重新开始下一次搜索。这样的剪枝操作大幅提升了程序的运行效率。

具体的判定条件如下:

  1. if (currTree.size() == n - 1)
  2. if (edges.size() - index + currTree.size() < n - 1 )
  3. if (currWeights + edges[i].weight > minWeights)

n即为顶点数。
其中第三条放置于内部循环中,在复制并查集前进行,这样可以免去一次复制操作。
当且仅当程序因为第一个条件进入终点时,我们执行一次最小生成树的保存。

时间复杂度分析

个人认为最后的平均时间复杂度应该是
O ( M × ln ⁡ M + N × C M N ) O \left ( M\times \ln_{}{M} + N \times C _{M}^{N} \right ) O(M×lnM+N×CMN)
其中边的个数为M,点的个数为N。

我也不知道结果是对还是错,就不展示推理过程误人子弟了。

源码

并查集 UnionFind.h

//并查集
//UnionFind.h

#ifndef UNIONFIND_H
#define UNIONFIND_H

#include <vector>
#include <numeric>
class UnionFind
{
private:
    std::vector<int> parent;
    std::vector<int> rank;
public:
    UnionFind() = default;
    
    UnionFind(int n)
    {
        parent.resize(n);
        rank.resize(n);
        std::iota(parent.begin(), parent.end(), 0);
        std::fill(rank.begin(), rank.end(), 1);
    }

    int Find(int x)
    {
        if (parent[x] == x)
            return x;
        return parent[x] = Find(parent[x]);
    }

    void Union(int x, int y)
    {
        int rootX = Find(x);
        int rootY = Find(y);
        if (rootX == rootY)
            return;
        if (rank[rootX] < rank[rootY])
            std::swap(rootX, rootY);
        parent[rootY] = rootX;
        rank[rootX] += rank[rootY];
    }

    bool Same(int x, int y)
    {
        return Find(x) == Find(y);
    }
};
#endif // !UNIONFIND_H

主函数 main .cpp

//主程序(注意调试宏的设置)
//main.cpp

#include <iostream>
#include <vector>
#include <algorithm>
#include <fstream>
#include <chrono>

#include "UnionFind.h"
using namespace std;

//过程调试开关,开启后会输出搜索过程
//#define PROCESS_DEBUG 

//时间测试开关,开启后会输出搜索时间
//#define TIME_DEBUG

struct Edge{
    int first;
    int second;
    int weight;
};

//参数含义:
//输入:
//uf:并查集,currTree:当前树,currWeights:当前权重和,minWeights:最小权重和
//indx:当前搜索的边的下标,edges:边的集合,n:节点数  
//输出:
//MSTs:最小生成树的集合
void Search (UnionFind &uf, vector<Edge> &currTree, int currWeights, int &minWeights, size_t index, const vector<Edge> &edges, const size_t n, vector<vector<Edge>> &MSTs){
    //参数虽然多了点,为了避免使用全局变量,也为了减少不必要的计算

    //如果剩余的边数不够构成最小生成树,直接返回,剪枝
    if (edges.size() - index + currTree.size() < n - 1 ){
#ifdef PROCESS_DEBUG
        cout << "剩余边无法构成最小生成树\n";
        cout << endl;
#endif
        return;
    }

    if (currTree.size() == n - 1){
        if (currWeights < minWeights){//该情况一般只会出现一次,因为边的权重是从小到大排序的,第一次出现最小权重和的情况就是最小生成树
            MSTs.clear();//清空之前的MSTs
            MSTs.push_back(currTree);
            minWeights = currWeights;//更新最小权重和
        }
        else if (currWeights == minWeights){
            MSTs.push_back(currTree);
        }

#ifdef PROCESS_DEBUG
        cout << "添加MST:\n";
        for (const auto &edge : currTree)
        {
            cout << "(" << edge.first << ", " << edge.second << ") ";
        }
        cout << ",权重和:" << currWeights << '\n';
        // cout << "Press any key to continue...";
        // cin.get();

        cout << endl;
#endif
        return;
    }

    for (size_t i = index; i < edges.size(); i++){
        if (!uf.Same(edges[i].first, edges[i].second)){

//如果当前权重和加上要加的边已经大于最小权重和,放弃当前搜索
            if (currWeights + edges[i].weight > minWeights){
#ifdef PROCESS_DEBUG
            cout << "尝试添加边:(" << edges[i].first << ", " << edges[i].second << ")"
                 << ",权重:" << edges[i].weight << ",";
            cout << "当前权重和:" << currWeights + edges[i].weight << ",当前最小权重和:" << minWeights << '\n';
            cout << "当前权重和已经大于最小权重和\n";
            cout << endl;
#endif
                continue;
            }

            UnionFind ufCopy = uf;//保存并查集的状态,这将造成大量的时间和空间开销,也许可以优化,比如说改用一个支持删除的并查集
            //更新状态
            uf.Union(edges[i].first, edges[i].second);
            currTree.push_back(edges[i]);
            currWeights += edges[i].weight;
            
#ifdef PROCESS_DEBUG
            cout << "添加边:(" << edges[i].first << ", " << edges[i].second << ")"
                 << ",权重:" << edges[i].weight;
            cout << ",当前权重和:" << currWeights;
            cout << ",当前树:";
            for (const auto &edge : currTree)
            {
                cout << "(" << edge.first << ", " << edge.second << ") ";
            }
            cout << endl;
#endif
      
            Search(uf, currTree, currWeights, minWeights, i + 1, edges, n, MSTs);//进入下一层搜索

            //回溯
            uf = ufCopy;
            currTree.pop_back();
            currWeights -= edges[i].weight;

#ifdef PROCESS_DEBUG
            cout << "移除边:(" << edges[i].first << ", " << edges[i].second << ")"
                 << ",权重:" << edges[i].weight;
            cout << ",当前权重和:" << currWeights;
            cout << ",当前树:";
            for (const auto &edge : currTree)
            {
                cout << "(" << edge.first << ", " << edge.second << ") ";
            }
            cout << endl;
#endif
        }
    }
}

vector<vector<Edge>> FindAllMST(vector<Edge> &edges, int n){
    sort(edges.begin(), edges.end(), [](Edge &a, Edge &b) { return a.weight < b.weight; });//按照权重从小到大排序

    //一些驱动变量
    vector<vector<Edge>> MSTs;
    vector<Edge> currTree;
    UnionFind uf(n + 1);
    int minWeights = INT_MAX;

    Search(uf, currTree, 0, minWeights, 0, edges, n, MSTs);
    return MSTs;
}

int main (){

    vector<Edge> edges;// {u, v, weight}
    ifstream fin("input.txt");
    int n, m;
    // cout << "Please input the number of nodes: ";
    // cin >> n;
    fin >> n;

    // cout << "Please input the number of edges: ";
    // cin >> m;
    fin >> m;

    //cout << "Please input the edges (first, second, weight): " << endl;
    for (int i = 0; i < m; i++){
        int u, v, weight;
        // cin >> u >> v >> weight;
        fin >> u >> v >> weight;
        edges.push_back({u, v, weight});
    }

#ifdef PROCESS_DEBUG
    cout << "过程调试模式开启\n";
#endif

#ifdef TIME_DEBUG
    cout << "时间测试模式开启\n";
    chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
#endif

    vector<vector<Edge>> MSTs = FindAllMST(edges, n);

#ifdef TIME_DEBUG
    chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
    cout << "Time: " << chrono::duration_cast<chrono::milliseconds>(t2 - t1).count() << "ms" << endl;
#endif

#ifndef TIME_DEBUG //如果为时间测试模式,不输出,刷屏太严重,影响看时间的输出了
    cout << "The number of MSTs is: " << MSTs.size() << endl;
    int num = 1;
    for (const auto &MST : MSTs)
    {
        int weightSum = 0;
        cout << "MST" << num++ << ": ";
        for (const auto &edge : MST)
        {
            cout << "(" << edge.first << ", " << edge.second << ") ";
            weightSum += edge.weight;
        }
        cout << " Weights:" << weightSum << "\n\n";
    }
#endif

    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值