并查集与Kruskal重构树

并查集

【二分】【种类并查集】LuoguP1525 关押罪犯

【题目大意】\(N\)号人划分为2个集团,存在对敌对关系,每对关系包含三个信息,表示若划分到同一集团,将产生分数,对全体合理的划分方案,求产生分数最大值的最小值(若上述关系均未触发,结果为0)

答案的求解满足二分的求解要求,故将其转化为判断合法性问题。

将关系按降序排序,并只关注大于等于mid的部分,要求此部分不触发任何关系。对于二元种类并查集,若要使\((a,b),(b,c)\)不触发关系,则\(a,c\)必划分到同一集团。即若要使\((a,b)\)不触发关系,则必将\(a\)\(b\)的所有敌人划分到同一集团。

此处并查集将扩充1倍,后\(N\)个元素表示对应敌人,对某关系\((a_j,b_j,c_j)\),有\(Union(a_j,b_j+N),Union(b_j,a_j+N)\)

【种类并查集】LuoguP2024

【二分】【并查集】LuoguP4047 部落划分

【离散化】【种类并查集】LuoguP1955

LuoguP1396 营救

Kruskal重构树

在利用Kruskal算法求解某个图最小生成树的过程中,将待加入的边记为一个新结点,其点权为此边的边权,且此边连接的两个儿子为结点的左、右儿子。

(1)得到的重构树是一个二叉树,共有\(2n-1\)个结点,其恰有\(n\)个叶子结点

(2)重构树可视为大根堆

(3)原图中两点间所有简单路径的最大边权的最小值,等于最小生成树上两点之间边权最大值,等于重构树上两点LCA的点权

(4)到某个结点简单路径上的最大边权最小值\(\le k\)的全体结点均在重构树上的某棵子树内,且恰为该子树的叶子结点。

【例题】

【Kruskal重构树】【ST表】CF1706E

【题目大意】连通无向图\(|V|=n,|E|=m\),其中边按顺序给出,要求回答\(q\)个询问,每个询问包含参数\(L,R\),输出最小的\(k\),使得全体满足\(L\le a<b\le R\)的整数对\((a,b)\),顶点\(a,b\)仅使用前\(k\)条边可以相互到达。

以边的顺序为标准建立Kruskal重构树后,求出顶点\(i,i+1\)的LCA权值,要求\([L,R]\)区域的最小\(k\),只需快速求区间最大值,使用\(ST\)表,总时间复杂度\(O(\max(n\log n,q,m))\)

【Kruskal重构树】LuoguP1967/P2245 货车运输/星际导航

无向图\(|V|=n,|E|=m\),边权为\(w_i\),给出\(q\)个询问,先判断\(x,y\)是否连通,连通则求出\(x\)\(y\)所有可能路径中最小边权最大值。

使用并查集判断连通。

方法一:直接套用Kruskal重构树;

LuoguP1967/P2245 货车运输参考解答一
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=1e6+5;
int n,m,q,root,tot;
int s[N],depth[N],fa[N][25],l[N],r[N],val[N];
struct edge{int x,y,w;}a[M];
bool cmp(edge x,edge y){return x.w>y.w;}
int find(int x)
{
    if(x==s[x]) return x;
    return s[x]=find(s[x]);
}
void getdepth(int x,int d)
{
    depth[x]=d;
    if(l[x]) getdepth(l[x],d+1);
    if(r[x]) getdepth(r[x],d+1);
}
int lca(int x,int y)
{
    if(depth[x]<depth[y]) swap(x,y);
    for(int i=20;i>=1;i--)
    {
        if(depth[fa[x][i]]>=depth[y])
            x=fa[x][i];
    }
    if(x==y) return x;
    for(int i=20;i>=1;i--)
    {
        if(fa[x][i]!=fa[y][i])
            x=fa[x][i],y=fa[y][i];
    }
    return fa[x][1];
}
int main()
{
    scanf("%d%d",&n,&m);
    tot=n;
    for(int i=1;i<=2*n-1;i++) s[i]=i;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].w);

    }
    sort(a+1,a+m+1,cmp);
    for(int i=1;i<=m;i++)
    {
        int xx=find(a[i].x);
        int yy=find(a[i].y);
        if(xx==yy) continue;
        tot++;
        s[xx]=s[yy]=tot;
        fa[xx][1]=fa[yy][1]=tot;
        l[tot]=xx,r[tot]=yy;
        val[tot]=a[i].w;
    }
    for(int i=1;i<=tot;i++)
        if(fa[i][1]==0)
            getdepth(i,0);
    scanf("%d",&q);
    int u,v;
    for(int i=2;i<=20;i++)  
        for(int j=1;j<=tot;j++)
            fa[j][i]=fa[fa[j][i-1]][i-1];
    for(int i=1;i<=q;i++)
    {
        scanf("%d%d",&u,&v);
        if(find(u)!=find(v)) printf("-1\n");
        else printf("%d\n",val[lca(u,v)]);
    }
    return 0;
}

