【图论】MST及拓展

MST的三种解法

1>prim

2>kruskal

算法步骤
步骤一:T是边的集合,其初始状态为空;
步骤二:从原图剩余边中选取一条最小代价的边;
步骤三:看其是否与当前T中其它边构成环路;
步骤四:如果未构成环路,则加入T中;否则,丢弃该边;
步骤五:是否还有剩余边,如果有则返回步骤二,否则,程序结束。

算法证明(来自网络)

(1)Kruskal算法一定能得到一个生成树;
(2)该生成树具有最小代价。

=》
(1)假设该算法得到的不是生成树,根据树的定义,就有两种情形,第一是得到的图是有环的,第二就是得到的图是不连通的。由于算法要求每次加入边都是无环的,所以第一种情形不存在,下面就只需证明第二种情形不存在即可。
假设得到的图是不连通的,则至少包含两个独立的的边集,假设其中一个为E,则E中边对应的所有点都无法到达其它边集对应的点
(否则,根据算法定义,相应的联系边应被加入树中),而这与原图是连通的产生矛盾,因此,第二种情形也不可能存在。得证。

(2)假设图有n个顶点,则生成树一定具有n-1条边.由于图的生成树个数是有限的,所以至少有一棵树具有最小代价,假设该树为U。
先做出如下假设:
1)得到的树为T。
2)U和T中不同的边的条数为k,其它n-1-k条边相同,这n-1-k条边构成边集E。;
3)在T中而不在U中的边按代价从小到大依次为a1,a2,...,ak。
4)在U中而不在T中的边按代价从小到大依次为x1,x2,...,xk。
现在我们通过把U转换为T(把T的边依次移入U中),来证明U和T具有相同代价。
首先,我们将a1移入U中,由于U本身是一棵树,此时任意加一条边都构成回路,所以a1的加入必然产生一条回路,且这条回路必然包括x1,x2,...,xk中的边。
(否则a1与E中的边构成回路,而E也在T中,这与T中无回路矛盾。)
在这个回路中删除属于x1,x2,...,xk且代价最大边xi构成一个新的生成树V。
假设a1代价小于xi,则V的代价小于U,这与U是最小代价树矛盾,所以a1不可能小于xi。
假设a1大于xi,按照Kruskal算法,首先考虑代价小的边,则执行Kruskal算法时,xi应该是在a1之前考虑,而a1又在a2,...,ak之前考虑,所以考虑xi之前,T中的边只能是E中的边,而xi既然没加入树T,就说明xi必然与E中的某些边构成回路,但xi和E又同时在U中,这与U是生成树矛盾,所以a1也不可能大于xi。
因此,新得到的树V与T具有相同代价。
依次类推,把a1,a2,...,ak的边逐渐加到U中,最终得到的树(T)与U代价相同。
证明结束。

3>破圈 这个容易超时

<1>最小生成树计数

kruskal+并查集

想象:
把图分成两部分,一部分是环,一部分是链

从破圈法反过来:
一个环,假设是1-2-3(-1)
我现在破圈,就是找里面最大的除去,
但是K,就是找前两条小的边

然后链,因为不连通,所以都要加上

那么如果我想在这个过程中加边,
就是一个环中多个边的值一样,选择多样,
然后找个办法,找出一环环,环上环的选择数

据说这是一个定理
1.不同的最小生成树中,每种权值的边出现的个数是确定的。
2.不同的生成树中,某一种权值的边连接完成后,形成的联通块状态是一样的 。

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
using namespace std;
int n,m;
const int N=103,M=1003,mod=31011;
struct node
{
    int u,v,w;
    bool operator < (const node & o) const
    { return w<o.w; }
} sd[M];
struct nd
{
    int l,r;
    int tm,val;
}dd[M];
int fa[N];
int find(int x)
{ return !fa[x] ?x : find(fa[x]); }//这道题不能路径压缩,???? 

int sum,goal,r;
void dfs(int nw,int kk)
{
    if(kk==goal)
    {
        sum++;
        return ;
    }
    if(nw>=r) return ;
    else
    {
        int fu=find(sd[nw].u ),fv=find(sd[nw].v );
        if(fu!=fv) 
            fa[fu]=fv,dfs(nw+1,kk+1),fa[fu]=fa[fv]=0;
    }
    dfs(nw+1,kk);
}

