最小生成树拓展应用

最小生成树拓展应用

  • 虚拟源点
  • kruskal拓展
  • 次小生成树

理论基础

  1. 任意一棵最小生成树一定可以包含无向图中权值最小的边
  2. 给定一张无向图 G = ( V , E ) , n = ∣ V ∣ , m = ∣ E ∣ G=(V,E),n=|V|,m=|E| G=(V,E),n=V,m=E,从E中选出k<n-1条边构成G的加一个生成森林。若再从剩余的m-k条边中选n-1-k条边添加到生成森林中,使其成为G的生成树,并且选出的边的权值之和最小。则该生成树一定可以包含m-k条边中连接生成森林的两个不连通接节点的权值最小的边

题单

1. 新的开始

第一眼:

  • 要么和其他矿井建立电网共用一个发电站,要么自建发电站
  • 只有当自建站比建电网费用要小时,才自建站
  • 把自环也当成一条边放进去sort,当用不到点时就结束?
  • 已经有电力供应的也可以给其他供应,意思就是进入了生成树就具有供电能力,因此不用担心加进来的边是那个点拉进来的

思考:

感觉是可以prim算法的

  • 为什么prim要过还得有 g [ j ] [ i ] = m i n ( g [ j ] [ i ] , v ) g[j][i]=min(g[j][i],v) g[j][i]=min(g[j][i],v),一篇ac题解

小试牛刀,没过

听y话:

建立一个超级源点,可以解决从哪个点开始的问题,如果只选最小点开始,会把其他自环(也应当看成一条边)忽略而没考虑到

#include<bits/stdc++.h>

using namespace std;
const int N=310,INF=0x3f3f3f3f;
int g[N][N],v[N],d[N],st[N];
int res,n;

void prim(int s){
  memset(d,0x3f,sizeof d);
  d[s]=g[s][s];

  for(int i=1;i<=n;i++){
    int t=-1;
    for(int j=1;j<=n;j++){
      if(!st[j]&&(t==-1||d[t]>d[j])){
        t=j;
      }
    }
    st[t]=1;
    //cout<<t<<","<<d[t]<<' ';
    res+=d[t];
    for(int j=1;j<=n;j++) d[j]=min(d[j],g[t][j]);
  }
}

signed main(){
  cin>>n;
  //memset(g,0x3f,sizeof g);
  int minx=INF,mindex=0;
  for(int i=1;i<=n;i++){
    cin>>g[i][i];
    for(int j=1;j<=n;j++){
      g[j][i]=min(g[j][i],g[i][i]);
    }
    if(minx>g[i][i]){
      mindex=i;
      minx=g[i][i];
    }
  }
  for(int i=1;i<=n;i++){
    for(int j=1;j<=n;j++){
      int x;
      cin>>x;
      if(i!=j) g[i][j]=x;
    }
  }
  prim(mindex);
  //cout<<endl;
  cout<<res<<endl;
  return 0;
}

手搓建立一个超级源点,ac了,prim算法

#include<bits/stdc++.h>

using namespace std;
int n;
const int N=310;
int g[N][N],st[N],d[N];
int res;

int prim(){
  memset(d,0x3f,sizeof d);
  d[0]=0;
  
  for(int i=0;i<=n;i++){
    int t=-1;
    for(int j=0;j<=n;j++){
      if(!st[j]&&(t==-1||d[t]>d[j])){
        t=j;
      }
    }
    st[t]=1;
    res+=d[t];
    for(int j=0;j<=n;j++) d[j]=min(d[j],g[t][j]);
  }
}


signed main(){
  cin>>n;
  for(int i=1;i<=n;i++){
    int x;
    cin>>x;
    g[0][i]=g[i][0]=x;
  }
  for(int i=1;i<=n;i++){
    for(int j=1;j<=n;j++){
      cin>>g[i][j];
    }
  }
  prim();
  cout<<res<<endl;
  return 0;
}
2. 北极通讯网络

