Prim和Kruskal算法应用----城市水管连接
问题描述:
Description:
现在有n个城镇,编号为1, 2, 3, 4…n。他们之间有m条互通的道路,每条道路有相应的长度,现在基于这些道路,选择其中的一部分,在其上铺设水管,水管长度等于道路长度。要求使得任意两个城市之间都能通水,即任何城市之间可以通过水管连接。求使得所有城市连接的水管最短长度。
Input:
第一行两个数,分别表示n和m (1<=n<=10001,1<=m<=10000*10000/2)。
后面为m行,其中每行3个数,分别表示连接的第一个城市,第二个城市,和道路长度,道路长度为大于0的整数。
Output
输出为1行,即水管最短长度。提示,如果不存在一种方案使得所有城市之间都能两两相通,那么输出的最短长度为-1。
Sample Input 1
4 5
1 2 1
1 3 1
1 4 2
2 4 3
3 4 3
Sample Output 1
4
Prim算法
该算法是采用贪婪策略进行设计的一种算法,有点类似于求最短路径的Dijkstra算法
1. Prim算法的思想方法
令G=(V,E,W),为简单起见,令顶点集为V={0,1,…,n-1}。用二维数组c来存放边集E中边的权,若与顶点i,相关联的边为eij,则eij的权为c[i][j]。假定T是最小花费生成树的边集。该算法维护两个集合S和N。开始时,令T=φ,S={0},N=V-S。然后,进行贪婪选择,选取i∈S,j∈N,并且c[i][j]最小,使S=S∪{j},N=N-{j},T=T∪{eij}。重复上述步骤,直到N为空,或找到n-1条边为止。此时,T中的边集就是我们要求取的G中的最小花费生成树。由此,Prim算法的步骤可描述如下:
(1)T=φ,S={0},N=V-S。
(2)如果N为空,则算法结束,否则转步骤(3)
(3)寻找使i∈S,j∈N,并且c[i][j]最小的i和j。
(4)S=S∪{j},N=N-{j},T=T∪{eij},转步骤(2)
具体问题具体分析,因为我上面的问题的边数最大可达10000*10000/2,若全用数组存储,会占据过多内存,所以我们就用邻接表来存储边的权值,而且我们只需要输出代价即可,不需要输出整个树,所以这里的T我们就不需要了,将上述思想稍加修改一下即可得到:
若与顶点i,相关联的边为eij,则eij的权为c[i][j]。该算法维护两个集合S和N。开始时,令sum=0,S={0},N=V-S。然后,进行贪婪选择,选取i∈S,j∈N,并且c[i][j]最小,使S=S∪{j},N=N-{j},sum累加j的代价。重复上述步骤,直到N为空,或找到n-1条边为止。此时,sum中的值就是我们要求取的最终代价。由此,Prim算法的步骤可描述如下:
(1)sum=0,S={0},N=V-S。
(2)如果N为空,则算法结束,否则转步骤(3)
(3)寻找使i∈S,j∈N,并且c[i][j]最小的i和j。
(4)S=S∪{j},N=N-{j},sum累加j的代价,转步骤(2)
2 .prim算法的代码
#include<iostream>
using namespace std;
typedef struct ArcNode {//边
int adjvex;//该边指向的顶点的下标,也就是邻接的顶点的编号
int info;//该边的权值
struct ArcNode *nextarc;//该边的下一个边
ArcNode() :adjvex(0),info(0),nextarc(nullptr) {}
}ArcNode;
typedef struct VNode {//顶点
ArcNode *firstarc;
//一般情况下每个顶点可能还有其他信息,这里比较简单就没有添加其他信息
//Type data;
VNode():firstarc(nullptr){}//该顶点的第一个弧(边)
}VNode;
typedef struct Graph
{
VNode* vertexs;//这是一个邻接表的首指针
int vexnum;//邻接表的顶点个数
int arcnum;//邻接表的边数
Graph() :vertexs(nullptr), vexnum(0), arcnum(0){}
}ALGraph;
ALGraph G;
void TailCreat()//尾插法创建链表
{
int i, k, j, info;
ArcNode *p=nullptr, *s=nullptr, *q=nullptr;
int x, y;
for (k = 0; k < G.arcnum; k++)
{
cin >> x >> y >> info;//要创建无向图,需要创建两次边
s = new ArcNode;
s->adjvex = y;
s->info = info;
p = G.vertexs[x].firstarc;
if (p == nullptr)//p为空表示这个顶点后面没有任何的节点
{
G.vertexs[x].firstarc = s;
}
else //若不空则遍历到最后一个,然后连接
{
while (p)
{
q = p;
p = p->nextarc;//p最后是nullptr,q是最后一个节点
}
q->nextarc = s;//将s接到最后一个节点上
}
//同上,再建立一个边
s = new ArcNode;
s->adjvex = x;
s->info = info;
p = G.vertexs[y].firstarc;
if (p == nullptr)//p为空表示这个顶点后面没有任何的节点
{
G.vertexs[y].firstarc = s;
}
else //若不空则遍历到最后一个,然后连接
{
while (p)
{
q = p;
p = p->nextarc;//p最后是NULL,q是最后一个节点
}
q->nextarc = s;//将s接到最后一个节点上
}
}
}
int prim_(int n) {
int i, j, u,sum=0;
//数组的第一个元素不要,下标从1开始
bool *s = new bool[n+1];
int *neig = new int[n+1];//neig[j]中的元素是集合S中的节点到第j个节点距离最小的那个值的下标
int min, *w = new int[n+1];//w[j]中的元素是集合S中的节点到第j个节点的距离的最小值
s[1] = true;//把第一个顶点并入集合S
//初始化集合N中个顶点的初始状态
ArcNode *e = nullptr;
e = G.vertexs[1].firstarc;
//从2开始各个节点初始化
for (i = 2; i <= n; i++) {
w[i] = INT_MAX;
neig[i] = 0;
s[i] = false;
}
while (e != nullptr) {//遍历顶点1的所有邻接边
w[e->adjvex] = e->info;
e = e->nextarc;
}
for (i = 1; i < n; i++) {//i用来控制循环的次数,共n-1条边
u = 0;
min = INT_MAX;
for(j=2;j<=n;j++)//从2号顶点开始寻找尚未加入S,且从S中各个顶点到这个顶点的边的权值最小的那个边
if (!s[j] && w[j] < min) {
u = j; min = w[j];
}
if (u == 0)return -1;//此图为非连通图,因为S无法到底N
sum += w[u]; s[u] = true;//符合条件的边累加到sum,将u并入S
e = G.vertexs[u].firstarc;
while(e!=nullptr) {//更新w和neig数组
if (!s[e->adjvex] && e->info < w[e->adjvex]) {
//边所指向的顶点尚未加入N且这条边的权值小于之前指向这个顶点的最小值
w[e->adjvex] = e->info;
neig[e->adjvex] = u;
}
e = e->nextarc;
}
}
delete s; delete w; delete neig;
return sum;
}
int main() {
int n, m;//输入顶点数和弧的个数
cin >> n >> m;
VNode vtemp;
ArcNode* etemp;
//对G的顶点进行初始化
G.vertexs = new VNode[n+1];//第一个顶点不要
G.vexnum = n;
G.arcnum = m;
//创建G的边
TailCreat();
//prim
cout << prim_(n);
return 0;
}
3 .复杂度分析
时间复杂度:Prim算法最外层n-1次循环寻找n-1个边中的代价,内部也是n-1次循环找N与S中最接近的边,时间复杂度共是O(n^2)
空间复杂度:从上述算法可以看出用于工作单元的空间是O(n)
Kruskal算法
此算法也是使用贪婪法策略设计的典型算法,但是和Prim算法完全不同
1 .Kruskal算法的思想方法
Kruskal算法俗称避环法。其思想方法如下:开始时,把图的所有顶点都作为孤立顶点,每个顶点都构成一棵只有根节点的树,由这些树构造一个森林T。然后,把所有的边按权的非降顺序排序,构成边集的一个非降序列。从边集中取出权最小的一条边,如果把这条边加入森林T中,不会使T构成回路,就把它加入森林中(或者把森林中某两棵树连接成一棵树);否则,就放弃它。在这种情况下,都把它从边集中删去。重复这个过程,直到把n-1条边都放到森林以后,结束这个过程。这时,该森林中所有的树就被连接成一棵树T,它就是所要求的图的最小花费生成树。
在把边e加入T中时,如果与边e相关联的顶点u和v分别在两棵树上,随着边e的加入,将使这两棵树合并成一棵树;如果与边e相关联的顶点u和v都在同一棵树上,则新加入的边e,将把这两个结点连接起来,使原来的树构成回路。为了判断把边e加入T中是否会构成回路,可以使用第3章所叙述的find(u)、find(v)操作及union(u,v)操作。前两个操作寻找u和v所在树的根结点,如果find(u)、find(v)操作表明u和v的根结点不想同,则继续执行的union(u,v)操作,将把边e加入T中,并使u和v所在的两棵树合并成一棵树;如果find(u)、find(v)操作表明u和v的根结点相同,则u和v同在一棵树上,这时就不再执行union(u,v)操作,并丢弃边e。
于是,对无向连通赋权图G=(V,E,W),求该图的最小花费生成树的Kruskal算法的步骤可叙述如下:
(1)按权的非降顺序排序E中的边
(2)令最小花费生成树的边集为T,T初始化为T=φ
(3)把每个顶点都初始化为树的根结点
(4)令e=(u,v)是E中权最小的边,E=E-{e}
(5)如果find(u)≠find(v),则执行union(u,v)操作,T=T∪{e}
(6)如果|T|<n-1,转步骤(4),否则,算法结束
同Prim算法,具体问题具体分析,同样是将边集T换成sum,这里就不详细说明了,读者应该学会自己总结归纳,凡事跟你说的明明白白,你就无法成长~,懂我意思吧。
堆和并查集的操作见郑宗汉老师的书《算法设计与分析》第3章3.2和3.4,因为c++ 11中union变成了关键字,所以我就把union函数改名为join函数。
2 .Kruskal算法的代码
#include<iostream>
using namespace std;
typedef struct edge{
int key;
int u;
int v;
edge():key(0),u(0),v(0){}
}EDGE;
typedef struct node {
struct node *p;
int rank;
int u;
node():p(nullptr),rank(0),u(0){}
}NODE;
NODE* find(NODE* xp) {
NODE *wp, *yp = xp, *zp = xp;
while (yp->p != nullptr) yp = yp->p;
while (zp->p != nullptr) {//路径压缩
wp = zp->p;
zp->p = yp;
zp = wp;
}
return yp;
}
NODE* join(NODE *xp, NODE *yp) {
NODE *up, *vp;
up = find(xp);
vp = find(yp);
if (up->rank <= vp->rank) {
up->p = vp;
if (up->rank == vp->rank)
vp->rank++;
up = vp;
}
else
vp->p = up;
return up;
}
void sift_down(EDGE E[], int m, int i) {//若第i个节点大于其儿子节点,进行循环下移
bool done = false;
while (!done && ((i = 2 * i) <= m)) {
//两个儿子节点进行比较,父亲节点将与较小的儿子节点比较
if ((i + 1 <= m)&& (E[i + 1].key < E[i].key))i++;
//E[i]是儿子节点,E[i/2]是父亲节点
if (E[i / 2].key > E[i].key) swap(E[i / 2], E[i]);
else done = true;//父亲节点不大于儿子节点
}
}
void sift_up(EDGE E[],int i) {
bool done = false;
while (!done&&i != 1) {
//子节点小于父节点则需要调整
if (E[i].key < E[i / 2].key)
swap(E[i], E[i / 2]);
else done = true;
i = i / 2;
}
}
void make_heap(EDGE E[], int m) {
int i;
//E[m] = E[0];//若数组从0开始索引则需要
for (i = m / 2; i >= 1; i--)
sift_down(E,m,i);
}
void delete_i(EDGE E[], int &m, int i) {//删除第i个节点
EDGE e;//用来存储被删除的节点
e = E[i];
if (i <= m) {
E[i] = E[m];
m--;
//若E[m]比被删除的节点小,则有可能需要上移,否则可能下移
if (E[i].key < e.key)
sift_up(E, i);
else
sift_down(E, m, i);
}
}
EDGE delete_min(EDGE E[], int &m) {
EDGE e;
e = E[1];
delete_i(E, m, 1);
return e;
}
int kruskal(NODE V[], EDGE E[], int n, int m) {
//V是顶点,E是边,n是顶点数,m是边数
int i=0, sum=0;
EDGE e;
NODE *u, *v;
make_heap(E, m);//小堆
//V[i]的rank和p已经初始化为0和nullptr
while ((i < n - 1) && (m > 0)) {
e = delete_min(E, m);//从最小堆中取下权最小的边
u = find(&V[e.u]);
v = find(&V[e.v]);
if (u != v) {
join(u, v);
sum += e.key;//符合要求的边累加到sum中
i++;
}
}
if (i == n - 1)return sum;
return -1;//这是一个非连通图,所以无最小生成树
}
int main() {
int n, m;//先输入顶点和边的个数
cin >> n >> m;
//根据顶点和边的个数创建顶点和边
auto V = new NODE[n + 1];
auto E = new EDGE[m + 1];//根据堆的性质,索引从1开始
for (int i = 1; i <= m; i++) {
cin >> E[i].u >> E[i].v >> E[i].key;//输入从u到v的权值
}
cout<<kruskal(V, E, n, m);
delete V, delete E;
return 0;
}
3 .复杂度分析
时间复杂度:find操作最多2m次,因此总花费时间最多为O(mlog*n)。构建小堆花费O(mlogm)。若所处理的是一个完全图,那么将有m=n(n-1)/2,此时,所花费的时间为O(n^2logn),若所处理的是一个平面图,那么将有m=O(n),这时,所花费的时间为O(nlogn)。
空间复杂度:因为没有边集,所以就是O(1)
总结
时间复杂度 | 空间复杂度 | |
---|---|---|
Prim算法 | O(n^2) | O(n) |
Kruskal算法 | 最坏O(n^2logn),最好O(nlogn) | O(1) |
总的来说这两个算法各有千秋,应该根据不同的应用场景去采纳不同的算法。