int ans;
int main()
{    
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++)
        scanf("%d%d%d",&sd[i].u ,&sd[i].v ,&sd[i].w );
    sort(sd,sd+m);
    
    int cnt=1,pos=0;
    for(int i=0;i<m;i++)//我在这放了一个cnt,然后去掉了不少可能的结果。。。 
    {
        if(sd[i].w !=dd[pos].val ) dd[++pos].val =sd[i].w ,dd[pos].l =dd[pos-1].r =i;
        int fu=find(sd[i].u ),fv=find(sd[i].v );
        if(fu^fv) 
            fa[fu]=fv,dd[pos].tm ++,cnt++;
    }dd[pos].r =m;
    if(cnt^n)
    {
        printf("0
");
        return 0;
    }
    else ans=1;
    
    memset(fa,0,sizeof(fa));
    for(int i=1;i<=pos;i++)
    {
        if(!dd[i].tm ) continue;
        //if(dd[i].r -dd[i].l ==dd[i].tm ) continue;//我这里计算,想减掉枝,忘了让fa更新,然后一直wa 
        sum=0,goal=dd[i].tm ,r=dd[i].r ;
        dfs(dd[i].l ,0);
        ans=ans*sum%mod;
        for(int j=dd[i].l ;j<dd[i].r ;j++)
        {
            int fu=find(sd[j].u ),fv=find(sd[j].v );
            if(fu!=fv) fa[fu]=fv;
        }
    }
    printf("%d",ans);
    
    return 0;
}

<1.5>新的开始

简单的加超级源点,然后建MST

<2>黑暗城堡

 https://loj.ac/problem/10064

简单最短路+生成树思维+乘法原理

解决了一个新的问题:最短路径生成树

#include<cstdio>
#include<cstdlib>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m;
const int N=1003;
int g[N][N],dis[N];
bool in[N];
queue <int> q;

struct node
{
    int d,xh;
    bool operator < (const node & o) const
    { return d<o.d; } 
}dd[N];

void SPFA()
{
    memset(g,0x3f,sizeof(g));
    memset(dis,0x3f,sizeof(dis));
    int u,v,w;
    while(m--)
        scanf("%d%d%d",&u,&v,&w),g[u][v]=g[v][u]=w;
    
    dis[1]=0,q.push(1);
    while(!q.empty() )
    {
        int u=q.front() ;q.pop() ;
        
        for(int i=2;i<=n;i++)
            if(dis[i]>dis[u]+g[u][i])
            {
                dis[i]=dis[u]+g[u][i];
                if(!in[i])
                    in[i]=true,q.push(i); 
            }
        in[u]=false;
    }
}

