最小生成树的理论基础
- 任意一颗最小生成树都可以包含无向图中权值最小的的边(可以是因为可能有多条最小边)。
- 证明(反证法):假设权值最小的边不在最小生成树中,那么必然可以断开其中的某一条边并把最小边连入最小生成树中,使总权值会变小
- 给定一张无向图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E),
n
=
∣
V
∣
n=|V|
n=∣V∣ ,
m
=
∣
E
∣
m=|E|
m=∣E∣,从
E
E
E 中选出
k
<
n
−
1
k<n-1
k<n−1 条边构成一个生成森林,若再从剩余的
m
−
k
m-k
m−k 条边中选
n
−
1
−
k
n-1-k
n−1−k 条边添加到生成森林中,使其成为
G
G
G 的生成树,并且选出的边权值之和最小,则该生成树一定可以包含
m
−
k
m-k
m−k 条边中连接生成树的两个不连通节点的权值最小的边
- 证法同上
Acwing.1146 新的开始
- 题意: 给定一个 n × n ( n < 300 ) n×n(n<300) n×n(n<300) 的矩阵,现需要给没一个点通上电,可以有如下操作:在某点耗费费用 v v v 建立一个发电站,或者和已经通上点的地方花费 p p p 建立电网,求保证所有点供上电的最小花费方案
- 思路: 相较于普通的最小生成树多了一个直接建立发电站的操作,这就使得最终结果可能是树也可能是森林。考虑引入虚拟原点作为一个超级发电站,所有需要建立发电站的点与其连接,这样最终结果肯定就是一棵树而且从思维上简化了是否需要建立发电站,转化成了普通的最小生成树问题
- C o d e Code Code:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef long long ll;
int fa[N],m;
struct node{
int u,v,w;
}e[N];
int find(int u){
if(fa[u]!=u) fa[u]=find(fa[u]);
return fa[u];
}
bool cmp(node a,node b){
return a.w<b.w;
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n;i++){
int w;
cin>>w;
e[++m]={0,i,w};
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int w;
cin>>w;
if(i>=j) continue;
else{
e[++m]={i,j,w};
}
}
sort(e+1,e+1+m,cmp);
ll res=0;
for(int i=1;i<=m;i++){
int a=find(e[i].u),b=find(e[i].v);
if(a!=b) res+=e[i].w,fa[a]=b;
}
cout<<res;
return 0;
}
Acwing.1145 北极通讯网络
- 题意: 给定二维坐标上的 n ( 500 ) n(500) n(500) 个点,要求在 n n n 个点之间通讯,给定两种通讯方式:无线电和卫星。对于无线电通讯,需要选定一个固定半径的无线电通讯设备,还另外给定若干数量的卫星,搭配的了卫星的村庄可以无视距离进行通讯,求最小无线电半径设备使得全部点联通
- 思路: 容易看出问题要求第 n − k n-k n−k 大的边 ,使用Kruskal即可
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
struct node{
int u,v;
double w;
}e[N];
bool cmp(node a,node b){
return a.w<b.w;
}
int n,m,k;
int fa[N];
double x[N],y[N];
int find(int x){
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
double cal(int i,int j){
return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n;i++){
cin>>x[i]>>y[i];
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
e[++m]={i,j,cal(i,j)};
}
if(k<1) k=1;
sort(e+1,e+1+m,cmp);
int cnt=0;
for(int i=1;i<=m;i++){
int a=find(e[i].u),b=find(e[i].v);
if(a!=b) fa[a]=b,cnt++;
if(cnt==n-1-(k-1)){
printf("%.2lf",e[i].w);
break;
}
}
return 0;
}
Acwing.346 走廊泼水节
- 题意: 给定一颗 N ( 6000 ) N(6000) N(6000) 个节点的树,要求添加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。求增加的边权值总和最小是多少。(完全图:假设一个图有n个顶点,那么如果任意两个顶点之间都有边的话,该图就称为完全图。)
- 思路: 题目已经给定了一颗最小生成树,而题目要求保留最小生成树。那么我们就需要按照构造出这个最小生成树的顺序来对每一个点进行连边。连接过程中为了不破坏最小生成树(且唯一),那么就是说新增边两点之间的最小值为w+1,对于两个连通块,一个点数为 p p p ,一个为 q q q ,那么可以得知两个连通块合并时的贡献计算: ( w + 1 ) × ( p ∗ q − 1 ) (w+1) × (p*q-1) (w+1)×(p∗q−1)
- C o d e Code Code :
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef long long ll;
int fa[N];
ll cnt[N];
struct node{
int u,v;
ll w;
}e[N];
bool cmp(node a,node b){
return a.w<b.w;
}
int find(int x){
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
void solve(){
int n;
cin>>n;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n;i++) cnt[i]=1;
for(int i=1;i<n;i++){
int a,b;
ll c;
cin>>a>>b>>c;
e[i]={a,b,c};
}
int m=n-1;
sort(e+1,e+1+m,cmp);
ll res=0;
for(int i=1;i<=m;i++){
int a=find(e[i].u),b=find(e[i].v);
if(a!=b){
res+=(cnt[a]*cnt[b]-1)*(e[i].w+1);
fa[a]=b;
cnt[b]+=cnt[a];
}
}
cout<<res<<endl;
}
int main(){
int t;
cin>>t;
while(t--){
solve();
}
return 0;
}
Acwing.1148 秘密的奶牛运输
- 题意: 给定 N ( 500 ) N(500) N(500) 个点和 M ( 1 e 4 ) M(1e4) M(1e4) 条边,求其严格次小生成树
- 方法一: 先求最小生成树,再每次删去最小生成树的一条边,在新图上做最小生成树求解。时间复杂度为
O
(
m
l
o
g
m
+
n
m
)
O(mlogm+nm)
O(mlogm+nm)
- 这种方法求不出严格次小生成树,因为如果又非严格次小生成树,删边后肯定会找到非严格的次小生成树
- 方法二: 先求最小生成树,然后依次枚举非树边,然后将该边加入树中,同时从该非树边的起点和终点路径上的一条最大边删去,使得最终图仍然是一颗树,统计最小值
-
证明:
- 定义1: 设 T T T 为图 G G G 的一棵生成树,对于非树边 a a a 和树边 b b b,插入边 a a a ,并删除边 b b b 的操作记为 ( + a , − b ) (+a,-b) (+a,−b) 。如果 T − a + b T-a+b T−a+b 之后让仍然是一棵生成树,称 ( + a , − b ) (+a,-b) (+a,−b) 是 T T T 的一次可行变换
- 定义2: 称由 T T T 进行一次可行变换所得到的新的生成树集合称为 T T T 的邻集
- 定理: 次小生成树一定在最小生成树的邻集中
- 为什么还要维护两点的次大值: 因为要求严格次小生成树,在删边时要求 w [ i ] > d i s [ a ] [ b ] w[i]>dis[a][b] w[i]>dis[a][b] 如果相等的话只维护最大可能导致无法删边
-
C o d e Code Code:
-
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef long long ll;
int to[N],head[N],nxt[N],idx,w[N];
int dis1[550][550],dis2[550][550];
struct node{
int u,v,w;
bool f;
}e[N];
int n,m;
int fa[N];
ll s;
void add(int u,int v,int val){
to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}
int find(int x){
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
bool cmp(node a,node b){
return a.w<b.w;
}
//dfs是找出非树边起点到终点中距离最大的边
void dfs(int u,int max1,int max2,int fa,int d1[],int d2[]){
d1[u]=max1,d2[u]=max2;
for(int i=head[u];i;i=nxt[i]){
int j=to[i];
int td1=max1,td2=max2;
if(j==fa) continue;
int t=max1;
if(w[i]>max1) td2=td1,td1=w[i];
else if(w[i]<max1&&w[i]>=max2) td2=w[i];
dfs(j,td1,td2,u,d1,d2);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
e[i]={u,v,w};
}
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++){
int a=find(e[i].u),b=find(e[i].v);
if(a!=b){
s+=e[i].w;
e[i].f=true;
fa[a]=b;
add(e[i].u,e[i].v,e[i].w);
add(e[i].v,e[i].u,e[i].w);
}
}
for(int i=1;i<=n;i++) dfs(i,-1e9,-1e9,0,dis1[i],dis2[i]);
ll res=1e18;
for(int i=1;i<=m;i++){
if(!e[i].f){
int a=e[i].u,b=e[i].v,c=e[i].w;
if(dis1[a][b]<c) res=min(res,s+c-dis1[a][b]);
else if(dis2[a][b]<c) res=min(res,s+c-dis2[a][b]);
}
}
cout<<res;
return 0;
}
Acwing.356 次小生成树
- 题意: 本题和上题题意一致, N N N 的数据范围变为 N ( 1 e 5 ) N(1e5) N(1e5)
- 思路: N N N 的范围使得不再能像上题一样直接暴力搜出两个点之间最长边和次长边。考虑使用 L C A LCA LCA 优化。在原来 L C A LCA LCA 的基础上增加 d 1 d1 d1 d 2 d2 d2数组来表示以 a a a 为起点向上 2 k 2^k 2k 个点的最长边和次长边。更新的时候把 L C A LCA LCA 的两段跳的更新的次大边和最大便同时用维护最大和次大的方法更新 k k k (用 k − 1 k-1 k−1 )。做 L C A LCA LCA 的时候就把向上经过的边开一个数组收集起来,最后一起更新
- C o d e : Code: Code:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int INF=0x3f3f3f3f;
typedef long long ll;
int Fa[N],fa[N][20];
int head[N],nxt[N],w[N],idx,to[N];
int d1[N][22],d2[N][22],depth[N];
int n,m;
struct node{
int u,v,w;
bool f;
}e[N];
ll s;
void add(int u,int v,int val){
to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}
int find(int x){
if(Fa[x]!=x) Fa[x]=find(Fa[x]);
return Fa[x];
}
bool cmp(node a,node b){
return a.w<b.w;
}
void Kruskal(){
for(int i=1;i<=m;i++){
int a=find(e[i].u),b=find(e[i].v);
if(a!=b){
e[i].f=true;
Fa[a]=b;
add(e[i].u,e[i].v,e[i].w);
add(e[i].v,e[i].u,e[i].w);
s+=e[i].w;
}
}
}
void bfs(){
memset(depth,0x3f,sizeof depth);
queue<int>q;
q.push(1);
depth[0]=0,depth[1]=1;
while(q.size()){
int t=q.front();
q.pop();
for(int i=head[t];i;i=nxt[i]){
int j=to[i];
if(depth[j]>depth[t]+1){
depth[j]=depth[t]+1;
fa[j][0]=t;
q.push(j);
d1[j][0]=w[i],d2[j][0]=-INF;
for(int k=1;k<=19;k++){
int anc=fa[j][k-1];
int DIS[4]={d1[j][k-1],d1[anc][k-1],d2[j][k-1],d2[anc][k-1]};
fa[j][k]=fa[anc][k-1];
d1[j][k] = d2[j][k] = -INF;
for(int u=0;u<4;u++){
int d=DIS[u];
if(d>d1[j][k]) d2[j][k]=d1[j][k],d1[j][k]=d;
else if(d!=d1[j][k]&&d>d2[j][k]) d2[j][k]=d;
}
}
}
}
}
}
int lca(int a,int b,int w){
if(depth[a]<depth[b]) swap(a,b);
static int DIS[10000];
int cnt=0;
for(int i=19;i>=0;i--){
if(depth[fa[a][i]]>=depth[b]){
DIS[++cnt]=d1[a][i];
DIS[++cnt]=d2[a][i];
a=fa[a][i];
}
}
if(a!=b){
for(int i=19;i>=0;i--){
if(fa[a][i]!=fa[b][i]){
DIS[++cnt]=d1[a][i];
DIS[++cnt]=d2[a][i];
DIS[++cnt]=d1[b][i];
DIS[++cnt]=d2[b][i];
a=fa[a][i];
b=fa[b][i];
}
}
DIS[++cnt]=d1[a][0];
DIS[++cnt]=d1[b][0];
// DIS[++cnt]=d2[a][0];
// DIS[++cnt]=d2[b][0];
}
int D1=-INF,D2=-INF;
for(int i=1;i<=cnt;i++){
if(DIS[i]>D1) D2=D1,D1=DIS[i];
else if(DIS[i]>D2&&DIS[i]!=D1) D2=DIS[i];
}
if(w>D1) return w-D1;
else if(w>D2) return w-D2;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) Fa[i]=i;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
e[i]={u,v,w};
}
sort(e+1,e+1+m,cmp);
Kruskal();
bfs();
ll res=1e18;
for(int i=1;i<=m;i++){
if(!e[i].f){
res=min(res,s+lca(e[i].u,e[i].v,e[i].w));
}
}
cout<<res;
return 0;
}