题解 P4180 【【模板】严格次小生成树[BJWC2010]】

· · 题解

然鹅刚刚有同学指出我的代码有bug,样例都过不了(然鹅还是AC了我也是...) 于是看了看,确实某个地方有点草率,于是把自己修改正确了的代码放到第一个,错的第二个,还有另一个放在第三个。。。

(楼下一堆大佬有没有出错的我就不知道了,但是貌似出错的程序也是能过的)。于是来发篇程序里头注解比较多的题解 。如果是那种思路的话还是看楼下大佬的吧,我的题解一般来讲是帮助童鞋们理解程序构造云云的...(QAQ 不喜勿喷)

但是这次还是好好分析一下吧(哪怕是按着代码思路来一遍)。

首先的首先,这个blog大家可以先看看(对于次小生成树的分析比我详细,看完这篇blog再看本蒟蒻的题解效果更佳。QAQ_(:з」∠)_):

某大佬的blog




首先的话,想必做到这题的童鞋都知道最小生成树mst怎么写了吧。
那么其实次小生成树也是可以分为两种的(严格和非严格),怎么说呢...
as we all know,mst是指一个图中所有包含全部顶点的树中边权和最小的树,
那么我们所说的严格次小生成树就是边权之和严格大于(也就是不能等于),但是又是满足该条件的生成树里边权之、和最小的树。
比如说某图的最小生成树的边权和为88,还有其他的几个生成树的边权和分别为88、88、90、100、103、111,
那么边权和为90的生成树就是该图的严格次小生成树,
而另外另一棵边权和为88的生成树就是该图的非严格次小生成树(此时这棵树的边权和 与 mst的边权和相等了呀)。
那么易知当一个图的mst唯一时,该图的严格次小生成树与非严格次小生成树就区别不大了。

那么怎么求一个图的次小生成树捏?以下是步骤:
1.首先读边什么的就不说了,那么第一步就是用kruskal跑一遍mst,并且把用到的边打上标记,然后还要记录下mst的边权和;
2.其次再把这些打上标记的边存到该边指向的两个顶点的后面(两个顶点都要存),以便接下来深搜建树用。
3.深搜建树,并维护好当前节点的一系列信息(比如说当前点祖先的信息啦、从当前点到某个祖先的路径中的最大边权和次大边权啦之类的)
4.再然后就是枚举无用边(最小树中没有用到的边),然后倍增找当前枚举到的边所指向的两个端点的lca并且求出路径中最大边权和次大边权,
当前枚举到的边减去找出的最大(或许是次大)的边权就是该边的贡献,然后最小贡献记录进ans里。
5.printf 边权和res + (非零的)最小贡献ans ,完毕
//by Judge
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstdio>
typedef long long ll;
using namespace std;
const int M=1e5+100;
ll n,m,res,ans=0x3f3f3f3f,mx;
int f[M],fa[25][M],dep[M];
ll d[2][25][M];
bool used[3*M],vis[M];
vector<int> a[M];
struct Edge{
    int from, to;
    ll val;
    bool operator < (const Edge y){
        return val < y.val;
    }
}e[3*M];
int F(int x){
    if(f[x]==x) return x;
    return f[x]=F(f[x]);
}
void kruskal(){ //kruskal 算最大生成树(已保证任意两点之间最小限重最优)
    sort(e,e+m); int lef=n-1;
    for(int i=1;i<=n;++i) f[i]=i;
    for(int i=0;i<m && lef;++i){
        int x=F(e[i].from),y=F(e[i].to);
        if(x!=y){
            f[x]=y; res+=e[i].val;
            used[i]=1; --lef;
            mx=max(mx , e[i].val);
        }  
    }
}
void dfs(int x){    //深搜建树(可能不止一棵,因为数据未保证是连通图) 
    vis[x]=true;
    for(int i=1;i<=23;++i){
        fa[i][x]=fa[i-1][fa[i-1][x]];
        ll t1=d[0][i-1][x], t2=d[0][i-1][fa[i-1][x]];
        d[0][i][x]=max(t1 , t2);
        d[1][i][x]=max(d[1][i-1][x] , d[1][i-1][fa[i-1][x]]);
        if(t1!=t2) d[1][i][x]=max(d[1][i][x] , min(t1 , t2));
    }
    for(int i=0;i<a[x].size();++i){
        int t=e[a[x][i]].to+e[a[x][i]].from-x;
        if(vis[t]) continue;    //vis为1表示是父节点 
        dep[t]=dep[x]+1; fa[0][t]=x;
        d[0][0][t]=e[a[x][i]].val; dfs(t);
    }
}
int lca(int u,int v){
    if(dep[u]<dep[v])
        swap(u,v);
    if(dep[u]!=dep[v]){     //将深度做相等
        for(int i=23,h=dep[u]-dep[v];i>=0;--i)
        if(h&(1<<i)) u=fa[i][u];
    }
    if(u==v) return u;  //如果已经在一个节点上就直接返回 
    for(int i=23;i>=0;--i) if(fa[i][u]!=fa[i][v])
        u=fa[i][u] , v=fa[i][v];
    return fa[0][u];
}
ll get(int u,int v,int c){
    int fht=lca(u,v);
    ll m1=0,m2=0;
    for(int i=23,h1=dep[u]-dep[fht],h2=dep[v]-dep[fht];i>=0;--i){
        if(h1&(1<<i)){
            if(d[0][i][u]>m1) m2=m1,m1=d[0][i][u];
            else if(d[0][i][u]>m2) m2=d[0][i][u];
            else m2=max(m2 , d[1][i][u]);
        } if(h2&(1<<i)){
            if(d[0][i][v]>m1) m2=m1,m1=d[0][i][v];
            else if(d[0][i][v]>m2) m2=d[0][i][v];
            else m2=max(m2 , d[1][i][v]);
        }
    } if(m1==c) return c-m2;
    else return c-m1;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;++i){
        int u,v; ll w;
        scanf("%d%d%lld",&u,&v,&w);
        e[i].from=u;
        e[i].to=v;
        e[i].val=w;
    } kruskal();
    for(int i=0;i<m;++i) if(used[i]){
        a[e[i].from].push_back(i);
        a[e[i].to].push_back(i);
    } dep[1]=1; dfs(1);
    for(int i=0;i<m;++i) if(!used[i]){
        if(e[i].val-mx>ans) break;
        ll t=get(e[i].from , e[i].to , e[i].val);
        ans=min(ans , t);
    } return printf("%lld\n",res+ans),0;
}

