【20171006】2017暑假北京学习 day 4

其实我觉得我自己以前写的那篇blog介绍的比较生动——我的口水话:最小生成树、Prim、Kruskal算法是什么?

而本篇blog仅作为复习回顾所用,所以介绍得比较简洁。

一、最小生成树

一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。

二、Prim算法

* Prim算法:由一棵树“长大”“开枝散叶”得到最小生成树。

算法简述

1.输入:一个加权连通图,其中顶点集合为V,边集合为E;

2.初始化:Vnew= {x},其中x为集合V中的任一节点(起始点),Enew= {},为空;

3.重复下列操作,直到Vnew= V:

1).在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);

2).将v加入集合Vnew中,将<u, v>边加入集合Enew中;

4.输出:使用集合Vnew和Enew来描述所得到的最小生成树。

 1 void Prim(int m[][MAXV],int nv,int begin)
 2 {
 3     int closest[MAXV];//c[i]=k记录i在最小生成树中的前驱结点是k
 4     int lowcost[MAXV];//l[i]=v记录了从节点i到已知树上任何一个节点的最小距离为v 
 5     int i,j,min,k;
 6     for(i=0;i<nv;i++)//初始化为树中存在begin节点的状态 
 7     {
 8         closest[i]=begin;
 9         lowcost[i]=m[begin][i];//这棵树就是只已知begin嘛~~ 
10     }
11     for(i=1;i<nv;i++)
12     {
13         min=INF;
14         for(j=0;j<nv;j++)
15         {
16             if(lowcost[j]!=0&&lowcost[j]<min)//寻找出所有节点中非树的节点与已知树的最短距离 
17             //lowcost[j]!=0证明此节点非树节点,lowcost[j]<min则表明当前j离树最近 
18             {
19                 min=lowcost[j];
20                 k=j;//来个k作为替身吧 
21             }
22         }
23         //输出边咯~(ò. ó)~//######################################################
24         printf("Prim %d -> %d = %d
",closest[k]+1,k+1,m[closest[k]][k]);
25         
26         lowcost[k]=0;//表明k已经在树上了 
27         for(j=0;j<nv;j++) 
28         {
29             if(m[k][j]!=0&&m[k][j]<lowcost[j])
30             /*m[k][j]!=0说明不是无用点(k==j) 
31             m[k][j]<lowcost[j]说明在之前的节点Finish以下工作以后, 
32             树上新的k节点比之前的节点 离节点j都要近 
33             */
34             {
35                 lowcost[j]=m[k][j];
36                 closest[j]=k;
37             }
38         }
39     }
40     
41     return ; 
42 }
View Code

三、Kruskal算法

* Kruskal算法:多棵树“枝蔓相连”得到最小生成树

算法简述

假设 WN=(V,{E}) 是一个含有 n 个顶点的连通网,则按照克鲁斯卡尔算法构造最小生成树的过程为:

1.先构造一个只含 n 个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有 n 棵树的一个森林。

2.之后,从网的边集 E 中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图,也就是说,将这两个顶点分别所在的两棵树合成一棵树;

3.反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。

4.依次类推,直至森林中只有一棵树,也即子图中含有 n-1条边为止。

//begin at 21:27
#include<stdio.h>
#include<stdlib.h>
#define MAXM 200000
#define MAXN 5000
typedef struct Edge
{
    int w,h,t;//weight,head,tail
}Edge;
Edge ed[MAXM+1];
int n,m;
int dad[MAXN+1];
int myCmp(const void *a,const void *b)
{
    Edge *c=(Edge *)a;
    Edge *d=(Edge *)b;
    return (c->w-d->w);
}
int findDad(int a)//by the way, merge the path
{
    int p=a;
    int q;
    while(dad[a]!=a)
        a=dad[a];
    while(dad[p]!=p)
    {
        q=p;
        p=dad[p];
        dad[q]=a;
    }
    return a;
}
void merge(int a,int b)
{
    dad[findDad(a)]=findDad(b);
}
int Kruskal()//return the total weight of the minTree
{
    int i,temp;
    scanf("%d%d",&n,&m);
    for(i=0;i<m;i++)
    {
        scanf("%d%d%d",&ed[i].h,&ed[i].t,&ed[i].w);
        if(ed[i].h>ed[i].t)
        {
            temp=ed[i].h;
            ed[i].h=ed[i].t;
            ed[i].t=temp;
        }
    }
    
    qsort(ed,m,sizeof(ed[0]),myCmp);//sort the edges 
    
    int anssum=0;
    for(i=1;i<=n;i++)
        dad[i]=i;
    i=0;
    int countEdge=0;
    while(countEdge<n-1)
    {
        if(findDad(ed[i].h)!=findDad(ed[i].t))
        {
            merge(ed[i].h,ed[i].t);
            anssum+=ed[i].w;
            countEdge++;
        }
        i++;
    }
    printf("%d
",anssum);
//Warning: now dad[] is unsorted, needs a 'for' of findDad() to sort it.
}
int main()
{
    Kruskal();
    
}
//end at 21:46
//used 19min

 四、【扩展】最小瓶颈生成树

瓶颈生成树 无向图G的一颗瓶颈生成树是这样的一颗生成树,它最大的边权值在G的所有生成树中是最小的。瓶颈生成树的值为T中最大权值边的权。

无向图的最小生成树一定是瓶颈生成树,但瓶颈生成树不一定是最小生成树。

例题:Vijos - 题库 - 繁忙的都市

五、【扩展】最小瓶颈路

摘自zengchenacmer的blog,我在这里进行了一下阅读优化: 每个点与点之间的瓶颈路

给定一个加权无向图,并给定无向图中两个结点u和v,求u到v的一条路径,使得路径上边的最大权值最小。这个问题可以稍微加强一下,即求很多对结点之间的最小瓶颈路。

【定理】无向图中,任意两个结点的最小瓶颈路肯定在最小生成树上。

因此对于最小瓶颈路问题,最普通的算法就是bfs和dfs,但这大多数时候都会超时。

对于第一个问题,我们可以先求出最小生成树,然后从结点u对最小生成树进行DFS直到访问到结点v,DFS过程中就可以求出最长边。这种方法非常简单,但是效率就不够高,如果结点对很多的话,我们每次都对最小生成树进行DFS就会很慢了(一次时间O(n))。

1、 时间高效率

当你要查询每对点与点之间的瓶颈路时 , 最好的方法是 , 先把所有点之间的求出来 , 然后存放到一个数组中 (二维数组)(查询代价O(1)),于是这要求节点不能过多,否则只能一个一个求。

1)我们先把最小生成树看成是一颗有根树 , 并且找出一个根节点(任意) ,

2)然后再从这个根节点开始遍历,求点与点之间的最大边权 。

