Description
Input
Output
Sample Input
4 0 4 9 21 4 0 8 17 9 8 0 16 21 17 16 0
Sample Output
28
这题是求解最小生成树权值之和的模板题。最常用的算法是Prim和Kruskal。其中,Prim从任一节点出发,不断扩展,适用于密集图;而Kruskal将所有边依权值序从小到大放入,在过程中判断是否产生回路,适用于稀疏图。
根据样例输入,对Prim算法实现的生成过程进行简要的说明。最初,用于为边权进行排序的优先队列pq中只有一个元素(0, 0)。经过一些条件判断,(0, 0)被从pq中提取出,(0, 0)的终点0被记作已经过。随后考虑从该终点0出发的边,用各边的路径长度更新vDist数组,直观上理解为从已经过的点(在建生成树)出发,到达未经过的点的最短路径长度。该步骤完成后,将点0出发的边 (1, 4),(2, 9),(3, 21)推入pq。进入下一次循环。
下一次循环中,首先,1被纳入在建生成树,1被标记为已经过。随后,(1, 4)被最早推出pq,因其在pq中具有最小的权值。根据类似的过程,(2, 8)和(3, 17)被推入pq。此时pq中包含四个元素:(2, 9), (3, 21), (2, 8), (3, 17),其中前面来个是从0点出发,后面两个是从1点出发。由pq的排序规则,最早出列的是(2, 8)。这是满足我们对该算法的理解的,因为(2, 8)是在建生成树与未被纳入生成树的点所发生的最短连接。
#include<iostream> #include<algorithm> #include<vector> #include<queue> #include<cstring> using namespace std; //by郭炜老师; //最小生成树模板题,给定图的邻接矩阵,求最小生成树的总权值; //优先队列实现Prim+堆 // const int INF = 1 << 30; struct Edge { int v; //端点,另一端点已知; int w; //权值; Edge (int vv, int ww): v(vv), w(ww){}; bool operator < (const Edge & e)const { return w > e.w; //权值越小,在队列中越优先; } }; vector<vector<Edge> > G(110); //图的邻接表; int HeapPrim( const vector<vector<Edge> > & G, int n) {//G是邻接表,n是顶点数目,返回值是最小生成树的权值和; //int i, j, k; Edge xDist(0,0); priority_queue<Edge> pq; //存放未经过顶点及其到在建生成树的距离; vector<int> vUsed(n); //标记顶点是否已经被加入最小生成树; int nDoneNum = 0; //已经被加入最小生成树的顶点数目; for (int i = 0; i < n; i++) { vUsed[i] = 0; } int nTotalW = 0; //最小生成树总权值; pq.push(Edge(0, 0)); //开始只有顶点0,它到最小生成树距离0; while (nDoneNum < n && !pq.empty()) { //do-while,先执行后判断; do{//每次从队列里面取离在建生成树最近的点; xDist = pq.top(); pq.pop(); }while(vUsed[xDist.v] == 1 && !pq.empty()); //如果xDist.v未被到达,则停止循环; if (vUsed[xDist.v] == 0) //作为被pop出来的第一个,它具有最小的权值,因而被加入到在建最小生成树中; { nTotalW += xDist.w; vUsed[xDist.v] = 1; nDoneNum++; for (int i = 0; i < G[xDist.v].size(); i++) { //更新新加入的邻点; int k = G[xDist.v][i].v; int w = G[xDist.v][i].w; if (vUsed[k] == 0) //只记录那些连接了当前所在点未到达点的边的信息,推入pq中; { pq.push(Edge(k, w)); } } } } if (nDoneNum < n) return -1; //图不连通 return nTotalW; } int main() { int n; while (cin >> n) { for (int i = 0; i < n; i++) G[i].clear(); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { int w; cin >> w; G[i].push_back(Edge(j, w)); } } cout << HeapPrim(G, n) << endl; } }
也可以使用Kruskal算法进行求解。
#include<iostream> #include<algorithm> #include<vector> #include<queue> #include<cstring> using namespace std; //BY 郭炜老师; //Kruskal算法的原理是简单的:按权值从小到大取边,满足不构成回路即可; //相比Prim算法,Kruskal算法在代码实现上要更简单; //使用了并查集; struct Edge{ int s, e, w; //起点,终点,权; Edge(int ss, int ee, int ww):s(ss), e(ee), w(ww){}; bool operator < (const Edge & e1)const{ //按权值排序; return w < e1.w; } }; vector<Edge> edges; vector<int> parent; //每个节点的父节点的编号; int GetRoot(int a) { if (parent[a] == a) //a是根; return a; parent[a] = GetRoot(parent[a]); return parent[a]; } void Merge(int a, int b) { int p1 = GetRoot(a); int p2 = GetRoot(b); if (p1 == p2) return; parent[p2] = p1; } int main() { int n; while (cin >> n) { parent.clear(); edges.clear(); for (int i = 0; i < n; i++) //初始状态,每个节点自成一棵树; parent.push_back(i); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { int w; cin >> w; edges.push_back(Edge(i, j, w)); } } sort(edges.begin(),edges.end()); int done = 0; //记录已完成的边; int totalLen = 0; for (int i = 0; i < edges.size(); i++) { if (GetRoot(edges[i].s) != GetRoot(edges[i].e)) { //这一条边的两个端点还未连通,可以放心地合并; Merge(edges[i].s, edges[i].e); //表示该边被选择,两端点被连通; done++; totalLen += edges[i].w; } if (done == n - 1) break; } cout << totalLen << endl; } }