原来错的我也不删了,同学们可以开个文本比对看看哪里不一样

//by Judge
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstdio>
typedef long long ll;
using namespace std;
const int M=1e5+100;
ll n,m,res,ans=0x3f3f3f3f,mx;
int f[M],fa[25][M],dep[M];
ll d[2][25][M];
bool used[3*M],vis[M];
vector<int> a[M];
struct Edge{
    int from, to;
    ll val;
    bool operator < (const Edge y){
        return val < y.val;
    }
}e[3*M];
int F(int x){  //并查集标准模板 
    if(f[x]==x) return x;
    return f[x]=F(f[x]);
}
void kruskal(){ //kruskal 算mst, 基本就是模板
    sort(e,e+m); int lef=n-1;
    for(int i=1;i<=n;++i) f[i]=i;
    for(int i=0;i<m && lef;++i){
        int x=F(e[i].from),y=F(e[i].to);
        if(x!=y){
            f[x]=y; res+=e[i].val;
            used[i]=1; --lef;
            mx=max(mx , e[i].val);  //这里处理出的mx作剪枝用
        }  
    }
}
void dfs(int x){    //深搜建树,从1开始
    vis[x]=true;
    for(int i=1;i<=23;++i){
        fa[i][x]=fa[i-1][fa[i-1][x]];
        ll t1=d[0][i-1][x], t2=d[0][i-1][fa[i-1][x]];
        d[0][i][x]=max(t1 , t2);
        d[1][i][x]=max(d[1][i-1][x] , d[1][i-1][fa[i-1][x]]);
        if(t1!=t2) d[1][i][x]=max(d[1][i][x] , min(t1 , t2));
    }
    for(int i=0;i<a[x].size();++i){
        int t=e[a[x][i]].to+e[a[x][i]].from-x;
        if(vis[t]) continue;    //vis为1表示是父节点 
        dep[t]=dep[x]+1; fa[0][t]=x;
        d[0][0][t]=e[a[x][i]].val; dfs(t);
    }
}
int lca(int u,int v){
    if(dep[u]<dep[v])
        swap(u,v);
    if(dep[u]!=dep[v]){     //将深度做相等
        for(int i=23,h=dep[u]-dep[v];i>=0;--i)
        if(h&(1<<i)) u=fa[i][u];
    }
    if(u==v) return u;  //如果已经在一个节点上就直接返回 
    for(int i=23;i>=0;--i) if(fa[i][u]!=fa[i][v])
        u=fa[i][u] , v=fa[i][v];
    return fa[0][u];
}
ll get(int u,int v,int c){
    int fht=lca(u,v);
    ll m1=-1,m2=-1; //m1是最大边权,m2是次大边权 
    // 倍增求最大边权和次大边权 
    for(int i=23,h1=dep[u]-dep[fht],h2=dep[v]-dep[fht];i>=0;--i){
        if(h1&(1<<i)){
            if(d[0][i][u]>m1) m2=m1,m1=d[0][i][u];
            m2=max(m2 , d[1][i][u]);
        }
        if(h2&(1<<i)){
            if(d[0][i][v]>m1) m2=m1,m1=d[0][i][v];
            m2=max(m2 , d[1][i][v]);
        }
    }
    if(m1!=c) return c-m1;
    //最大边权和当前关键边的权值相等返回次大边权的贡献(因为本题的次小生成树是严格的) 
    if(m2!=-1) return c-m2; //不然直接返回最大边权的贡献(这里是hack的关键哦,因为m1==c且m2==-1时代表该环只有一种权值)
    //另外这个分函数里也可以把-1都改成0,别问我为什么,数据水你问数据去。
    return 0; //不然嘛...返回个0(当然很明显,无解的情况也能由此判断,加些语句就ok了) 
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;++i){ //读边 
        int u,v; ll w;
        scanf("%d%d%lld",&u,&v,&w);
        e[i]=(Edge){u , v , w};
    }
    kruskal();  //跑一边最小生成树 
    for(int i=0;i<m;++i) if(used[i]){  //记录下mst用到的边,分别加到两个点后面 
        a[e[i].from].push_back(i);
        a[e[i].to].push_back(i);
    }
    dep[1]=1; dfs(1);  //深搜根节点(1) 
    for(int i=0;i<m;++i) if(!used[i]){ //然后枚举无用边作为关键边 
        if(e[i].val-mx>ans) break; //剪枝,贡献不可能再小的时候直接break 
        ll t=get(e[i].from , e[i].to , e[i].val); //求出贡献 
        if(t) ans=min(ans , t); //最小贡献存进ans 
    }
    printf("%lld\n",res+ans);  //输出最小生成树的边权和加上(非零的)最小贡献就是次小生成树 
    return 0;
}

