最小生成树
算法理解
最小生成树用到了一个贪心策略:图上最小的边一定在最小生成树上(MST),证法选取三个点,手模一下,很显然
Kruskal算法
最小的边一定在MST上,每次选取最小的边,添加到MST中,再判圈,若加入这条边形成圈,则不合法,可以用并查集实现,复杂度瓶颈为排序 \(O(mlogm)\)
代码
#include<bits/stdc++.h>
using namespace std;
const int N=305,M=1e5+5;
int n,m,u,v,w,ans;
int fa[N];
struct edge{
int u,v,w;
}b[M];
bool cmp(edge x,edge y){
return x.w<y.w;
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
bool merge(int x,int y){
x=find(x),y=find(y);
if(x==y) return 0;
fa[x]=y;
return 1;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
b[i]={u,v,w};
}
for(int i=1;i<=n;i++) fa[i]=i;
sort(b+1,b+1+m,cmp);
for(int i=1;i<=m;i++){
if(merge(b[i].u,b[i].v)) ans=max(b[i].w,ans);
}
printf("%d %d",n-1,ans);
}
Prim算法
和dijkstra很像,通往最近的邻居的路一定在MST上,证法如下图
然后,我们每次找到一个不在MST上的点,就把它以与MST的距离为关键字加入堆中(注:一个点可以被加入多次,因为点到MST的距离可能由新点的加入而改变),复杂度为每一次确定一条边的N乘上每次堆排序的复杂度,均摊下来有m次进入堆的操做,数在堆中的量级是m的,复杂度 \(O((n+m)logm)\)
代码
#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=305;
int n,m,u,v,w,ans1,ans2;
int vis[N];
vector<pii>b[N];
priority_queue<pii,vector<pii>,greater<pii> >q;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&w);
b[u].push_back({v,w});
b[v].push_back({u,w});
}
q.push({0,1});
while(!q.empty()){
int k=q.top().second,val=q.top().first;
q.pop();
if(vis[k]) continue;
vis[k]=1;
ans1+=val;
ans2=max(ans2,val);
for(auto i:b[k]){
int to=i.first,c=i.second;
if(vis[to]) continue;
q.push({c,to});
}
}
printf("%d %d",n-1,ans2);
}
应用
最长路径
kruskal将排序大于号改小于号即可
prim?众所周知dij是跑不了最长路的,因为其有一个松弛操作,不符合贪心策略,然而prim就不一样了,它没有松弛操作,并且在我上文的证明中是成立的,所以可行
严格次小生成树
我们先考虑一个非严格次小生成树,就是求这张图的最小生成树是否唯一,我们考虑若不唯一则一定有一条等长的边,但不一定有等长的边就会有非严格次小MST,我们枚举每一条不是树边的边,若加入这条边形成的圈上有和这条边相等的边则说明存在
怎么判断呢,只需要用倍增维护一下 \(lca(u,v)\) 上边的最大值即可,为啥是最大值,因为你加入的这条边若比圈内某一条树边要小,那么加入的就不是那条树边了,而是这条边
严格次小生成树,只需要将判断等于改成替换掉这条树边就行了
P4951 Earthquake
递一份题解吧,讲的比我明白
ybtoj 3.2最小生成树
3.2.1
板子题
3.2.2
考虑贪心,取最小的点为发电站,然后跑一遍最小生成树,然后看起来非常的对啊,喜提50分
为什么呢,因为可能并没有建立两个发电站,然后分别供应更优,所以我们建立超级原点,将原点到各个基站的代价改为,建立发电站所需的费用即可
3.2.3
由于要求动态加边,所以我们先考虑暴力没加一条边都做一次最小生成树,prim的瓶颈在堆维护,这个不好搞,而kruskal的复杂度瓶颈在排序,每次加入一条边都要排一遍序,考虑只新加入一条边,前面边的大小都是有序的,所以只需要加入一条边进行冒泡排序即可
3.2.4
考虑每加入一条边将两连通块相连时带来的贡献,注意,因为有且仅有一棵MST,所以由这条树边构造出来的边要比这条树边+1
3.2.5
ybt题解讲的很明白,太巧妙了
3.2.6
这篇的转化也很巧妙,但是我没看懂连通代表什么,所以我证明了一下,在下方图片中
注意本题的输入,本蒟蒻在这里卡了好久
3.2.7
仔细想,题目中给的约束条件相当于让你求一棵树,再仔细一想,完全符合最小生成树的定义,最后让你求一下每个点在最小生成树上的父亲,用prim做直接板子题,维护一下父亲即可
3.2.8
其实3.2.3最优解应该就是这种方法,但我没有想到
就是从小到大枚举g,然后针对剩下的边跑一遍最小生成树,考虑优化,针对已经形成的最小生成树,边的最大值只会越来越小,所以只有新加入的边才会对最小生成树产生影响,所以只需要对这部分进行最小生成树即可
本题代码实现难度于我而言很高,特意奉上代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5,inf=1e18+9;
int n,m,mg,ms,u,v,g,s,ans=inf,top;
int fa[N],vis[N],a[N];
struct edge{
int u,v,g,s;
}b[N];
bool cmp(edge x,edge y){
return x.g<y.g;
}
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
bool merge(int x,int y){
x=find(x),y=find(y);
if(x==y) return 0;
fa[x]=y;
return 1;
}
signed main(){
scanf("%lld%lld",&n,&m);
scanf("%lld%lld",&mg,&ms);
for(int i=1;i<=m;i++){
scanf("%lld%lld%lld%lld",&u,&v,&g,&s);
b[i]={u,v,g,s};
}
sort(b+1,b+1+m,cmp);
for(int i=1;i<=m;i++){
a[++top]=i;
int jj=top-1;
while(jj&&b[a[jj]].s>b[a[jj+1]].s) swap(a[jj],a[jj+1]),jj--;
for(int j=1;j<=n;j++) fa[j]=j;
int tot=0,num=0;
for(int j=1;j<=top;j++){
if(merge(b[a[j]].u,b[a[j]].v)){
tot=max(tot,b[a[j]].s);
a[++num]=a[j];
}
}
if(num==n-1) ans=min(ans,mg*b[i].g+ms*tot);
top=num;
}
if(ans==inf) printf("-1");
else printf("%lld",ans);
}
3.2.9
和上面P4951,是同一题