第一眼:

  • 加了k限制的最小生成树,能不能用一个变量去计数呢?
  • 挺复杂的一道题,看的第一遍没懂

听y讲:

  • 涉及到通信问题——”中转“,卫星通信(有限),无线收发器(无限)
  • 用并查集就不需要二分了

思考:

  • 要找到最小的d,那一定是先让小边进入kruskal
  • 关于连通块的问题
    • 并查集
    • bfs和dfs有联想到
  • 有点明白,因为这道题一开始并没有直接相连的点,所有点都是独立的,我们要去找最小生成树的话,在本题用kruskal的时候,枚举一次,连通块的个数就会减一
  • 找到第一个能让剩余连通块个数小于等于k的边就行了

版本一过啦,烙铁~

#include<bits/stdc++.h>

using namespace std;
#define x first
#define y second
typedef pair<int,int> PII;
const int N=510,M=N*N;
PII dian[N];
int fa[N];
int n,k;

double get_dis(PII a,PII b){
  double dx=a.x-b.x;
  double dy=a.y-b.y;
  return sqrt(dx*dx+dy*dy);
}

struct edge{
  int x,y;
  double z;
  bool operator<(const edge& M)const{
    return z<M.z;
  }
}e[M];

int find(int x){
  if(x!=fa[x]) fa[x]=find(fa[x]);
  return fa[x];
}

signed main(){
  cin>>n>>k;
  for(int i=1;i<=n;i++) fa[i]=i;
  for(int i=1;i<=n;i++){
    int x,y;
    cin>>x>>y;
    dian[i]={x,y};
  }
  int cnt=0;
  for(int i=1;i<=n;i++){
    for(int j=1;j<=n;j++){
      double dist=get_dis(dian[i],dian[j]);
      e[cnt++]={i,j,dist};
    }
  }
  
  sort(e,e+cnt);
  int count=n;
  double res=0;
  for(int i=0;i<cnt;i++){
    int a=e[i].x,b=e[i].y;
    double c=e[i].z;
    a=find(a),b=find(b);
    if(a!=b){
      fa[a]=b;
      res=c;
      count--;
    }
    if(count<=k){
      break;
    }
  }
  printf("%.2f\n",res);
  return 0;
}
3. 走廊泼水节

第一眼:

  • 何为完全图(俩俩之间有边就是完全图)image-20240627183647506
  • 求的是增加的,而不总的,好像可以kruskal解决,但是得知道完全图是什么意思

听y说:

  • 按照什么样的顺序连接能够得到最小值
  • 新边 < w i w_i wi ❌——》不满足生成树定义
  • 新边 = w i w_i wi ❌——》要求生成树唯一

思考:

  • 关于 新边 〉= w i + 1 w_i+1 wi+1 为什么构造的生成树一定是唯一的
    • 因为你要求最小生成树,如果 w i + 1 w_i+1 wi+1 不唯一,那就意味着有 w i w_i wi可以被添加到里面去,那求的原来的生成树就不是最小生成树了,和原树是一个最小生成树矛盾。
  • 怎么把两个集合的所有点连起来
    • 用并查集维护各连通块点的个数
  • 优质题解image-20240628004028620
#include<bits/stdc++.h>

using namespace std;
int n;
const int N=6e3+10,M=N*N;
int fa[N],psize[N];

struct edge{
  int x,y,z;
  bool operator<(const edge& M)const{
    return z<M.z;
  }
}edges[M];

int find(int x){
  if(x!=fa[x]) fa[x]=find(fa[x]);
  return fa[x];
}

signed main(){
  int t;
  cin>>t;
  while(t--){
    cin>>n;
    for(int i=1;i<=n;i++) fa[i]=i,psize[i]=1;
    for(int i=0;i<n-1;i++){
      int a,b,c;
      cin>>a>>b>>c;
      edges[i]={a,b,c};
    }
		sort(edges,edges+n-1);
    int res=0;
    for(int i=0;i<n-1;i++){
      int a=find(edges[i].x),b=find(edges[i].y),c=edges[i].z;
      if(a!=b){
        res+=((psize[a]*psize[b]-1)*(c+1));
        //因为是还需要多少边,所以原本存在的c不用加
        psize[b]+=psize[a];
        fa[a]=b;
      }
    }
    cout<<res<<endl;
  }
  return 0;
}
4. 秘密的牛奶运输

