Kruskal重构树

一、最小生成树 & Kruskal

  最小生成树:将无向连通图的一些边删掉,使得剩下的点和边构成一棵树,且这棵树的边权总和最小,这样的树称为最小生成树。

 

  Kruskal:每次选不属于同一生成树的且权值最小的边的顶点,将边加入生成树,并将所在的2个生成树合并,直到只剩一个生成树。复杂度 O( m log m )。

 

 

#include <bits/stdc++.h>
using namespace std;
struct node {int a,b,Len;}A[100005];
bool cmp(node x,node y) {return x.Len<y.Len;}
int n,m,Father[100005],x,y,k,Cnt,ans;
int getFather(int x)
{
    if(x==Father[x]) return Father[x];
    else {Father[x]=getFather(Father[x]);return Father[x];}
}
int main()
{
    scanf("%d%d",&n,&m);
    for(register int i=1;i<=m;++i) scanf("%d%d%d",&A[i].a,&A[i].b,&A[i].Len);
    for(register int i=1;i<=n;++i) Father[i]=i;
    sort(A+1,A+m+1,cmp);
    while(Cnt<n-1)
    {
        ++k,x=getFather(A[k].a),y=getFather(A[k].b);
        if(x!=y) Father[y]=x,ans+=A[k].Len,++Cnt;
    }
    printf("%d",ans);
}
Kruskal

 



 


二、Kruskal 重构树

  1. 过程: 按照kruskal规则连边,对于要连接的边( a , b ),边权为c,令 x = getFather ( a )  , y = getFather ( b ) 。 若 x != y , 新建一个节点 z 作为 x 和 y 的父亲,z 的点权为边 ( a  , b  ) 的边权,即 father [ x ] = z , father [ y ] = z , Val [ z ] = c ;

  2. 性质

    (1). 重构树是节点总数为 2n-1 的 二叉树

    (2). 重构树是一个根堆(最小生成树 -- 大根堆,最大生成树 -- 小根堆);

    (3). 重构树 ( 按最小生成树重构 ) 中任意两点 a , b 的路径中的最大边权为它们LCA ( a  , b ) 的点权。也是原图中,a , b 路径中最大边 权的最小值 ;

        重构树 ( 按最大生成树重构 ) 中任意两点 a , b 的路径中的最 小边权为它们LCA ( a , b ) 的点权。也是原图中,a , b 路径中最小边权的最大值 ;

    (4). 如果原图不连通,会得到重构树森林;

    (5). 只有叶子节点是原图中的点,其他非叶节点代表的是原图中的边。

  3. 实现(见例题一)。

 



 

三、例题

 

(一)、NKOJ-2495【NOIP2013-D1T3】货车运输

问题描述:

A国有n座城市,编号从1到n,城市之间有m条双向道路。
每一条道路对车辆都有重量限制,简称限重。
现在有q辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。

输入格式:

第一行有两个用一个空格隔开的整数 n,m,表示 A 国有n座城市和m条道路。
接下来m行,每行3个整数x,y,z,表示从x号城市到y号城市有一条限重为z的道路(x≠y,两座城市之间可能有多条道路)
接下来一行有一个整数 q,表示有 q 辆货车需要运货。
接下来q行,每行两个整数x,y,之间用一个空格隔开,表示一辆货车需要从x城市运输货物到y城市(x≠y)。

输出格式:

输出共有 q 行,每行一个整数,表示对于这一辆货车,它的最大载重是多少。
如果货车不能到达目的地,输出-1

数据范围:

对于 30%的数据,1<=n<=10001<=m<=100001<=q<=1000;
对于 60%的数据,1<=n<=10001<=m<=500001<=q<=1000;
对于 100%的数据,1<=n<=1000001<=m<=5000001<=q<=300000<=z<=100000

 

  这个就是个板题,按最大生成树建重构树,LCA ( x ,  y ) 的权值即为 x 到 y 路径上最大载重的最小值。