方法二:使用朴素的倍增区间最大值、LCA

在倍增求解\(fa_{j,i}\)时增加\(ST_{j,i}\)表示从\(j\)结点开始向上到达第\(2^i\)个祖先路径上的最小边权值值,初始化\(st_{j,0}=w_0,由(fa_{j,0},j,w_0)边在求解depth_j,fa_{j,0}\)函数中得到。

\(st_{j,i}=\min(st_{j,i-1},st_{fa_{j,i-1},i-1})\)

在求解LCA时,顺便求出路径区间最小值。

LuoguP1967/P2245 货车运输参考解答二
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5,M=1e6+5;
struct edge{int v,w,next;}a[M];
struct edge2{int u,v,w;}b[M];
int st[N][25],fa[N][25],tot,n,m,q,h[M];
int depth[N],s[N];
int find(int x)
{
    if(x==s[x]) return x;
    return s[x]=find(s[x]);
}
bool cmp(edge2 x,edge2 y){return x.w>y.w;}
void add(int x,int y,int z)
{
    a[++tot]=(edge){y,z,h[x]};
    h[x]=tot;
}
void maketree(int x,int father,int d)
{
    fa[x][0]=father,depth[x]=d;
    for(int i=h[x];i;i=a[i].next)
    {
        int v=a[i].v;
        if(v==father) continue;
        st[v][0]=a[i].w;
        maketree(v,x,d+1);
    }
}
int getans(int x,int y)
{
    int ans=1e9;
    if(depth[x]<depth[y]) swap(x,y);
    for(int i=20;i>=0;i--)
        if(depth[fa[x][i]]>=depth[y])
            ans=min(ans,st[x][i]),x=fa[x][i];
    if(x==y) return ans;
    for(int i=20;i>=0;i--)
        if(fa[x][i]!=fa[y][i])
        {
            ans=min(ans,min(st[x][i],st[y][i]));
            x=fa[x][i],y=fa[y][i];
        }
    return min(ans,min(st[x][0],st[y][0]));
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) s[i]=i;
    for(int i=1;i<=m;i++) 
        scanf("%d%d%d",&b[i].u,&b[i].v,&b[i].w);
    sort(b+1,b+m+1,cmp);
    for(int i=1;i<=m;i++)
    {
        int xx=find(b[i].u),yy=find(b[i].v);
        if(xx==yy) continue;
        s[xx]=yy;
        add(b[i].u,b[i].v,b[i].w);
        add(b[i].v,b[i].u,b[i].w);
    }
    for(int i=1;i<=n;i++)
        if(s[i]==i) maketree(i,0,1);
    for(int i=1;i<=20;i++)
        for(int j=1;j<=n;j++)
            fa[j][i]=fa[fa[j][i-1]][i-1];
    for(int i=1;i<=20;i++)
        for(int j=1;j<=n;j++)
            st[j][i]=min(st[j][i-1],st[fa[j][i-1]][i-1]);
    cin>>q;
    int u,v;
    for(int i=1;i<=q;i++)
    {
        scanf("%d%d",&u,&v);
        if(find(u)!=find(v)) printf("-1\n");
        else printf("%d\n",getans(u,v));
    }
    return 0;
}

【Kruskal重构树】LuoguP4768 归程

【题目大意】无向连通图\(G\)\(|V|=n,|E|=m\),其边为四元组\((x_i,y_i,w_i,p_i)\),表示从\(x_i\)到有长度为\(w_i\),海拔为\(p_i\)的双向边。有\(Q\)个询问,指示当天的出发点\(v\)与水位线\(p\)

每条询问中,某人要从\(v\)号结点到达1号结点,在起始点处有一辆车,允许在高于水位线的边上行驶,否则需要停车步行,求停车步行的最短距离。

每条询问是独立的,车的信息只适用于本条询问。

