Kruskal重构树

先推荐一篇对我帮助很大的博客:Kruskal重构树入门

前置知识

1、\(Kruskal\)算法

2、基础数据结构,如主席树

简述

\(Kruskal\)重构树与\(Kruskal\)算法密切相关。

我们知道,\(Kruskal\)算法是按照边权排序,依次合并节点,并用并查集维护联通。

\(Kruskal\)重构树则是合并节点\(x,y\)时,断开\(x,y\)的连边,新建一个节点\(z\),将\(z\)作为\(x,y\)的父亲,同时把\((x,y)\)的边权作为\(z\)的点权,然后用并查集维护联通。

1503934-20190402235815823-898145925.png
1503934-20190402235827644-1938858608.png

那么它有一些很有用的性质。

1、它是一个二叉堆。

2、若边权升序,则它是一个大根堆

3、任意两点路径边权最大值为\(Kruskal\)重构树上\(LCA\)的点权。

实现大致如下:

int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
void Kruskal()
{
    sort(E+1, E+m+1, cmp);
    for (int i=1; i<=n; i++) fa[i]=i; int now=n;
    for (int i=1; i<=m; i++)
    {
        int x=E[i].x, y=E[i].y, w=E[i].w;
        int fx=find(x), fy=find(y);
        if (fx^fy)
        {
            val[++now]=w; fa[fx]=fa[fy]=fa[now]=now; 
            add(now, fx, 1); add(now, fy, 1);
        }
    }
}

例题

我们来看一道例题:

bzoj3551 Peaks

题目传送门

你没发现这是两个链接吗

Description

\(N​\)座山峰,有高度\(h_i​\)。山峰间有\(M​\)条双向边,有边权。\(Q​\)组询问,询问从\(v​\)开始经过边权不超过\(x​\)能到达的山峰中第\(k​\)高。强制在线。

Solution

显然,一个点能到达,在最小生成树上必定能到达。但建出最小生成树并没有什么用,于是我们建出\(Kruskal\)重构树,然后发现\(dfs\)+主席树静态区间\(K\)大就可以了。

代码如下(本代码为洛谷4197AC代码):

#include<bits/stdc++.h>
using namespace std;
const int N=200005, M=500005;
struct node{int x, y, w;}E[M];
struct NODE{int to, nxt;}edge[N];
int size[N], deg[N], fa[N], dfn[N];
int in[N], out[N], h[N>>1], val[N];
int f[N][30], head[N], cnt, Cnt, tot, n, m, q;
int rt[N], ls[N<<5], rs[N<<5], sum[N<<5];

inline int read()
{
    int x=0,f=1;char ch=getchar();
    for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
    for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
    return x*f;
}

void add(int u, int v)
{
    edge[++Cnt]=(NODE){v, head[u]};
    head[u]=Cnt; deg[v]++;
}

bool cmp(node a, node b){return a.w<b.w;}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}

void dfs(int u)
{
    dfn[++tot]=u; in[u]=tot; 
    for (int i=1; (1<<i)<=(n<<1); i++) f[u][i]=f[f[u][i-1]][i-1];
    for (int i=head[u]; i; i=edge[i].nxt)
    {
        int v=edge[i].to; if (v==f[u][0]) continue;
        f[v][0]=u; dfs(v); size[u]+=size[v];
    }
    if (!size[u]) size[u]=1; out[u]=tot;
}

void insert(int &o, int l, int r, int x, int v)
{
    ls[++cnt]=ls[o]; rs[cnt]=rs[o]; 
    sum[cnt]=sum[o]+v; o=cnt;
    if (l==r) return; int mid=(l+r)>>1;
    if (x<=mid) insert(ls[o], l, mid, x, v);
        else insert(rs[o], mid+1, r, x, v);
}

int kth(int u, int v, int l, int r, int k)
{
    if (l==r) return l; 
    int mid=(l+r)>>1, s=sum[rs[v]]-sum[rs[u]];
    if (k<=s) return kth(rs[u], rs[v], mid+1, r, k);
        else return kth(ls[u], ls[v], l, mid, k-s);
}

int query(int v, int x, int k)
{
    for (int i=25; ~i; i--) 
        if (f[v][i] && val[f[v][i]]<=x) v=f[v][i];
    if (size[v]<k) return -1;
    return kth(rt[in[v]-1], rt[out[v]], 0, 1e9, k);
}