Code:

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
bool Mark[200005];
int n,m,q,Fa[100005],K,tot(1),Father[100005][25],Deep[100005],pos,V[200005],Cnt,Head[200005],Next[200005],To[200005]; 
struct node {int x,y,z;}E[500005];
bool cmp(node x,node y) {return x.z>y.z;}
#define gc (p1==p2&&(p2=(p1=buf)+fread(buf,1,65536,stdin),p1==p2)?EOF:*p1++)
char buf[65536],*p1,*p2;
inline int read()
{
    char ch;int x(0);
    while((ch=gc)<48);
    do x=x*10+ch-48;while((ch=gc)>=48);
    return x;
}
inline int GetFa(int x)
{
    if(Fa[x]==x) return x;
    return Fa[x]=GetFa(Fa[x]);
}
inline void ADD(int x,int y) {Next[++Cnt]=Head[x],Head[x]=Cnt,To[Cnt]=y;}
inline void DFS(int x,int fa)
{
    Mark[x]=1,Deep[x]=Deep[fa]+1,Father[x][0]=fa;
    for(register int i=0;Father[x][i];++i) Father[x][i+1]=Father[Father[x][i]][i];
    for(register int i=Head[x];i;i=Next[i]) DFS(To[i],x);
}
inline int LCA(int u,int v)
{
    if(Deep[u]>Deep[v]) swap(u,v);
    for(register int i=20;i>=0;--i) if (Deep[u]<=Deep[v]-(1<<i)) v=Father[v][i];
    if(u==v) return u;
    for(register int i=20;i>=0;--i) if(Father[u][i]!=Father[v][i]) u=Father[u][i],v=Father[v][i];
    return Father[u][0];
}
int main()
{
    n=pos=read(),m=read(),V[0]=-1;
    for(register int i=1;i<=2*n;++i) Fa[i]=i;
    for(register int i=1;i<=m;++i) E[i].x=read(),E[i].y=read(),E[i].z=read();
    sort(E+1,E+m+1,cmp);
    while(tot<n&&K<m)
    {
        int x,y;x=GetFa(E[++K].x),y=GetFa(E[K].y);
        if(x!=y) V[++pos]=E[K].z,Fa[x]=pos,Fa[y]=pos,ADD(pos,x),ADD(pos,y),++tot;
    }
    for(register int i=pos;i;--i) if(!Mark[i]) DFS(i,0);
    q=read();
    for(register int i=1,x,y;i<=q;++i) x=read(),y=read(),printf("%d\n",V[LCA(x,y)]);
    return 0;
}
货车运输

 


 

(二)、NKOJ-8437  删边

问题描述:

一个n个节点 m 条带权边的无向连通图。
有 q 次询问,每次询问图中 ki 个互不相同的点,你可以选择一个数 x,然后将图中所有边权小于等于 x 的边删除。求当删除这些边后 ki 个点互不连通时,求 x 的最小值。
注意,本题强制在线

输入格式:

第一行三个整数 n,m,q,表示无向连通图有 n 个节点,m 条边,q 次询问。
接下来 m 行,每行三个整数 u,v,w,表示 u,v 之间有一条边权为 w 的无向边。
接下来 q 行,每行第一个整数为 ki,表示第 i 次询问有 ki 个点。
之后有 ki 个整数 a′1,a′2,...,a′ki,真实询问的点 aj 为 aj′ 按位异或上 lastans 后的值。
lastans 为上一次询问的答案,初始时 lastans=0

输出格式:

共 q 行,每行一个整数表示每次询问的答案。

数据范围:

 

   首先,要删除小于等于 x 的边,并且 x 要尽量小,所以按最大生成树重建。

   但答案是什么呢?

 

  对于 a , b 两点,答案显然是 LCA( a , b ) ,但k个数呢,是 k 个数的 LCA吗???

  显然不是(反例很好构造),答案应该是K个数两两LCA权值的最大值,这样会计算k2次,q次询问,喜提TLE。

  

  在重构树上,相邻两点的Lca的点权就是这两点直接所连的边的边权。因为是按照最大生成树建立的重构树,本质是小根堆。”距离”越远的两点,其Lca的深度可能越小,对应的点权也越小。那怎么确定两点的相邻关系的远近?

 

  D F S 序!DFS序越接近的点,在树中越相邻。

 

  所以:

    (1)求出重构树的DFS序;

    (2)对于第i次询问的ki个点,将它们放入容器A(A是vector),然后按DFS序排序。

    (3)询问的答案 Ans [ i ]  = max { V [ Lca (A1 ,A2 ) ], V [ Lca (A2, A3) ] , … , V [ Lca (Ak-1, Ak) ] } V [ x ] 为重构树中,x号点的点权 , A1,A2,…,Ak 为按DFS序排序后的 k 个点。

 

  时间复杂度 O( log n * ∑ k )

 

