关于生成树的拓展 {附【转】最小瓶颈路与次小生成树}(图论--生成树)

P.S.我最近挖了好多坑啊~U·ェ·U 先讲一下蓝书上提到的关于Spanning Tree 生成树的内容。

2个性质:切割(连X-Y的边权最小的边必被生成树包含)、回路(任意回路/环上的边权最大的边必不被生成树包含)。

增量最小生成树:N点的空图上依次加入一共M条边,求每加入一条的MST最小生成树权值。
解法:1.O(m*nlog n)。每生成一个MST就删掉没有选的边,加入一条新边就做一次(N-1)+1=N条边的Kruskal算法。
2.O(mn)。加了一条边e=(u,v)后成了一个回路/环,据回路性质,找到这个回路里的最大边删掉就是解。而(u,v)必在这个回路中,便O(n)用dfs或bfs从u到v找这样的一条路径就可以了。

最小瓶颈生成树:给出加权无向图,求一棵最大边权值尽量小的生成树。
解法:由于关心最大边便把边从小到大排序,最先生成的那棵生成树就是答案。而这就是Kruskal算法,所以原图的最小生成树就是一棵最小瓶颈生成树了。
注意——最小瓶颈生成树不一定是最小生成树,但最小生成树一定是最小瓶颈生成树(这个前几行不就简单地证了吗)
模版代码见:【bzoj2429】[HAOI2006]聪明的猴子(图论--最小瓶颈生成树 模版题)

最小瓶颈路:给出加权无向图的两个结点u和v,求从u到v的一条路径,使得路径上权值最大的边的权值尽量小。
解法:1.O(logd*m)(d表示最大的边权的最大值,然后...时间复杂度←我猜是这样的 (╯o╰))二分法+bfs,二分最大边的权值,bfs跑一遍看这样的最大边权是否可以找到一条从u到v的路径。
2.O(m*logm)。Kruskal也是由于关心最大边,便同求“最小瓶颈生成树”一样思考,边排序后,求到的第一条路径就是答案,便可以跳出来了!!不需要求完整颗生成树。因此最小瓶颈路便是最小生成树上的对应路径。
模版代码见:【uva 534】Frogger(图论--最小瓶颈路 模版题)

最小瓶颈路(多组询问):给出加权无向图,求每两个结点u和v之间的最小瓶颈路的最大边长f(u,v)。
解法:相似地,先求出MST。再用dfs把MST变成有根树,每新访问一个结点v / 一条边(u,v)就更新之前求过的所有点。f(x,v)=f(v,x)=max(f(x,u),w(u,v))。这里是O(n2)。

次小生成树求权值之和第二小的生成树的权值和。
解法:先求出MST。再枚举新加的一条边(u,v),这时成环便去掉(u,v)所在环上,即u到v的路径上的权值最大的边(不删(u,v)这个新增的边)。这样就是答案了。【原因是:次小生成树一定至少有一条边与最小生成树不一样,那么存在不同于最小生成树的生成树中权值和最小的生成树就是次小生成树啦。】于是,我们可以像上一个问题一样处理出每对结点(u,v)的最小瓶颈路的最大边长f(u,v)。这样O(m log m)和O(n2)之后,就是由MST加一条边,删一条边(“边交换”)O(m)枚举m-n+1条加的边,最后O(1)算出新生成树的权值。总时间复杂度为O(m log m+n2+m)。
模版代码见:【uva 10600】ACM Contest and Blackout(图论--次小生成树 模版题)

蓝书上还提到一个最小有向生成树,我还是不学了。(.=^・ェ・^=)

----------------------------------------------------------------------------------------------------------------------------------------------

接着是一篇博文:原博——http://blog.csdn.net/fuyukai/article/details/51321680

P.S.同样的,我略加修改。下面是原文——

简介:
最小生成树是图论里面一类经典问题,可以有很多种变形,其中最小瓶颈路和次小生成树就是两种比较经典的变形。最小瓶颈路就是在两个结点之间求一条最长边最短的路径,而次小生成树则是所有生成树中权值排名第二的生成树(可以和最小生成树相等)。下面我们分别来看看这两个问题。