int main()
{
    n=read(); m=read(); q=read(); int nn=n;
    for (int i=1; i<=n; i++) h[i]=read();
    for (int i=1; i<=m; i++) E[i]=(node){read(), read(), read()};
    sort(E+1, E+m+1, cmp);
    for (int i=1; i<=(n<<1); i++) fa[i]=i;
    for (int i=1; i<=m; i++)
    {
        int fx=find(E[i].x), fy=find(E[i].y);
        if (fx^fy)
        {
            val[++nn]=E[i].w; fa[fx]=fa[fy]=nn;
            add(nn, fx); add(nn, fy); 
        }
    }
    for (int i=1; i<=nn; i++) if (!deg[i]) dfs(i);
    for (int i=1; i<=nn; i++)
    {
        rt[i]=rt[i-1]; 
        if (dfn[i]<=n) insert(rt[i], 0, 1e9, h[dfn[i]], 1);
    }
    for (int i=1; i<=q; i++)
    {
        int v=read(), x=read(), k=read();
        printf("%d\n", query(v, x, k));
    }
    return 0;
}

再看一题

NOI2018 归程

题目传送门

机房神犇\(zzr\)用200+行可持久化并查集大力秒题,可是我不会。

Description

\(N\)个点\(M\)条边的连通图,用\(l, a\)表示长度,海拔。

\(Q​\)次询问,每次询问给定起点\(v ​\),水位线\(p​\),求经过海拔不低于\(p​\)能到达的点中距离1号节点最近的距离。

Solution

建立以海拔为关键字的\(Kruskal\)重构树(海拔为降序),那么能到达的点在\(Kruskal\)重构树的一个子树中。

那么就很清晰了,先用\(Dijkstra\)预处理单源最短路,再构建\(Kruskal\)重构树,然后\(dfs\)维护每个店为根的子树中距1号点的最小距离。处理询问时树上倍增即可。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=800005;
struct node{int x, y, l;}E[N];
struct Edge{int to, nxt, w;}edge[N];
struct heap{int u, w;};
int f[N][21], fa[N], vis[N], dis[N], mn[N], val[N];
int head[N], cnt, n, m;
bool operator < (heap a, heap b){return a.w>b.w;}
bool cmp(node a, node b){return a.l>b.l;}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}

inline int read()
{
    int x=0,f=1;char ch=getchar();
    for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
    for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
    return x*f;
}

void add(int u, int v, int w)
{
    edge[++cnt]=(Edge){v, head[u], w};
    head[u]=cnt;
}

void Dijkstra()
{
    memset(vis, 0, sizeof(vis)); vis[1]=1;
    memset(dis, 0x3f, sizeof(dis)); dis[1]=0;
    priority_queue<heap> q; q.push((heap){1, 0});
    while (!q.empty())
    {
        int u=q.top().u; q.pop();
        for (int i=head[u]; i; i=edge[i].nxt)
        {
            int v=edge[i].to, w=edge[i].w;
            if (dis[v]>dis[u]+w)
                dis[v]=dis[u]+w, q.push((heap){v, dis[v]});
        }
    }
}

void dfs(int u, int fa)
{
    f[u][0]=fa; mn[u]=dis[u];
    for (int i=1; i<=20; i++) f[u][i]=f[f[u][i-1]][i-1];
    for (int i=head[u]; i; i=edge[i].nxt)
    {
        int v=edge[i].to; 
        dfs(v, u); mn[u]=min(mn[u], mn[v]);
    }
}

void Kruskal()
{
    sort(E+1, E+m+1, cmp);
    for (int i=1; i<=n; i++) fa[i]=i; int now=n;
    memset(head, 0, sizeof(head)); cnt=0;
    for (int i=1; i<=m; i++)
    {
        int x=E[i].x, y=E[i].y, w=E[i].l;
        int fx=find(x), fy=find(y);
        if (fx^fy)
        {
            val[++now]=w; fa[fx]=fa[fy]=fa[now]=now; 
            add(now, fx, 1); add(now, fy, 1);
        }
    }
    dfs(now, 0);
}

int main()
{
    int T=read();
    while (T--)
    {
        memset(head, 0, sizeof(head)); cnt=0;
        memset(mn, 0, sizeof(mn));
        memset(f, 0, sizeof(f));
        n=read(); m=read();
        for (int i=1; i<=m; i++)
        {
            int u=read(), v=read(), l=read(), a=read();
            E[i]=(node){u, v, a}; add(u, v, l); add(v, u, l);
        }
        Dijkstra(); Kruskal();
        int q=read(), k=read(), s=read();
        for (int i=1, last=0; i<=q; i++)
        {
            int v=(read()+k*last-1)%n+1, p=(read()+k*last)%(s+1);
            for (int j=20; ~j; j--)
                if (f[v][j] && val[f[v][j]]>p) v=f[v][j];
            printf("%d\n", last=mn[v]);
        }   
    }
    return 0;
}

最后一题

此题借鉴了Dance_Of_Faith大佬的博客。

IOI2018 Werewolf狼人

Description

\(N\)个点\(M\)条边的无向连通图。对于每次询问,给定\(S,E,L,R\),求出能否从\(S\)出发,前一段只经过\(L\)\(N\)的点,后一段只经过\(1\)\(R\)的点,到达\(E\)点。