Code:

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
bool Mark[2000005];
int n,m,q,Fa[2000005],TOT,Left[2000005],K,tot(1),Father[2000005][25],Deep[2000005],pos,V[2000005],Cnt,Head[2000005],Next[2000005],To[2000005],Q[2000005]; 
struct node {int x,y,z;}E[2000005];
bool cmp1(node x,node y) {return x.z>y.z;}
bool cmp2(int x,int y) {return Left[x]<Left[y];}
#define gc (p1==p2&&(p2=(p1=buf)+fread(buf,1,65536,stdin),p1==p2)?EOF:*p1++)
char buf[65536],*p1,*p2;
inline int read()
{
    char ch;int x(0);
    while((ch=gc)<48);
    do x=x*10+ch-48;while((ch=gc)>=48);
    return x;
}
inline int GetFa(int x) {if(Fa[x]==x) return x;return Fa[x]=GetFa(Fa[x]);}
inline void ADD(int x,int y) {Next[++Cnt]=Head[x],Head[x]=Cnt,To[Cnt]=y;}
inline void DFS(int x,int fa)
{
    bool S(0);
    Mark[x]=1,Deep[x]=Deep[fa]+1,Father[x][0]=fa;
    for(register int i=0;Father[x][i];++i) Father[x][i+1]=Father[Father[x][i]][i];
    for(register int i=Head[x];i;i=Next[i]) DFS(To[i],x),S=1;
    if(!S) Left[x]=++TOT;
}
inline int LCA(int u,int v)
{
    if(Deep[u]>Deep[v]) swap(u,v);
    for(register int i=20;i>=0;--i) if (Deep[u]<=Deep[v]-(1<<i)) v=Father[v][i];
    if(u==v) return u;
    for(register int i=20;i>=0;--i) if(Father[u][i]!=Father[v][i]) u=Father[u][i],v=Father[v][i];
    return Father[u][0];
}
int main()
{
    n=pos=read(),m=read(),q=read();
    for(register int i=1;i<=2*n;++i) Fa[i]=i;
    for(register int i=1;i<=m;++i) E[i].x=read(),E[i].y=read(),E[i].z=read();
    sort(E+1,E+m+1,cmp1);
    while(tot<n&&K<m)
    {
        int x,y;x=GetFa(E[++K].x),y=GetFa(E[K].y);
        if(x!=y) V[++pos]=E[K].z,Fa[x]=pos,Fa[y]=pos,ADD(pos,x),ADD(pos,y),++tot;
    }
    for(register int i=pos;i;--i) if(!Mark[i]) DFS(i,0);
    for(register int i=1,k,Ans(0);i<=q;++i)
    {
        k=read();
        for(register int j=1;j<=k;++j) Q[j]=read()^Ans;
        sort(Q+1,Q+k+1,cmp2),Ans=0;
        for(register int j=1;j<k;++j) Ans=max(Ans,V[LCA(Q[j],Q[j+1])]);
        printf("%d\n",Ans);
    }
    return 0;
}
删边

 


 

 

(三)、NKOJ-8438  路径权值

问题描述:

给定一个带边权的树,树上任意两点间的路径权值d(x,y)定义为x,y这两个点之间路径上的最小值,
树上任意一点x的权值Val(x)定义为这个点到树上其他所有点的路径权值和,即Val(x)=∑d(x,i), 1<=i<=n。
现求树上一点,使得这个点的权值最大,输出这个值。

输入格式:

第一行,一个整数 n(1≤n≤100000) ,树的点的个数,节点编号1到n。
接下来n−1行,每行三个整数 x,y,s(1≤x,y≤n,1≤s≤10000),表示编号为x的节点和编号为y的节点之间存在一条权值为s的边。

输出格式:

一行,树上的点的最大权值

 

  按最大生成树重建,对路径 (a,b), LCA( a , b ) 即为路径上最小值。

  问题是,怎么快速算出答案呢?

 

  既然我们无法一条路径一条路径算贡献,那我们可以算每个点的贡献:

    上面在Kruskal重构树的性质中有这么一条:重构树是一颗二叉树 ;

    以 x 为根的子树中:对左儿子中一点,x 的贡献为  V [ x ] * Size [ Rs [ x ] ] ;对右儿子中一点,x 的贡献为  V [ x ] * Size [ Ls [ x ] ] (其中,V [ ] 为点权,Size [  ] 为子树大小,Ls [ ] 为左儿子,Rs [ ] 为右儿子);

    一个儿子中所以点都要加上另一个儿子使 x 产生的贡献,很容易想到了 DFS序 + 树状数组,即:

  Modify(In[Lt[i]], V[i]*Size[Rt[i]])
  Modify(Out[Lt[i]]+1, -V[i]*Size[Rt[i]])

  Modify(In[Rt[i]], V[i]*Size[Lt[i]])
  Modify(Out[Rt[i]]+1, -V[i]*Size[Lt[i]])

 

  最后答案是什么呢?

  显然,Ans [ i ] = GetSum ( In [ i ] )  (GetSum为树状数组求和)。

 

Code:

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
int n,K,Fa[200005],C[200005],Size[200005],tot,In[200005],Out[200005],pos,V[200005],Ls[200005],Rs[200005],Ans;
struct node {int x,y,z;}E[100005];
bool cmp(node x,node y) {return x.z>y.z;}
#define gc (p1==p2&&(p2=(p1=buf)+fread(buf,1,65536,stdin),p1==p2)?EOF:*p1++)
char buf[65536],*p1,*p2;
inline int read()
{
    char ch;int x(0);
    while((ch=gc)<48);
    do x=x*10+ch-48;while((ch=gc)>=48);
    return x;
}
inline int GetFa(int x) {if(Fa[x]==x) return x;return Fa[x]=GetFa(Fa[x]);}
inline void Modify(int x,int y) {while (x<=pos) C[x]+=y,x+=(x&(-x));}
inline int GetSum(int x) {int RET=0;while(x) RET+=C[x],x-=(x&(-x));return RET;}
inline void DFS(int x)
{
    In[x]=++tot;
    if(Ls[x]) DFS(Ls[x]),Size[x]+=Size[Ls[x]],DFS(Rs[x]),Size[x]+=Size[Rs[x]];
    Out[x]=tot;
    if(!Ls[x]) {Size[x]=1;return;}
    Modify(In[Ls[x]],V[x]*Size[Rs[x]]),Modify(Out[Ls[x]]+1,-V[x]*Size[Rs[x]]),Modify(In[Rs[x]],V[x]*Size[Ls[x]]),Modify(Out[Rs[x]]+1,-V[x]*Size[Ls[x]]);
}
int main()
{
    n=pos=read();
    for(register int i=1;i<=2*n;++i) Fa[i]=i;
    for(register int i=1;i<n;++i) E[i].x=read(),E[i].y=read(),E[i].z=read();
    sort(E+1,E+n,cmp);
    while(K<n-1) 
    {
        int x,y;
        x=GetFa(E[++K].x),y=GetFa(E[K].y),V[++pos]=E[K].z,Fa[x]=pos,Fa[y]=pos,Ls[pos]=x,Rs[pos]=y;
    }
    DFS(pos);
    for(register int i=1;i<=n;++i) Ans=max(Ans,GetSum(In[i]));
    printf("%d",Ans);
    return 0;
}
路径权值

 

posted @ 2021-08-17 21:19  qfxl  阅读(2171)  评论(0)    收藏  举报