最小生成树 总结
一个 \(n\) 个点,\(m\) 条无向边的图的生成树,通俗来说,就是保留 \(n-1\) 条边使得这些被保留的边能构成一棵 \(n\) 个点的树。若边带边权,则定义最小生成树为被保留的边边权和最小的生成树。这样的生成树可能有多个。
严谨来说就是对于边集 \(E\),从中选出一个子集 \(F\) 使得 \(F\) 能构成一棵 \(n\) 个点的树。其中权值和最小的 \(F\) 为最小生成树。
解决这类问题,常用算法有 Kruskal 算法、Prim 算法和Boruvka 算法。
Kruskal 算法
能在 \(O(m\log m)\) 的复杂度解决该问题。
算法流程
将 \(m\) 条边按边权从小到大排序。依次遍历每一条边,如果加入该边后生成树会形成环就扔掉该边。用并查集维护该过程。
正确性证明
使用归纳法。
假设当前状态成立,考虑下一条加入的边。
设当前最小生成树为 \(T\),已选出的边集为 \(F\),下一条加入 \(F\) 的边为 \(e\)。
如果 \(e\in T\),则成立。
否则 \(T+e\) 一定存在一个环。找到这个环上有且仅有一条不属于 \(F\) 的边 \(f\)。
设 \(w(k)\) 为边 \(k\) 的权值。首先一定有 \(w(f)\ge w(e)\),不然 \(f\) 会比 \(e\) 优先考虑,不会出现 \(e\in F\) 且 \(f\notin F\)。
其次一定有 \(w(f)\le w(e)\),不然 \(T+e-f\) 较 \(T\) 而言是一棵更优的生成树,不符合 \(T\) 的定义。
而当 \(w(f)=w(e)\) 时,\(F\) 并不比 \(T\) 劣。
综上得证。
代码实现
struct edge
{
int u,v,w;
}a[M];
bool cmp(edge a,edge b){return a.w<b.w;}
int find(int x)
{
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
void unionn(int x,int y)
{
int xx=find(x),yy=find(y);
fa[xx]=yy;
}
int Kruskal()
{
sort(a+1,a+m+1,cmp);
int ans=0;
for(int i=1;i<=m;i++)
{
int u=a[i].u,v=a[i].v;
if(find(u)==find(v)) continue;
ans+=a[i].w;
unionn(u,v);
}
return ans;
}
Prim 算法
能在 \(O((n+m)\log n)\) (二叉堆)、\(O(n\log n+m)\) (Fib 堆)的时间复杂度解决该问题。
实际在稠密图上运行效率略微比 Kruskal 算法快。
算法流程
维护一个点集 \(S\)。初始选定一个点 \(u\) 使得 \(S=\{u\}\)。
每一次找到不在 \(S\) 中且距离 \(S\) 中任意一个点最近的一个点。然后加入 \(S\)。加入最小生成树的边就是这个点与原先 \(S\) 中最近一个点的连边。
这个过程可以用堆优化。
正确性证明
类似 Kruskal 算法。
代码实现
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
int Prim()
{
int res=0;
memset(dis,0x3f3f3f3f,sizeof(dis));
dis[1]=0;
vis[1]=1;
q.push({0,1});
while(!q.empty())
{
int u=q.top().second,d=q.top().first;
if(d!=dis[u]||vis[u]) continue;
vis[u]=1;
res+=d;
for(int i=head[u];i!=0;i=a[i].nxt)
{
int v=a[i].v,w=a[i].w;
if(w<dis[v]) dis[v]=w,q.push({w,v});
}
}
return res;
}
Boruvka 算法
时间复杂度 \(O(m\log n)\)。较 Prim 算法在更加稠密的图上(如完全图)有更优效率。
因为每一轮操作时,每一个节点所在的连通块都已知,且在这一轮操作结束前不会更改每一个节点所在的连通块。于是能解决一些 Kruskal 算法和 Prim 算法无法解决的特殊问题。
算法流程
初始时将 \(n\) 个点视为 \(n\) 个集合。现在不断进行合并集合。
定义一轮操作如下:
对于当前每一个集合,找到距离它最近的一个集合(定义集合 \(S,T\) 间的距离为 \(\min w(u,v)( u\in S,v\in T)\)。然后将每一个集合与离它最近的集合合并成一个集合。加入最小生成树的边为两集合之间最短的边。
不断进行一轮操作,直到合并成一个集合。
可以发现,最多进行 \(O(\log n)\) 轮操作。因为设当前有 \(k\) 个集合,一轮操作后,最多还剩 \(\lceil\frac k2\rceil\) 个集合。
每一轮操作遍历所有边依次,然后处理每一个集合最近的集合。因此时间复杂度为 \(O(m\log n)\)。
一些细节说明:
问:如何保证我们不会在一轮中加入一个环?
答:集合间最短的边的两端点所在集合的最近集合一定是彼此。所以一轮中最多会加入 \(k-1\) 条边。
但是有一个例外:如果存在边权相同的情况,我们可能会在一轮中加入一个环。
因此当边权相同时,我们一定要把它们按第二关键字区分开。
代码实现
int ans=0,fa[N];
pair<int,int> dis[N];
int find(int x)
{
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
void unionn(int x,int y)
{
int xx=find(x),yy=find(y);
fa[xx]=yy;
}
void Boruvka()
{
for(int i=1;i<=n;i++) dis[i]={inf,0};
for(int i=1;i<=m;i++)
{
int u=a[i].u,v=a[i].v,w=a[i].w;
if(find(u)==find(v)) continue;
dis[find(u)]=min(dis[find(u)],{w,v});
dis[find(v)]=min(dis[find(v)],{w,u});
}
for(int u=1;u<=n;u++)
{
int v=dis[u].second;
if(v&&find(v)!=find(u))
{
unionn(u,v);
ans+=dis[u].first;
}
}
}
bool check()
{
for(int i=2;i<=n;i++) if(find(i)!=find(1)) return false;
return true;
}
int main()
{
for(int i=1;i<=n;i++) fa[i]=i;
while(!check()) Boruvka();
}
好题记录
D 【0504 B组】天空璋
题目描述:
给定一个 \(n\times n\) 的二维数组 \(a\),初始全为 \(0\)。有 \(m\) 次修改,第 \(i\) 次让 \(a_i\le x\le b_i,c_i\le y\le d_i\) 的 \(a_{x,y}\) 加上 \(w\)。
所有修改完成后,考虑一张 \(n\) 个点的完全图,\(i,j\) 之间连边的权值为 \(a_{i,j}+a_{j,i}\)。请求出这张完全图的最小生成树的边权和。
\(1\le n,m\le 10^5,1\le a_i,b_i,c_i,d_i\le n,0\le |w|\le 10^6\)。
很容易有 \(O(n^2\log (n^2)+m)\) 的二维差分+Kruskal/Prim的做法。但是该做法难以拓展。
边 \((i,j)\) 边权为 \(a_{i,j}+a_{j,i}\),过于复杂,考虑更改一下。将每一次操作对 \(a_i\le x\le b_i,c_i\le y\le d_i\) 的 \(a_{x,y}\) 加上 \(w\) 向对角线进行一次对称的操作。即每一次操作,在原来的基础上,再将 \(c_i\le x\le d_i,a_i\le y\le b_i\) 的 \(a_{x,y}\) 加上 \(w\)。这样边 \((i,j)\) 的边权就变为 \(a_{i,j}\) 了。
考虑用 Boruvka 算法。那么现在目标是求出一个连通块最近的连通块。
如果我们知道所有的 \(a_{i,j}\),那么对于连通块 \(S\),离它最近的连通块的距离即为 \(u\in S,\min_{v\notin S} a_{u,v}\),找到最小值对应的 \(v\) 所在连通块即可。
设 \(col_u\) 为当前轮操作中 \(u\) 所在的连通块编号。则我们需要处理出第 \(u\) 行中的 \(\min_{col_v\neq col_u} a_{u,v}\)。如何处理这样的 \(v\)?
其实对于每一行,我们只需要处理出 \(v\) 满足 \(a_{u,v}\) 是本行最小的,和 \(k\) 满足 \(a_{u,k}\) 是本行满足 \(col_v\neq col_k\) 中最小的。也就相当于最小值,以及与最小值不在同一连通块的次小值。这个东西是可以用线段树处理的。
考虑用扫描线处理出每一行的 \(a\) 值,同时处理出上述值。然后第 \(u\) 行就判断是否有 \(col_u\neq col_v\),如果有就连 \((u,v)\),否则连 \((u,k)\)。
于是可以在 \(O(n\log n)\) 处理一轮操作。总共有 \(O(\log n)\) 轮,于是总时间复杂度为 \(O(n\log^2 n)\),可以通过本题。
本题主要利用了 Boruvka 算法在一轮操作中,每个点所在的连通块不会发生改变的性质来处理问题。

浙公网安备 33010602011771号