Solution

我们构建两棵Kruskal重构树。

一棵从大到小枚举点\(u\),枚举连边且编号比它大的\(v\),让\(v\)所在集合为\(u\)的儿子节点,那么从\(S\)出发前一段能经过的点在一个子树中,用树上倍增即可找到这个子树。

\(E​\)能到达的后一段也可同理求得。

接下来要找到一个前后两段的分界线,我们只需考虑每一个点在两棵树的\(dfs\)序是否都在每一次询问的\(S\)\(E\)的出入的\(dfs\)序的区间中。这是经典的二维数点问题,离线用树状数组解决,在线用主席树解决。

因为本人比较懒,只给出了离线树状数组实现的代码。

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int N=200005;
struct node{int x, y, id, typ, sig;}q[N<<3];
bool operator < (node a, node b){return a.x<b.x || (a.x==b.x && a.typ<b.typ);}
vector<int> G[N];
int ans[N], d[N], n, m, Q;

struct dsu
{
    int fa[N];
    void init(){for (int i=1; i<=n; i++) fa[i]=i;}
    int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
}u, v;

struct Kruskal
{
    vector<int> G[N];
    int in[N], out[N], deg[N], dep[N], rnk[N], f[N][22], tot;
    void add(int u, int v){G[u].push_back(v); deg[v]++;}
    void dfs(int u, int fa)
    {
        in[u]=++tot; rnk[tot]=u;
        dep[u]=dep[fa]+1; f[u][0]=fa;
        for (int i=1; i<=20; i++) f[u][i]=f[f[u][i-1]][i-1];
        for (int v: G[u]) dfs(v, u);
        out[u]=tot;
    }
    void build()
    {
        int rt; for (int i=1; i<=n; i++) if (!deg[i]) rt=i;
        dfs(rt, 0);
    }
}U, V;

int getfa_U(int u, int anc)
{
    for (int i=20; ~i; i--) 
        if (U.f[u][i] && U.f[u][i]>=anc) u=U.f[u][i];
    return u;
}
int getfa_V(int v, int anc)
{
    for (int i=20; ~i; i--)
        if (V.f[v][i] && V.f[v][i]<=anc) v=V.f[v][i];
    return v;
}
    

void add(int x){for (; x<=n; x+=x&-x) d[x]++;}
int query(int x){int res=0; for (; x; x-=x&-x) res+=d[x]; return res;}

inline int read()
{
    int x=0,f=1;char ch=getchar();
    for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
    for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
    return x*f;
}

int main()
{
    n=read(); m=read(); Q=read();
    for (int i=1; i<=m; i++)
    {
        int u=read()+1, v=read()+1;
        G[u].push_back(v); 
        G[v].push_back(u);
    }
    u.init(); v.init();
    
    for (int i=n; i; i--)
        for (int j: G[i]) if (j>i) 
        {
            int k=u.find(j);
            if (i^k) U.add(i, k), u.fa[k]=u.fa[i];
        }
    for (int i=1; i<=n; i++)
        for (int j: G[i]) if (j<i)
        {
            int k=v.find(j); 
            if (i^k) V.add(i, k), v.fa[k]=v.fa[i];
        }
    U.build(); V.build();
    
    for (int i=1; i<=n; i++)
        q[i].x=U.in[i], q[i].y=V.in[i], q[i].typ=0;
    int cnt=n;
    for (int i=1; i<=Q; i++) 
    {
        int x=read()+1, y=read()+1, l=read()+1, r=read()+1;
        int u=getfa_U(x, l), v=getfa_V(y, r);
        q[++cnt]=(node){U.out[u], V.out[v], i, 1, 1};
        if (U.in[u]>1) 
            q[++cnt]=(node){U.in[u]-1, V.out[v], i, 1, -1};
        if (V.in[v]>1) 
            q[++cnt]=(node){U.out[u], V.in[v]-1, i, 1, -1};
        if (U.in[u]>1 && V.in[v]>1) 
            q[++cnt]=(node){U.in[u]-1, V.in[v]-1, i, 1, 1};
    }
    
    sort(q+1, q+cnt+1);
    for (int i=1; i<=cnt; i++)
        if (q[i].typ)
            ans[q[i].id]+=q[i].sig*query(q[i].y);
        else add(q[i].y);
    for (int i=1; i<=Q; i++) printf("%d\n", (bool)ans[i]);
    return 0;
}

写在最后

其实有很多题目能够用\(Kruskal\)重构树暴力优雅地写过,例如\(NOIp2013\)货车运输。所以看到求经过权值不超过\(k\)的点或边能到达的点的相关的信息时,就可以艰难简单地考虑用\(Kruskal ​\)重构树来做啦。

转载于:https://www.cnblogs.com/ACMSN/p/10646306.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值