次小生成树 (NKOJ-2754)

首先,我要感谢南开中学某位大佬的题解,没有那个大佬的题解,我根本不可能做出这道题,更不可能写出这篇题解,网上的题解我都看不懂,但她的我却看得懂。题在:http://oi.nks.edu.cn/zh/Problem/Details/2754,她的题解在那个下方的讨论中(%%%).

然后,这是我对这道题的理解。首先这道题我们要用到回路性质即次小生成树一定能由最小生成树换一条边得到。想到这里,我们一定要先跑一遍最小生成树,把其边权和记录下来,把最小生成树的所有边记下来,方便建树,并把它们标记下来。这一步无论是暴力还是好方法,都要有。然后,我们再判断能不能生成次小生成树,若m<=n-1,或连最小生成树都不能生成,那么我们就输出"Keng Die!"(不能生成次小生成树)。如果可以生成次小生成树,那么暴力的时间是O(n*m*log(n)或O(n*n+m),第二zhong是先预处理出所有的u到v的路径之中的最长的一条边的长度,然而这就需要倍增和LCA了,求出pre[i][j]和maxx[i][j],其中pre[i][j]表示i的2^j级父亲,maxx[i][j]表示i到i的2^级父亲的路径中最长的一条边的长度。但是,我们可以发现,我们要枚举的只是非最小生成树边的两个端点,即把n*n优化为m-n+1,而用这条边来替代一条边,替代的肯定是两个端点在最小生成树的最短路径中最长的一条边,用LCA和倍增来求。这样一来,我们算法就变成了O(m*log(n)+(m-n+1)*log(n))即O( (2m-n)*log(n) ),这样一来,就不会超时了。


#include<cstdio>
#include<algorithm>
#include<vector>
#include<cmath>
using namespace std;
struct edge{
   int from,to;
   int len;
}a[200005];
struct node{
   int to;
   int dis;
};
vector<node>son[100005];
int cmp(edge b,edge c){
     return b.len<c.len;
}
int n,m,sum,z;
int maxx[100005][20],pre[100005][20];
int depth[100005],vis[100005];//depth[i]表示i所在的层数,
//vis记录这条边是不是最小生成树中的一条边,其中1表示是
int p[200005],father[100005];//father表示所在子集的根
int find(int x){//并查集寻根操作
   if(x!=father[x])father[x]=find(father[x]);
   return father[x];
}
void unionn(int x,int y){//并查集合并操作
    x=find(x);y=find(y);
    father[y]=x;
}
int solve(){//求其最小生成树
    sort(a+1,a+m+1,cmp);
    int x,y,edges;
    for(int i=1;i<=m;i++){
        x=a[i].from,y=a[i].to;
        if(find(x)!=find(y)){
          edges++;sum+=a[i].len;
          vis[i]=1;unionn(x,y);
          son[a[i].from].push_back((node){a[i].to,a[i].len}); 
          son[a[i].to].push_back((node){a[i].from,a[i].len}); 
  //如果它是最小生成树的一条边,那么我们就把它下了,方便建树
          if(edges==n-1)return 1;
        }
    }
    return 0;
}
void buildtree(){             //建树,其中我们用数组来模拟队列(用空间换时间)。(以1为根建树)用宽搜搜出每个结点所在的层数,然后用DP(也可以说是倍增)求出now的2^i级父亲,和now到now的2^i级父亲的路径中最长的一条边的长度
    int x,top=1,now;        //now表示当前所在节点
    p[1]=1; depth[1]=1;
    while(top){
        now=p[top--]; 
        depth[now]=depth[pre[now][0]]+1;
        for(int i=1;(1<<i)<depth[now];i++){       //倍增(DP)预处理出now的2^i级父亲,和now到now的2^i级父亲的路径中最长的一条边的长度
            pre[now][i]=pre[pre[now][i-1]][i-1]; 
            maxx[now][i]=max(maxx[now][i-1],maxx[pre[now][i-1]][i-1]); 
        } 
        for(int i=0;i<son[now].size();i++){            //把它的儿子结点都入队
            x=son[now][i].to; 
            if(x==pre[now][0])continue; 
            pre[x][0]=now; 
            maxx[x][0]=son[now][i].dis; 
            p[++top]=x; 
        }    
    }


int LCA(int u,int v){           
//求出u到v的路径中最长的那条边的长度
    if(depth[u]<depth[v])swap(u,v);
    int ans=0,t=depth[u]-depth[v];           //ans表示u到v的路径中最长的那条边的长度
    for(int i=0;(1<<i)<=t;i++){                  //把他们统一到同一高度 
        if(t&(1<<i)){
           ans=max(ans,maxx[u][i]);
           u=pre[u][i];
        }
    }
    if(u==v)return ans;
    for(int i=log(depth[u]);i>=0;i--){          //现在我们统一到了统一深度,就可以开始倍增往最近公共祖先靠近(它们两个之间的路径一定是从u到它们的最近公共祖先,然后再到v,这里我们是u,v同时往v靠近)
        if(pre[u][i]!=pre[v][i]){ 
            ans=max(ans,max(maxx[u][i],maxx[v][i])); 
            u=pre[u][i]; 
            v=pre[v][i]; 
        } 
    } 
    ans=max(ans,max(maxx[u][0],maxx[v][0])); //最后再到它们的最近公共祖先
    return ans;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&a[i].from,&a[i].to,&a[i].len);
    }
    for(int i=1;i<=n;i++)father[i]=i; 
    if(m<n||solve()==0){//表示没有次小生成树
      printf("Keng Die!");
      return 0;
    }
    buildtree();
    int ans=1111111111;
    for(int i=1;i<=m;i++){
        if(vis[i]==0)ans=min(ans,a[i].len-LCA(a[i].from,a[i].to));
    }
    printf("%d",sum+ans);
    return 0;
}

原文地址:https://www.cnblogs.com/c201904xyorz/p/9990784.html