怎么遍历呢?我们想到了dfs (经过优化的dfs) , 我们让节点只与被标记(已经被遍历的节点)的节点进行比较 , 这样就不会使每对节点都要被比较两次 。

 1 void dfs(int v)
 2 {
 3     for ( int u = 1; u <= n; ++u )  //这里还可以用邻接表来减少时间
 4     {
 5         if(!done[u] && p[u] == v)  //  done检查是否被标记 , p是指u是不是v的父亲节点
 6         {
 7            done[u] = 1;
 8            for ( int x = 1; x <= n; ++x )
 9                if (done[x] && x != u ) 
10                {
11                    maxbian[x][u] = maxbian[u][x] =  maxbian[x][v]>maxbian[u][v]?maxbian[x][v]:maxbian[u][v];
12                }
13            dfs(u);
14         }
15     }
16 }
View Code

2、空间高效率

这是当不能存储所有节点之间的最大边权的情况下的算法 , 这个算法计算单条最小瓶颈路时,肯定优于上面那个算法 , However,要求所有点与点之间的...时 , 就比上面那个算法更慢了。

这个算法充分地考虑了最小生成树是一颗树的性质:

1)先把其建立成一颗有根树(根是任意的)

2)然后求LCA(u,v)即可(倍增LCA可能更快,当然代码复杂度也提升,看情况需不需要吧)(查询一次的时间O(logn))

 1 int solve(int x , int y)
 2 {
 3     int m1 = -1 , m2 = -1;  //  初始化两个节点在遍历过程中的最大边权
 4     if(f[x] > f[y])   //  当这两个节点深度不一样时
 5     {
 6         while(f[x]>f[y])
 7         {
 8             if(m1 < dist[x])  m1 = dist[x];  //  改变最大边权
 9             x = p[x]; //  向上走后的节点 , 也就是其父亲节点
10         }
11     }
12     else if(f[x] < f[y])
13     {
14         while(f[x] < f[y])
15         {
16             if(m2 < dist[y])  m2 = dist[y];
17             y = p[y];
18         }
19     }
20     while(x != y)  //  这时 , 两个节点的深度已经一样了
21     {
22         if(m1 < dist[x])  m1= dist[x];  //  两个节点同时向上走 , 直到两个节点相同时
23         if(m2 < dist[y])  m2 = dist[y];
24         x = p[x];
25         y = p[y];
26     }
27     return m1>m2?m1:m2;  // 返回两个节点中的最大边权
28 }
View Code

六、【扩展】Matrix-Tree定理

相关Blog——Matrix-Tree定理和模意义下的矩阵行列式(【推荐】他介绍的更详细些)

矩阵一树定理(matrix-tree theorem)是一个计数定理.

若连通图G的邻接矩阵为A,将A的对角线(i,i)元素依次换为节点 i 的度d(i),其余元素(i,j) (j!=i) 取Aij的相反数,所得矩阵记为M,

则M的每个代数余子式相等,且等于G的生成树的数目.这就是 矩阵一树定理.

——from Baidu

代数余子式

在一个n阶行列式D中,把元素aij (i,j=1,2,.....n)所在的行与列划去后,剩下的(n-1)^2个元素按照原来的次序组成的一个n-1阶行列式Mij,称为元素aij余子式

Mij带上符号(-1)^(i+j)称为aij的代数余子式,

记作Aij=(-1)^(i+j) Mij。

——from Baidu

n阶行列式的计算方法
是由排成n阶方阵形式的n²个数aij(i,j=1,2,...,n)确定的一个数,
则其值为n!项之和
——from Baidu

我来稍稍解释一下:

1.对于一个图G,其邻接矩阵为A;

2.另建一个矩阵M,对于M:

 if (i==j) m[i][j] = d[i]; //d[i]为第i号点的度数 

 else m[i][j]= - a[i][j]; 

3.求M的代数余子式即可

 例题:[HEOI2015] 小z的房间

 

原文地址:https://www.cnblogs.com/CXSheng/p/7631695.html