08-图7 公路村村通 (30 分)
现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
输入格式:
输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。
输出格式:
输出村村通需要的最低成本。如果输入数据不足以保证畅通,则输出−1,表示需要建设更多公路。
输入样例:
6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3
输出样例:
12
代码
使用的是Kruskal算法,原因是为了锻炼一下自己,顺便重新写一下最小堆还有复习一下相关的数据结构。测试结果出现了段错误,不明所以,我自己使用了浙大数据结构实验书上面的数据进行了测试,没有发现问题,不知道是用什么数据进行测试的
#include <iostream>
#define MaxSize 1001
using namespace std;
typedef int Vertex;
typedef int ElementType;
/*图的邻接表定义*/
typedef struct ListNode* List;
typedef struct ENode* Edge;
struct ENode {
Vertex v1;
Vertex v2;
ElementType Weight;
};
struct ListNode {
Vertex V;
ElementType Weight;
List Next;
ListNode():Next(NULL){}
};
typedef List PtrToNode;
typedef struct VNode{
PtrToNode FirstEdge;
}PtrArray[MaxSize];
typedef struct GraphNode* Graph;
struct GraphNode {
int Nv;
int Ne;
PtrArray heads;
};
Graph CreatGraph(int N) {
Graph G = new GraphNode;
G->Nv = N;
G->Ne = 0;
for (int i = 0; i <= N; i++) {
G->heads[i].FirstEdge = NULL;
}
return G;
}
void InsertEdge(Graph G) {
Vertex v1, v2;
ElementType Weight;
cin >> v1 >> v2 >> Weight;
List tmp = new ListNode;
tmp->Next = G->heads[v1].FirstEdge;
tmp->V = v2;
tmp->Weight = Weight;
G->heads[v1].FirstEdge = tmp;
tmp = new ListNode;
tmp->Next = G->heads[v2].FirstEdge;
tmp->V = v1;
tmp->Weight = Weight;
G->heads[v2].FirstEdge = tmp;
G->Ne++;
}
/*最小堆的定义*/
typedef Vertex SetType[MaxSize];//用于记录每个点所属的集合
typedef Vertex SetName;
Edge Eset;/*定义一个全局变量Eset作为边集合,是一个最小堆*/
void PercDown(Edge Eset, int i, int N);
void InitializeEset(Graph G,Edge Eset) {
/*用图来初始化边数组*/
List tmp;
int Ecount = 1;//边数组也从1开始计数,方便最小堆排列
for (int i = 1; i <= G->Nv; i++) {
tmp = G->heads[i].FirstEdge;
while (tmp != NULL) {
if (tmp->V < i) {
Eset[Ecount].v1 = i;
Eset[Ecount].v2 = tmp->V;
Eset[Ecount].Weight = tmp->Weight;
Ecount++;
}
tmp = tmp->Next;
}
}
/*调整为最小堆*/
for (int i = Ecount / 2; i > 0; i--) {
PercDown(Eset, i, Ecount);
}
}
void PercDown(Edge Eset, int i, int N) {
/*对Eset边数组,从第i个顶点进行向下整堆,数组的大小为N,数组区间为[1,N]*/
ENode Etmp = Eset[i];
int child, parent;
for (parent = i; parent*2 <= N;parent=child) {
child = 2 * parent;
if (child!=N&&Eset[child].Weight > Eset[child + 1].Weight) { //找更小的
child++;
}
if (Eset[child].Weight < Etmp.Weight) {
Eset[parent] = Eset[child];
}
else break;
}
Eset[parent] = Etmp;
}
ENode PopEdge(Graph G,Edge Eset, int N) {
/*从Eset中弹出一条最短边*/
ENode E = Eset[1];
Eset[1] = Eset[N];
PercDown(Eset, 1, N - 1);
return E;
}
Vertex FindRoot(SetType S, Vertex V) {
/*返回V所在的根节点的集合,使用路径压缩*/
if (S[V] < 0)
return V;
else
return S[V] = FindRoot(S, S[V]);
}
int IsCycle(SetType V_Set, ENode E) {
/*检查收录边E是否会在V_Set中构成回路,是回路返回1,不是回路返回0*/
Vertex v1 = E.v1;
Vertex v2 = E.v2;
Vertex root1, root2;
root1 = FindRoot(V_Set, v1);
root2 = FindRoot(V_Set, v2);
if (root1 == root2) {
return 1;//v1,v2在同一棵树里面,再联通会构成回路
}
else {
return 0;
}
}
void Union(SetType V_Set, Vertex v1, Vertex v2) {
V_Set[FindRoot(V_Set,v1)] = FindRoot(V_Set, v2);
}
int Kruskal(Graph G) {
int TotalWeight;
int EIsCount, CountPopEdge;
SetType VSet;//VSet是一个最小生成树集合
for (int i = 1; i <= G->Nv; i++) {/*初始化顶点集合VSet*/
VSet[i] = -1;
}
TotalWeight = 0;
EIsCount = 0;/*用于记录收录的边*/
CountPopEdge = 0;/*用于记录弹出的边,弹出的边不一定都会被收录*/
ENode Etmp;
while (EIsCount < G->Nv - 1) {
if (CountPopEdge >= G->Ne)break;
Etmp = PopEdge(G, Eset,G->Ne-CountPopEdge);
CountPopEdge++;
if (IsCycle(VSet, Etmp) ==0) {
Union(VSet, Etmp.v1,Etmp.v2);//不在同一棵树里面,连接v1,v2
TotalWeight = TotalWeight + Etmp.Weight;
EIsCount++;
}
}
if (EIsCount < G->Nv - 1) {//没有能够生成最小生成树,边不够,不连通
TotalWeight = -1;
}
return TotalWeight;
}
int main() {
int N, M;
cin >> N >> M;
Graph G = CreatGraph(N);
for (int i = 0; i < M; i++) {
InsertEdge(G);
}
Eset = new ENode[M+1];
InitializeEset(G, Eset);
cout<<Kruskal(G);
return 0;
}
测试结果
测试点
第一测试点
第2测试点
Prim算法代码
主要是有的地方1-N的时候写循环是要for(i=1;i<=Nv;i++),有的时候一重循环注意到了,二重循环没有注意到,就出错了。另外果然还是初始化什么的用一个函数来写会顺眼很多,代码里面写在了main()里面进行初始化,虽然写的时候会快一点,但是出错了查找还是比较慢的。
另外,个人认为prim算法比kruskal好写,而且使用邻接矩阵即使是稀疏图,速度和内存也还好,要求不高的情况下用邻接矩阵建图,使用ide来debug的效率会高很多!
#include<iostream>
#define MaxSize 1001
#define Inf 65535
#define Error -1
using namespace std;
typedef struct GraphNode* Graph;
typedef int Vertex;
struct GraphNode {
int Nv;
int Ne;
int Weight[MaxSize][MaxSize];
};
void InsertEdge(Graph G,int M) {
/*向G中插入M条边*/
Vertex v1, v2;
int weight;
auto W = G->Weight;
for (int i = 0; i < M; i++) {
cin >> v1 >> v2 >> weight;
W[v1][v2] = weight;//序号从1-N
W[v2][v1] = weight;
G->Ne++;
}
}
int dist[MaxSize];
int collect[MaxSize];//这里其实不需要collect,因为dist[i]==0就说明已经收录进集合了
int path[MaxSize];
Vertex FindMinDist(int Dist[], int Collect[],int Nv) {
/*搜寻未收录顶点中的最小边,由于是未收录的顶点,因此不可能形成回路,但是dist[]的定义和dijkstra不同,要注意这点!*/
int MinDist = Inf;
Vertex V;
for (int i = 1; i <= Nv; i++) {
if (Collect[i] != 1 && dist[i]<MinDist) {
V = i;
MinDist = dist[i];
}
}
if (MinDist < Inf)return V;
else
return Error;
}
int Prim(Graph G) {
Vertex S = 1;//这里我打算做个特殊处理,假设从S点开始建立最小生成树,S可以改变,而不是固定从0开始
/*初始化dist[]与collect[],path[]*/
for (int i = 1; i <= G->Nv; i++) {
dist[i] = Inf;
collect[i] = -1;
path[i] = -1;
}
path[S] = -1;
dist[S] = 0;
collect[S] = 1;//将S点收入
/*对S邻接点进行初始化*/
for (int i = 1; i <= G->Nv; i++) {
if (G->Weight[S][i] != Inf) {
dist[i] = G->Weight[S][i];
path[i] = S;
}
}
int TotalCount = 0;
Vertex V;
int Vcount = 1;//prim算法收录的点个数是NV个,收录边的条数是NV-1条
/*执行prim算法*/
while (1) {
V = FindMinDist(dist,collect,G->Nv);//寻找未收录点中的最小边
if (V == Error)break;
collect[V] = 1;//将V收入集合,从此不再计算
Vcount++;//收入的节点数加1
TotalCount = TotalCount + dist[V];//添加边长
/*更新V周围的点*/
for (int i = 1; i <= G->Nv; i++) {
if (G->Weight[V][i] != Inf&&collect[i] != 1) {
/*未收录的邻接点中如果因为收录了dist[]使得边长更小了,那么就更新dist和path*/
if (G->Weight[V][i] < dist[i]) {
dist[i] = G->Weight[V][i];
path[i] = V;
}
}
}
}
if (Vcount != G->Nv)
TotalCount = -1;
return TotalCount;
}
int main() {
int N, M;
cin >> N >> M;
Vertex v1, v2;
int weight;
Graph G = new GraphNode;
G->Nv = N;
G->Ne = 0;
for (int i = 0; i <= N; i++) {
for (int j = 0; j <= N; j++) {
G->Weight[i][j] = Inf;
}
}
InsertEdge(G,M);
/*以上为简略的使用邻接矩阵建图*/
cout<<Prim(G);
return 0;
}