然后发一波老师被我hack掉的代码~ (滑稽)

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
const int maxn=100010,maxm=300010,maxk=22;
using namespace std;
typedef long long ll;
int n,m,pre[maxm],now[maxn],son[maxm],val[maxm],delta=(int)1e9+7,tot;ll ans;
int fa[maxn][maxk],fim[maxn][maxk],sem[maxn][maxk],dep[maxn],f[maxn];bool in[maxm];
struct Edge{int x,y,v;}E[maxm];
bool operator <(Edge a,Edge b){return a.v<b.v;} 
void add(int a,int b,int c){pre[++tot]=now[a],now[a]=tot,son[tot]=b,val[tot]=c;}
int getfa(int x){return f[x]==x?x:f[x]=getfa(f[x]);}
void dfs(int x){
    for (int i=1;i<=18;i++){
        fa[x][i]=fa[fa[x][i-1]][i-1];
        int t1=fim[x][i-1],t2=fim[fa[x][i-1]][i-1];
        fim[x][i]=max(t1,t2);
        sem[x][i]=max(sem[x][i-1],sem[fa[x][i-1]][i-1]);
        if (t1!=t2) sem[x][i]=max(sem[x][i],min(t1,t2));
    }
    for (int y=now[x];y;y=pre[y]) if (son[y]!=fa[x][0]){
        dep[son[y]]=dep[x]+1,fa[son[y]][0]=x;
        fim[son[y]][0]=val[y],dfs(son[y]);
    }
}

void kruskal(){
    sort(E+1,E+1+m);int cnt=0;
    for (int i=1;i<=n;i++) f[i]=i;
    for (int i=1;i<=m&&cnt<n-1;i++){
        int x=E[i].x,y=E[i].y,v=E[i].v;
        if (getfa(x)==getfa(y)) continue;
        cnt++,f[getfa(x)]=getfa(y),ans+=v,in[i]=1;
        add(x,y,v),add(y,x,v);
    }
}

int lca(int x,int y){
    if (dep[x]<dep[y]) swap(x,y);
    for (int h=dep[x]-dep[y],i=18;i>=0&&h;i--) if (h&(1<<i)) x=fa[x][i];
    if (x==y) return x;
    for (int i=18;i>=0;i--)
        if (fa[x][i]!=fa[y][i])
            x=fa[x][i],y=fa[y][i];
    return fa[x][0];
}

void query(int x,int u,int v){
    int max1=0,max2=0;
    for (int i=18,h=dep[x]-dep[u];i>=0;i--) if (h&(1<<i)){
        if (fim[x][i]>max1) max2=max1,max1=fim[x][i];
        max2=max(max2,sem[x][i]),h-=(1<<i);
    }
    if (v==max1) delta=min(delta,v-max2);
    else delta=min(delta,v-max1);
}

void solve(int id){
    int x=E[id].x,y=E[id].y,v=E[id].v,u=lca(x,y);
    query(x,u,v),query(y,u,v);
}

int main(){
    scanf("%d%d",&n,&m);
    for (int i=1,x,y,z;i<=m;i++)
        scanf("%d%d%d",&x,&y,&z),E[i]=(Edge){x,y,z};
    kruskal(),dfs(1);
    for (int i=1;i<=m;i++) if (!in[i]) solve(i);
    printf("%lld\n",ans+delta);
    return 0;
}

这就hack了?如果你不信的话这里有组数据:

input:复制(手动)

5 6
1 2 3
1 3 3
2 3 3
3 4 3
3 5 3
4 5 8

the answer: 粘贴(滑稽)

17

wrong answer:

15