最小生成树
最小生成树:一幅加权图的最小生成树是他的一棵权值(树种所有边的权值之和)最小的生成树。
关于最小生成树的一些约定:
1)只考虑连通图
2)边的权重不一定表示距离
3)边的权重可能是0或负数
4)所有边的权重都各不相同
原理:
1)只用一条边连接树中的任意两个顶点都会产生一个新的环;
2)从树中删去一条边将会得到两棵独立的树。
切分:将图的所有顶点分为两个非空且不重叠的两个集合。
横切:是一条连接两个属于不同集合的顶点的边。
在一幅加权图中,给定任意的切分,它的横切边中的权重最小者必然于图的最小生成树。
加权无向图:
1.带权的边:
为边设计一个数据结构,他需要带有,两个顶点和边的权重,以及返回他们值的函数。
以及需要为边构造比较运算符(<,==,<=等)用来比较权重,为边排序时需要用到。
代码:
#ifndef EDGE_H
#define EDGE_H
#include<exception>
#include<string>
#include<sstream>
#include<iostream>
using namespace std;
class Edge
{
private:
int v;//顶点
int w;//另一个顶点
double weight;//边的权重
public:
Edge(){}
Edge(int v, int w, double weight) :
v(v), w(w), weight(weight){}
bool operator<(const Edge& itr)const
{
return weight < itr.weight;
}
bool operator>(const Edge& itr)const
{
return weight > itr.weight;
}
bool operator==(const Edge& itr)const
{
return weight == itr.weight;
}
bool operator!=(const Edge& itr)const
{
return weight != itr.weight;
}
bool operator<=(const Edge& itr)const
{
return !(weight > itr.weight);
}
bool operator>=(const Edge& itr)const
{
return !(weight < itr.weight);
}
double getweight(){ return weight; }
int either(){ return v; }
int other(int vertex)//求另一个顶点
{
if (vertex == v)return w;
else if (vertex == w)return v;
else exit(0);
}
string toString()//输出边
{
string s1, s2;
stringstream stream;
stream << v << "-" << w << ":";
stream >> s1;
stream.clear();
stream << weight << endl;
stream >> s2;
return s1 + s2;
}
};
#endif
测试用例:
#include<iostream>
#include"Edge.h"
int main()
{
int n, m;
double weight;
cout << "输入边" << endl;
cin >> n >> m >> weight;
Edge e(n, m, weight);
cout << e.toString() << endl;
system("pause");
}
2.加权无向图:
与一般无向图代码相差不大,只需要在创建时把边的添加改为添加数据类型,以及加一个返回边的集合的函数。
代码:
#ifndef EDGEG_H
#define EDFEG_H
#include<vector>
using std::vector;
#include"Bag.h"
#include"Edge.h"
class EdgeGraph
{
private:
int V;
int E;
vector<Bag<Edge>> adj;//邻接表
public:
EdgeGraph(int V) :V(V), adj(V),E(0){}
EdgeGraph(int V, int E) :V(V), adj(V), E(0)
{
int v, w;
double weight;
for (int i = 0; i < E; i++)
{
cout << "输入第" << i << "条边的两个顶点和权重:" << endl;
cin >> v >> w >> weight;
Edge e(v, w, weight);
addEdge(e);
}
}
int getV(){ return V; }
int getE(){ return E; }
void addEdge(Edge e)//添加边
{
int v = e.either(), w = e.other(v);
adj[v].add(e);
adj[w].add(e);
E++;
}
Bag<Edge> getadj(int v){ return adj[v]; }
Bag<Edge> edges()//返回所有边
{
Bag<Edge> b;
for (int v = 0; v < V; v++)
for (auto e : adj[v])
if (e.other(v) > v)b.add(e);//从小到大查询,比v小的都已经输出过了
return b;
}
};
#endif
测试用例:
#include<iostream>
#include<vector>
using namespace std;
#include"Edge.h"
#include"EdgeGraph.h"
#include"Bag.h"
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeGraph G(n, m);
Bag<Edge> b = G.edges();
for (auto w : b)
cout << w.toString() << endl;
system("pause");
}
两种最小生成树的算法
1.Prim算法
Prim算法能够得到任意加权连通图的最小生成树。
他的每一步都会为一棵生成中的树添加一条边。一开始这棵树只有一个顶点,然后会添加V-1条边(结点数-1),每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中。
1)延时性Prim算法
首先我们需要一条优先队列来保存横切边,也就是保存待加入树的边;
需要一个顶点数组来标记结点;
以及一条队列记录树的结点。
优先队列:
#ifndef MINPQ_H
#define MINPQ_H
template<typename Item>
class MinPQ
{
public:
MinPQ() :pq(1){}//数组0位不使用.
bool Empty(){ return N == 0; }
int size(){ return N; }
void insert(Item item)//尾插入新元素
{
pq.push_back(item);
swim(++N);
}
Item delMin()//删除最xiao元素
{
if (Empty())exit(0);
Item min = pq[1];//取得根结点元素(最xiao元素)
eaxh(1, N--);//将其和最后一个结点交换
pq.pop_back();//删除尾元素
if (N>0)sink(1);//如果有,下沉首元素
return min;
}
private:
vector<Item> pq;
int N = 0;
bool greater(int i, int j){ return pq[i] > pq[j]; }//比较
void eaxh(int i, int j)//交换
{
Item t = pq[i]; pq[i] = pq[j]; pq[j] = t;
}
void swim(int k)//小元素上浮
{
while (k > 1 && greater(k / 2, k))
{
eaxh(k/2, k);
k = k / 2;
}
}
void sink(int k)//大元素下沉
{
while (2 * k <= N)
{
int j = 2 * k;
if (j < N&&greater(j, j + 1))j++;
if (!greater(k, j))break;
eaxh(k, j);
k = j;
}
}
};
#endif
队列:
#ifndef QUEUE_H
#define QUEUE_H
template<typename Item>
class queue
{
class Node
{
friend class queue;
Item data;
Node* next;
Node(Item item, Node* x) :data(item), next(x){}
};
class Iterator
{
private:
Node* curr;
public:
Iterator(Node* c) :curr(c){}
bool operator==(const Iterator& itr)const
{
return curr == itr.curr;
}
bool operator!=(const Iterator& itr)const
{
return curr != itr.curr;
}
Item& operator*()const
{
return curr->data;
}
Iterator& operator++()
{
curr = curr->next;
return *this;
}
};
private:
Node* first = NULL;
Node* last = NULL;
int N = 0;
public:
bool Empty(){ return N == 0; }
int size(){ return N; }
void enqueue(Item item)
{
Node* oldlast = last;
last = new Node(item,NULL);
if (Empty())first = last;
else oldlast->next = last;
N++;
}
Item dequeue()
{
if (Empty())exit(0);
Node* curr = first;
Item item = first->data;
first = first->next;
delete curr;
N--;
return item;
}
Iterator begin(){ return Iterator(first); }
Iterator end(){ return Iterator(NULL); }
};
#endif
延时Prim算法步骤:
A.记录顶点,将查询顶点的边都加入优先队列;
B.取出优先队列中最小的边,如果边的两个顶点都被标记(加入边会形成环),跳过;
C.否则,将边加入树的队列,将未被标记的顶点标记,他的边加入优先队列;
D.然后再查询从优先队列中取出下一条边,继续上面的步骤,直到优先队列中无元素。
代码:
#ifndef LPMST_H
#define LPMST_H
#include<vector>
using std::vector;
#include"queue.h"
#include"MinPQ.h"
#include"Edge.h"
class LazyPrimMST
{
private:
vector<bool> marked;
queue<Edge> mst;
MinPQ<Edge> pq;
void visit(EdgeGraph G, int v)
{
marked[v] = true;
for (auto e : G.getadj(v))
if (!marked[e.other(v)])
pq.insert(e);
}
public:
LazyPrimMST(EdgeGraph G) :marked(G.getV())
{
visit(G, 0);
while (!pq.Empty())
{
Edge e = pq.delMin();
int v = e.either(), w = e.other(v);
if (marked[v] && marked[w])continue;
mst.enqueue(e);
if (!marked[v])visit(G, v);
if (!marked[w])visit(G, w);
}
}
queue<Edge> edges(){ return mst; }//输出树
double weight()//输出树的总权重
{
double ws = 0;
for (auto w : edges())
ws += w.getweight();
return ws;
}
};
#endif
测试用例:
#include<iostream>
#include<vector>
using namespace std;
#include"Edge.h"
#include"EdgeGraph.h"
#include"LazyPrimMST.h"
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeGraph G(n, m);
LazyPrimMST mst(G);
queue<Edge> b = mst.edges();
double ws = mst.weight();
for (auto w : b)
cout << w.toString() << endl;
cout << ws << endl;
system("pause");
return 0;
}
2)即时性Prim算法
顾名思义,即有效的找到最小的横切边的方法。
我们先确定树的总长度建立数组以每一结点对应一条边存储边,然后从根开始广度优先搜索图,找到结点的边,若另一结点不在树内,且权重小于结点记录的权重则替换以前的边,直到所有的遍历完图。
其中比较结点权重需要索引优先队列:
#ifndef INDEXMINPQ_H
#define INDEXMINPQ_H
#include<vector>
using std::vector;
template<typename Item>
class IndexMinPQ
{
private:
int N;//元素数量
vector<int> pq;//二叉堆索引,由1开始
vector<int> qp;//逆序:qp[pq[i]]=pq[qp[i]]=i;元素找索引
vector<Item> items;//元素
bool greater(int i, int j){ return items[pq[i]] > items[pq[j]]; }
void eaxh(int i, int j)//交换
{
int t = pq[i]; pq[i] = pq[j]; pq[j] = t;
qp[pq[i]] = i; qp[pq[i]] = j;
}
void swim(int k)//小元素上浮
{
while (k > 1 && greater(k / 2, k))
{
eaxh(k / 2, k);
k = k / 2;
}
}
void sink(int k)//大元素下沉
{
while (2 * k <= N)
{
int j = 2 * k;
if (j < N&&greater(j, j + 1))j++;
if (!greater(k, j))break;
eaxh(k, j);
k = j;
}
}
public:
//创建一个maxN的优先队列,索引取值为0~maxN
IndexMinPQ(int maxN) :items(maxN + 1), pq(maxN),qp(maxN + 1, -1), N(0){}
bool Empty(){ return N == 0; }
bool contains(int k){ return qp[k] != -1; }//是否存在索引为k的元素
void insert(int k, Item item)//索引,元素
{
N++;
qp[k] = N;
pq[N] = k;
//pq.push_back(k);//pq[N] = K,第N个数的索引为K
items[k] = item;//索引->值
swim(N);//上浮
}
int delMin()//删除元素,并返回他的索引
{
int indexofMin = pq[1];//存储最小值
eaxh(1, N--);//交换首尾
sink(1);//新头元素下沉
items[pq[N + 1]] = NULL;
qp[pq[N + 1]] = -1;
//pq.pop_back();
return indexofMin;
}
void change(int k, Item item)//将索引为K的元素设为item
{
items[k] = item;//替换值
swim(qp[k]);//索引排序
sink(qp[k]);
}
};
#endif
代码:
#ifndef PRIMMST_H
#define PRIMMST_H
#include<vector>
using std::vector;
#include"IndexMinPQ.h"
#include"Edge.h"
#include"queue.h"
class PrimMST
{
private:
vector<Edge> edgeTo;//距离树最近的边
vector<double> distTo;//distTo=edgeTo[].weight()
vector<bool> marked;//如果顶点v在树中则为true
IndexMinPQ<double> pq;//有效的横切边
public:
PrimMST(EdgeGraph G) :edgeTo(G.getV()), distTo(G.getV(),DBL_MAX),
marked(G.getV()), pq(G.getV())
{
distTo[0] = 0.0;//初始化
pq.insert(0,0.0);//初始化优先队列
while (!pq.Empty())
visit(G, pq.delMin());
}
void visit(EdgeGraph G, int v)
{
marked[v] = true;
for (auto e : G.getadj(v))//遍历边
{
int w = e.other(v);//另一条边
if (marked[w])continue;//如果已经标记,这跳过
if (e.getweight() < distTo[w])//如果边比已经记录的小
{
edgeTo[w] = e;//记录新的边
distTo[w] = e.getweight();
if (pq.contains(w))pq.change(w, distTo[w]);//如果存在顶点w的边,优先队列中替换边
else pq.insert(w, distTo[w]);//插入边
}
}
}
queue<Edge> edges()
{
queue<Edge> que;
for (int i = 1; i < edgeTo.size();i++)
que.enqueue(edgeTo[i]);
return que;
}
double weight()
{
double ws = 0.0;
for (auto e : edges())
ws += e.getweight();
return ws;
}
};
#endif
测试用用例:
#include<iostream>
#include<vector>
using namespace std;
#include"Edge.h"
#include"EdgeGraph.h"
#include"PrimMST.h"
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeGraph G(n, m);
PrimMST mst(G);
queue<Edge> b = mst.edges();
double ws = mst.weight();
for (auto w : b)
cout << w.toString() << endl;
cout << ws << endl;
system("pause");
return 0;
}
2.Kruskal算法
主要思想:按照边的权重顺序(从小到大)处理他们,将边加入最小生成树中,加入的边的不会与以前的边构成环(边的顶点有不存在于树中的),直到树有V-1条边为止。
代码:
#ifndef KRUSKAlMST_H
#define KRUSKALMST_H
#include<vector>
using std::vector;
#include"Edge.h"
#include"queue.h"
#include"MinPQ.h"
class KruskalMST
{
private:
vector<bool> marked;
queue<Edge> mst;
public:
KruskalMST(EdgeGraph G) :marked(G.getV())
{
MinPQ<Edge> pq;
for (auto e : G.edges())
pq.insert(e);
while (!pq.Empty() && mst.size() < G.getV() - 1)
{
Edge e = pq.delMin();
int v = e.either(), w = e.other(v);
if (marked[v] && marked[w])continue;
marked[v] = true;
marked[w] = true;
mst.enqueue(e);
}
}
queue<Edge> edges(){ return mst; }
double weight()
{
double ws = 0;
for (auto e : edges())
ws += e.getweight();
return ws;
}
};
#endif
测试用例:
#include<iostream>
#include<vector>
using namespace std;
#include"Edge.h"
#include"EdgeGraph.h"
#include"KruskalMST.h"
int main()
{
int n, m;
cout << "输入图的顶点数和边数:" << endl;
cin >> n >> m;
EdgeGraph G(n, m);
KruskalMST mst(G);
queue<Edge> b = mst.edges();
double ws = mst.weight();
for (auto w : b)
cout << w.toString() << endl;
cout << ws << endl;
system("pause");
return 0;
}