为求出车可以行驶到达的结点集合,选择建立最小生成树,再构建kruskal重构树,对于某水位线\(p_0\),从结点\(v\)出发通过车辆行驶可以互相到达的结点一定处于Kruskal重构树的某个子树\(T\)中。

\(dis_i\)表示从1号结点到号结点的最短路长度,可知最终答案为\(\min\limits_{j\in T}dis_j\),记1号结点的祖先路径为\(path\),若\(T\cap path \neq \varnothing\),则答案为0,否则,可以在构建Kruskal重构树,将\(x,y\)设为新结点\(z\)的儿子时,增加记录\(dis_z=\min(dis_x,dis_y)\)

最终输出\(dis_{root}即可,其中root为T的根\)

参考实现
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+5,M=8e5+5;
int tot,dis[N],vis[N],h[M],fa[N][21],stop[N];
int val[N],n,m,s[N];
struct edge{int v,w,next;}a[M];
struct edge2{int u,v,h;}e[M];
bool cmp(edge2 x,edge2 y){return x.h>y.h;}
int find(int x)
{
    if(x==s[x]) return x;
    return s[x]=find(s[x]);
}
struct priority
{
    int w,v;
    bool operator<(const priority &x)const
    {
        return x.w<w;
    }
};
void add(int x,int y,int z)
{
    a[++tot]=(edge){y,z,h[x]};
    h[x]=tot;
}
void dijkstra()
{
    for(int i=1;i<=2*n-1;i++) dis[i]=1e9;
    std::priority_queue<priority> q;
    q.push((priority){0,1});
    dis[1]=0;
    memset(vis,0,sizeof(vis));
    while(!q.empty())
    {
        priority temp=q.top();q.pop();
        int x=temp.v;
        if(vis[x]) continue;
        vis[x]=1;
        for(int i=h[x];i;i=a[i].next)
        {
            int v=a[i].v;
            if(dis[v]>dis[x]+a[i].w)
            {
                dis[v]=dis[x]+a[i].w;
                if(!vis[v])
                    q.push((priority){dis[v],v});
            }
        }
    }
}
void kruskal()
{
    int cnt=0;
    for(int i=1;i<n;)
    {
        cnt++;
        int xx=find(e[cnt].u),yy=find(e[cnt].v);
        if(xx==yy) continue;
        s[xx]=s[yy]=i+n;
        fa[xx][0]=fa[yy][0]=i+n;
        dis[n+i]=min(dis[xx],dis[yy]);
        val[n+i]=e[cnt].h;
        i++;
    }
    for(int i=1;i<=20;i++)
        for(int j=1;j<=2*n-1;j++)
            fa[j][i]=fa[fa[j][i-1]][i-1];
}
int getans(int x,int p)
{
    for(int i=20;i>=0;i--)
        if(stop[fa[x][i]]!=1&&val[fa[x][i]]>p)
            x=fa[x][i];
    if(stop[fa[x][0]]==1&&val[fa[x][0]]>p)
        return 0;
    else return dis[x];
}
void findreverse()
{
    int cnt=1;
    while(cnt) stop[cnt]=1,cnt=fa[cnt][0];
    stop[0]=1;
}
void test()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=2*n-1;i++) s[i]=i;
    int w,Q,K,S,lastans=0,V,P;
    for(int i=1;i<=m;i++) 
    {
        scanf("%d%d%d%d",&e[i].u,&e[i].v,&w,&e[i].h);
        add(e[i].u,e[i].v,w);
        add(e[i].v,e[i].u,w);
    }
    sort(e+1,e+m+1,cmp);
    dijkstra();
    kruskal();
    findreverse();
    cin>>Q>>K>>S;
    for(int i=1;i<=Q;i++)
    {
        scanf("%d%d",&V,&P);
        int v=(V+K*lastans-1)%n+1;
        int p=(P+K*lastans)%(S+1);
        lastans=getans(v,p);
        printf("%d\n",lastans);
    }
}
int main()
{
    //freopen("D:\\return5.in","r",stdin);
    //freopen("D:\\out.txt","w",stdout);
    int t;cin>>t;
    while(t--) 
    {
        test();
        tot=0;
        memset(a,0,sizeof(a));
        memset(h,0,sizeof(h));
        memset(val,0,sizeof(val));
        memset(fa,0,sizeof(fa));
        memset(stop,0,sizeof(stop));
    }
    return 0;
}