最小瓶颈路:
给定一个加权无向图,并给定无向图中两个结点u和v,求u到v的一条路径,使得路径上边的最大权值最小。这个问题可以稍微加强一下,即求很多对结点之间的最小瓶颈路。
无向图中,任意两个结点的最小瓶颈路肯定在最小生成树上。因此,对于第一个问题,我们可以先求出最小生成树,然后从结点u对最小生成树进行DFS直到访问到结点v,DFS过程中就可以求出最长边。这种方法非常简单,但是效率就不够高,如果结点对很多的话,我们每次都对最小生成树进行DFS就会很慢了(一次时间O(n))。
我们可以在查询前进行预处理,将所有结点对的最长边保存在一个maxcost数组中,之后每次查询直接访问数组即可,只需要O(1)时间。具体的方法是将无根的最小生成树转成有根树,转换过程中同时计算maxcost[u][v]。当访问一个新结点u时,考虑所有已经访问过的结点j,对maxcost[j][u]进行更新,公式如下:
maxcost[j][u]=max(maxcost[j][fa[u]], maxcost[j][u])
其中,fa数组中保存结点在有根树中的父节点。
代码实现:
代码实现有两种具体方式,一种基于prim算法,一种则为kruskal算法。
Prim算法:
如果使用prim算法,那么我们可以在求解最小生成树的过程中将有根树建立起来,并且同时求所有结点对maxcost。

 1 int prim(int s)
 2 {
 3     int res=0;
 4     memset(maxcost,0,sizeof(maxcost));
 5     for(int i=1;i<=n;i++)
 6         vis[i] = 0, d[i] = INF, pre[i]=i;
 7 
 8     d[s]=0;
 9     for(int i=0;i<n;i++)
10     {
11         int maxx=INF, index=-1;
12         for(int j=1;j<=n;j++)
13         {
14             if(!vis[j]&&d[j]<maxx)
15             {
16                 maxx=d[index=j];
17             }
18         }
19         if(index==-1)
20             break;
21 
22         for(int j=1;j<=n;j++)
23             if(vis[j])
24                 maxcost[index][j] = maxcost[j][index] = 
25                     max(maxcost[pre[index]][j], maxx);
26 
27         res+=maxx;
28         vis[index]=1;
29 
30         for(int j=1;j<=n;j++)
31         {
32             if(!vis[j]&&g[index][j]<d[j])
33             {
34                 d[j] = g[index][j];
35                 pre[j] = index;
36             }
37         }
38     }
39     return res;
40 }
View Code

Kruskal算法:
如果使用的是kruskal算法,其实在算法进行时我们也可以通过并查集将最小瓶颈路求解出来,但是这种方法要求并查集不能使用路径压缩优化,所以我个人更喜欢另一种做法,即先求解出最小生成树,之后将无根树转成有根树,在转换的过程中求解maxcost。详见代码:

 1 struct Edge{
 2     int u,v,w;
 3     Edge(int u=0,int v=0,int dist=0):u(u),v(v),w(dist){}
 4 }e[maxm];
 5 vector<Edge> vec[maxn];
 6 
 7 bool cmp(const Edge& a,const Edge& b){
 8     return a.w<b.w;
 9 }
10 
11 int find(int x){
12     return p[x]==x?x:p[x]=find(p[x]);
13 }
14 
15 void kruskal()
16 {
17     sort(e,e+m,cmp);
18     for(int i=1;i<=n;i++)
19         p[i]=i;
20     int cnt=0;
21     for(int i=0;i<m;i++){
22         int x=find(e[i].u),y=find(e[i].v);
23         if(x!=y){
24             p[y]=x;
25             vec[e[i].u].push_back(Edge(e[i].u, e[i].v, e[i].w));
26             vec[e[i].v].push_back(Edge(e[i].v, e[i].u, e[i].w));
27             if(++cnt==n-1)
28                 break;
29         }
30     }
31 }
32 
33 void dfs(int index)
34 {
35     vis[index]=1;
36     for(int i=0;i<vec[index].size();i++)
37     {
38         int tmp=vec[index][i].v;
39         if(!vis[tmp])
40         {
41             for(int j=1;j<=n;j++)
42                 if(vis[j])
43                 {
44                     maxcost[j][tmp] = maxcost[tmp][j] = 
45                         max(maxcost[j][index], vec[index][i].w);
46                 }
47             pre[tmp]=index;
48             dfs(tmp);
49         }
50     }
51 }
View Code

这两种方法的使用情景不同,prim算法一般用于稠密图,而kruskal算一般用于稀疏图。还有一种基于LCA的算法,在UVA 11534会用到,这里就不讲了。

次小生成树:
次小生成树问题,有很多种解法,一种很容易想到的是枚举最小生成树中不在次小生成树中的一条边(因为次小生成树一定是最小生成树替换一条边构成的),然后去掉这条边之后求解最小生成树,这种方法的时间复杂度大概是O(n*m)。另一种比较高效的做法是基于回路性质的,还是枚举要加入哪条新边(u, v),加入新边之后必然形成环,我们要做的就是在u到v的路径山删掉一条边,使权值尽量小。很显然,要删除的边就是最长边,所以我们可以用最小瓶颈路的方法求出maxcost数组,之后枚举所有的新边并求解生成树权值,这些生成树中权值最小的就是次小生成树的权值,时间复杂度为O(n^2)。
这里代码就略去了,因为跟最小瓶颈路其实是一个问题,多了一个枚举每条边的过程而已。

总结:
这看上去完全不相关的两个问题,其实我们可以通过预处理出每对结点的最小瓶颈路来求解次小生成树,这种思想是值得我们借鉴的。

posted @ 2016-11-01 20:52  konjac蒟蒻  阅读(1733)  评论(1编辑  收藏  举报