第一眼:

  • 又是奶牛,又是usaco
  • 费用第二小怎么搞次最小生成树
  • 费用第二严格大于费用最小,距离z代表着成本

思考:

  • 可不可以找到最小生成树的后一条边(这个边需要满足能生成树)当作答案?

  • 啥玩意image-20240628005813618

听y说:

  • image-20240628012126169
  • 注意总长度会爆int,要开long long
#include<bits/stdc++.h>

#define int long long 
using namespace std;
int n,m;
const int N=510, M=1e4+10;
int fa[N],d1[N][N],d2[N][N];
//d1存储的是两点之间路径的最长的边
//d2存储的是两点之间路径的次长的边
int h[N],e[2*N],ne[2*N],w[2*N],idx;
//因为是树的结构,可以看成每个点最多有两个子节点

void add(int a,int b,int c){
  e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

struct edge{
  int x,y,z;
  bool f;
  bool operator<(const edge& M)const{
    return z<M.z;
  }
}es[M];

int find(int x){
  if(x!=fa[x]) fa[x]=find(fa[x]);
  return fa[x];
}

//d1,d2形式参数的类型是一维数组,而实参穿的也是一维数组的地址
void dfs(int u,int father,int dmax1,int dmax2,int d1[],int d2[]){
  d1[u]=dmax1,d2[u]=dmax2;
  for(int i=h[u];~i;i=ne[i]){
    int j=e[i];
    if(j!=father){
      int td1=dmax1,td2=dmax2;
      if(w[i]>td1) td2=td1,td1=w[i];
      else if(w[i]<td1&&w[i]>td2) td2=w[i];
      dfs(j,u,td1,td2,d1,d2);
    }
  }
}

signed main(){
  cin>>n>>m;
  memset(h,-1,sizeof h);
  //memset(d1,0x3f,sizeof d1);
  //memset(d2,0x3f,sizeof d2);
  for(int i=1;i<=n;i++) fa[i]=i;
  for(int i=0;i<m;i++){
    int x,y,z;
    cin>>x>>y>>z;
    es[i]={x,y,z};
  }
  sort(es,es+m);
  //求最小生成树
  int sum=0;
  for(int i=0;i<m;i++){
    int a=es[i].x,b=es[i].y,c=es[i].z;
    int pa=find(a),pb=find(b);
    if(pa!=pb){
      fa[pa]=pb; //这里需要找到各自的父节点然后再创建连接
      sum+=c;
      add(a,b,c),add(b,a,c);
      es[i].f=true;
    }
  }
  //以每个点为根找到其与其他点之间的最远距离?
  for(int i=1;i<=n;i++) dfs(i,-1,-1e9,-1e9,d1[i],d2[i]);
  
  //debug dfs找任意两点之间路径最长边和次长边
  //for(int i=1;i<=n;i++){
  //    cout<<i<<":"<<endl;
  //    int k=h[i];
  //    for(int j=k;~j;j=ne[j]){
  //        cout<<e[j]<<","<<d1[i][e[j]]<<","<<d1[i][e[j]]<<" ";
  //    }
  //    cout<<endl;
  //}
  //为什么debug代码打完就过了?
  
	int res=1e18;
  for(int i=0;i<m;i++){
    bool f=es[i].f;
    int a=es[i].x,b=es[i].y,c=es[i].z;
    if(!f){
      if(c>d1[a][b])
				res=min(res,sum+c-d1[a][b]);
      else if(c>d2[a][b]){
        res=min(res,sum+c-d2[a][b]);
      }
    }
  }
  cout<<res<<endl;
  return 0;
}

后台测试样例

4 4
1 2 1
2 3 2
3 4 1
2 4 2

5
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值