const long long mod=(1<<30)-1 +(1<<30);
long long ans=1;
int main()
{
    scanf("%d%d",&n,&m);
    SPFA();
    
    for(int i=1;i<=n;i++)
        dd[i].d =dis[i],dd[i].xh =i;
    sort(dd+1,dd+n+1);
    
    for(int i=2;i<=n;i++)
    {
        int u=dd[i].xh ;
        long long sum=0;
        for(int j=1;j<i;j++)
        {
            int v=dd[j].xh ;
            if(dis[u]==dis[v]+g[u][v]) sum++;
        }
        
        if(!sum)
        {
            printf("0
");
            return 0;
        }
        ans=sum%mod*ans%mod;
    }
    
    printf("%lld",ans);
    return 0;
}

我...又因为看错数据范围wa了

<3>北极通讯网络

好神奇的题,

n座村庄,每座村庄的坐标用一对整数 (x,y) 表示。

两种通讯工具:

无限的 无线电收发机,k台 卫星设备。

不同型号的无线电收发机有一个不同的参数 ,两座村庄之间的距离如果不超过 d就可以用该型号的无线电收发机直接通讯, d值越大的型号价格越贵。

拥有卫星设备的两座村庄无论相距多远都可以直接通讯。通讯有传递性。

求d的最小值

神仙思路:将点分为两种,一种普通点,一种神奇点。

神奇点之间的交流不花钱,但与普通点交流要钱,

你看这个神奇点,全部合起来像不像是一个点,

所以这其实是:求k个生成树,

或者换句话说:kruskal的时候,不求完,只做一半的工作

最小生成树的第k大边

#include<cstdio>
#include<cstdlib>
#include<queue>
#include<cmath>
using namespace std;
int n,k;
const int N=503;
int pz[N][2];

struct node
{
    int u,v;double w;
    bool operator < (const node & o) const
    { return w>o.w; }
    node(int uu,int vv,double ww)
    { u=uu,v=vv,w=ww; }
    node(){}
};
priority_queue <node> q;
double get_dis(int u,int v)
{
    double a=pz[u][0]-pz[v][0];
    double b=pz[u][1]-pz[v][1];
    return sqrt(a*a+b*b);
}

double ans;
int fa[N];
int find(int x)
{ return !fa[x]?x:fa[x]=find(fa[x]); }
void kruskal()
{
    if(k>=n)
    {
        printf("0.00
");
        return ;
    }
    
    int cnt=k;
    if(!k) cnt++;
    while(cnt<n)
    {
        node t=q.top() ;q.pop() ;
        int fu=find(t.u ),fv=find(t.v );
        if(fu==fv) continue;
        
        fa[fu]=fv,cnt++;
        ans=t.w ;
    }
    printf("%.2lf
",ans);
}

int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
        scanf("%d%d",&pz[i][0],&pz[i][1]);
    for(int i=1;i<=n;i++)
        for(int j=i+1;j<=n;j++) 
            q.push(node(i,j,get_dis(i,j))); 
    kruskal();
    
    return 0;
}

<4>构造完全图

给你看我傻乎乎的枚举判断+LCA倍增

最后只过了50分,tle掉了

#include<cstdio>
#include<cstdlib>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
const int N=1e5+3;
struct node
{
    int v,w;
    node(int vv,int ww)
    { v=vv,w=ww; }
    node(){}
};
vector <node> g[N];
int g_sz[N];
bool f[N];

int fa[N][19],d[N][19],dep[N];
void dfs(int nw,int pre)
{
    fa[nw][0]=pre;
    for(int i=1;i<19 && fa[nw][i-1];i++)
        fa[nw][i]=fa[fa[nw][i-1]][i-1],
        d[nw][i]=max(d[nw][i-1],d[fa[nw][i-1]][i-1]);
    
    dep[nw]=dep[pre]+1;
    g_sz[nw]=g[nw].size() ;
    for(int i=0;i<g_sz[nw];i++)
    {
        int v=g[nw][i].v ;
        if(v!=pre) d[v][0]=g[nw][i].w ,dfs(v,nw);
    }
}

int LCA(int u,int v)
{
    int mx=0;
    if(dep[u]<dep[v]) swap(u,v);
    int dis=dep[u]-dep[v];
    for(int i=18,j=1<<18;j;i--,j>>=1)
        if(dis&j) 
        {
            mx=max(mx,d[u][i]);
            u=fa[u][i];
        }
    if(u==v) return mx;
    
    for(int i=18;i>=0;i--)
        if(fa[u][i]!=fa[v][i])
        {
            mx=max(mx,max(d[u][i],d[v][i]));
            u=fa[u][i],v=fa[v][i];
        }
    return max(mx,max(d[u][0],d[v][0]));
}

inline int read()
{
    int x=0;char c=getchar();
    while(c<'0' || c>'9') c=getchar();
    while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
    return x;
}

long long ans;
int main()
{
    n=read();
    int u,v,w;
    for(int i=1;i<n;i++)
        u=read(),v=read(),w=read(),g[u].push_back(node(v,w)),g[v].push_back(node(u,w)),ans+=w;  
    dfs(1,0);
    
    for(int i=1;i<=n;i++)
    {
        memset(f,false,sizeof(f));
        for(int j=0;j<g_sz[i];j++)
            f[g[i][j].v ]=true;
        
        for(int j=1;j<i;j++)
            if(!f[j]) ans+=LCA(i,j)+1;
    }
    printf("%lld
",ans);
    return 0;
}
正解是这样的:

再次从kruskal的过程入手操作,一边连入最优边,一边在自己记录答案的小册子上,加上那些不优的边

对于当前这条边的2个端点u和v,所处的2个连通块U和V之间必然是还未连通的。

加入这条边之后2个才能够连通,

换言之,使得这2个连通块连通的,权值最小的边,就是当前的u和v边。

因此被排除的边就是Sum(U)*Sum(V) -1(U中取一个点,V中取一个点,然后排除u,v这条边),而这条边的权值就是w(u,v) + 1。

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
using namespace std;
int n;
const int N=1e5+3;
struct node
{
    int u,v,w;
    bool operator < (const node & o) const
    { return w<o.w; }
}g[N];

int fa[N],cnt[N];
int find(int x)
{return !fa[x]?x:fa[x]=find(fa[x]);}

long long ans=0;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<n;i++)
        scanf("%d%d%d",&g[i].u ,&g[i].v ,&g[i].w );
    sort(g+1,g+n);
    
    for(int i=1;i<=n;i++) cnt[i]=1;
    for(int i=1;i<n;i++)
    {
        int fu=find(g[i].u ),fv=find(g[i].v );
        
        ans+=1LL*cnt[fu]*cnt[fv]*(g[i].w +1);
        fa[fu]=fv,cnt[fv]+=cnt[fu];
    }
    
    printf("%lld
",ans-n+1);
    return 0;
}

然后正解:

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cstring>
using namespace std;
int n;
const int N=1e5+3;
struct node
{
    int u,v,w;
    bool operator < (const node & o) const
    { return w<o.w; }
}g[N];

int fa[N],cnt[N];
int find(int x)
{return !fa[x]?x:fa[x]=find(fa[x]);}

long long ans=0;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<n;i++)
        scanf("%d%d%d",&g[i].u ,&g[i].v ,&g[i].w );
    sort(g+1,g+n);
    
    for(int i=1;i<=n;i++) cnt[i]=1;
    for(int i=1;i<n;i++)
    {
        int fu=find(g[i].u ),fv=find(g[i].v );
        
        ans+=1LL*cnt[fu]*cnt[fv]*(g[i].w +1);
        fa[fu]=fv,cnt[fv]+=cnt[fu];
    }
    
    printf("%lld
",ans-n+1);
    return 0;
}

<5>tree

求恰好nd条白边时的最小生成树

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
inline int read()
{
    int x=0;char c=getchar();
    while(c<'0' || c>'9') c=getchar();
    while(c>='0'&&c<='9') x=(x<<3)+(x<<1)+c-'0',c=getchar();
    return x;
}
int n,m,nd;
const int N=5e4+3,M=1e5+3;
int uu[M],vv[M],ww[M],cc[M];
struct node
{
    int u,v,w;bool c;
    bool operator < (const node & o) const
    { return w==o.w ?c<o.c :w<o.w; }//我现在还是很奇怪,为什么这个找的时候,用的是int不是double类 
}g[M];

int fa[N],ans=0;;
int find(int x) { return !fa[x] ?x :fa[x]=find(fa[x]); }
int kruskal(int add)
{
    for(int i=0;i<m;i++)
    {
        g[i].u =uu[i],g[i].v =vv[i],g[i].w =ww[i],g[i].c =cc[i];
        if(!g[i].c ) g[i].w +=add;
    }sort(g,g+m);
    
    ans=0;
    int cnt=1,sum=0;
    memset(fa,0,sizeof(fa));
    for(int i=0;cnt<n;i++)
    {
        int fu=find(g[i].u ),fv=find(g[i].v );
        if(fu==fv) continue;
        
        fa[fu]=fv,cnt++;
        ans+=g[i].w ;
        if(!g[i].c ) sum++;
    }
    
    if(sum==nd)
    {
        printf("%d
",ans-nd*add);
        exit(0);
    }
    else return sum;
}

int t_ans;
int main()
{
    n=read(),m=read(),nd=read();
    for(int i=0;i<m;i++) 
        uu[i]=read(),vv[i]=read(),ww[i]=read(),cc[i]=read(),uu[i]++,vv[i]++;
    //两种题型:一种求l(恰好成立的那个变量),一种求ans(恰好成立后的结果)
    //两种写法,1)l<r,r=mid 2)l<=r,r=mid-1 
    int l=-103,r=103;
    while(l<=r)
    {
        int mid=(l+r)>>1;
        if(kruskal(mid)>nd) l=mid+1,t_ans=ans-nd*mid;//这里防止因为边的排列问题,导致在一片等长的边中,先选了白边 
        else r=mid-1;
    }
    printf("%d
",t_ans);
    
    return 0;
} 
原文地址:https://www.cnblogs.com/xwww666666/p/11476958.html