简介:
最小生成树是图论里面一类经典问题,可以有很多种变形,其中最小瓶颈路和次小生成树就是两种比较经典的变形。最小瓶颈路就是在两个结点之间求一条最长边最短的路径,而次小生成树则是所有生成树中权值排名第二的生成树(可以和最小生成树相等)。下面我们分别来看看这两个问题。
最小瓶颈路:
给定一个加权无向图,并给定无向图中两个结点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。
int prim(int s)
{
int res=0;
memset(maxcost,0,sizeof(maxcost));
for(int i=1;i<=n;i++)
vis[i] = 0, d[i] = INF, pre[i]=i;
d[s]=0;
for(int i=0;i<n;i++)
{
int maxx=INF, index=-1;
for(int j=1;j<=n;j++)
{
if(!vis[j]&&d[j]<maxx)
{
maxx=d[index=j];
}
}
if(index==-1)
break;
for(int j=1;j<=n;j++)
if(vis[j])
maxcost[index][j] = maxcost[j][index] =
max(maxcost[pre[index]][j], maxx);
res+=maxx;
vis[index]=1;
for(int j=1;j<=n;j++)
{
if(!vis[j]&&g[index][j]<d[j])
{
d[j] = g[index][j];
pre[j] = index;
}
}
}
return res;
}
Kruskal算法:
如果使用的是kruskal算法,其实在算法进行时我们也可以通过并查集将最小瓶颈路求解出来,但是这种方法要求并查集不能使用路径压缩优化,所以我个人更喜欢另一种做法,即先求解出最小生成树,之后将无根树转成有根树,在转换的过程中求解maxcost。详见代码:
struct Edge{
int u,v,w;
Edge(int u=0,int v=0,int dist=0):u(u),v(v),w(dist){}
}e[maxm];
vector<Edge> vec[maxn];
bool cmp(const Edge& a,const Edge& b){
return a.w<b.w;
}
int find(int x){
return p[x]==x?x:p[x]=find(p[x]);
}
void kruskal()
{
sort(e,e+m,cmp);
for(int i=1;i<=n;i++)
p[i]=i;
int cnt=0;
for(int i=0;i<m;i++){
int x=find(e[i].u),y=find(e[i].v);
if(x!=y){
p[y]=x;
vec[e[i].u].push_back(Edge(e[i].u, e[i].v, e[i].w));
vec[e[i].v].push_back(Edge(e[i].v, e[i].u, e[i].w));
if(++cnt==n-1)
break;
}
}
}
void dfs(int index)
{
vis[index]=1;
for(int i=0;i<vec[index].size();i++)
{
int tmp=vec[index][i].v;
if(!vis[tmp])
{
for(int j=1;j<=n;j++)
if(vis[j])
{
maxcost[j][tmp] = maxcost[tmp][j] =
max(maxcost[j][index], vec[index][i].w);
}
pre[tmp]=index;
dfs(tmp);
}
}
}
这两种方法的使用情景不同,prim算法一般用于稠密图,而kruskal算一般用于稀疏图。还有一种基于LCA的算法,在UVA 11534会用到,这里就不讲了。
次小生成树:
次小生成树问题,有很多种解法,一种很容易想到的是枚举最小生成树中不在次小生成树中的一条边(因为次小生成树一定是最小生成树替换一条边构成的),然后去掉这条边之后求解最小生成树,这种方法的时间复杂度大概是O(n*m)。另一种比较高效的做法是基于回路性质的,还是枚举要加入哪条新边(u, v),加入新边之后必然形成环,我们要做的就是在u到v的路径山删掉一条边,使权值尽量小。很显然,要删除的边就是最长边,所以我们可以用最小瓶颈路的方法求出maxcost数组,之后枚举所有的新边并求解生成树权值,这些生成树中权值最小的就是次小生成树的权值,时间复杂度为O(n^2)。
这里代码就略去了,因为跟最小瓶颈路其实是一个问题,多了一个枚举每条边的过程而已。
总结:
这看上去完全不相关的两个问题,其实我们可以通过预处理出每对结点的最小瓶颈路来求解次小生成树,这种思想是值得我们借鉴的。