前言
此为西南交通大学数据结构祖传课程设计,可惜网上似乎并没有相关资料。故在此公开个人题解。
本资料仅供交流学习,抄袭所造成的后果请自负
题目
用字符文件提供数据建立连通带权无向网络邻接矩阵存储结构。编写程序,求所有不同构的最小生成树。要求输出每棵最小生成树的各条边(用顶点无序偶表示)、最小生成树所有边上的权值之和;输出所有不同构的生成树的数目
程序设计报告
总体设计
总体上利用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,从边集的下一条边开始搜索。
对搜索的边界条件可以考虑以下几种情况:
- currTree中有n-1条边。
- 剩余的边加上currTree中的边不足n-1。
- 当前权值和超过已知的最小生成树的权值和。
对于一颗最小生成树,顶点数为n,则边数一定为n-1。所以第一种情况是容易想到的。
而其余两种情况是对搜索的剪枝。当我们发现当前的搜索已经不可能生成一颗最小生成树,则我们应该放弃当前的搜索,重新开始下一次搜索。这样的剪枝操作大幅提升了程序的运行效率。
具体的判定条件如下:
- if (currTree.size() == n - 1)
- if (edges.size() - index + currTree.size() < n - 1 )
- 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;
}