可持久化并查集、带删并查集

P3402 可持久化并查集

链接:https://www.luogu.com.cn/problem/P3402

题意:给定 n 个集合,第 i 个集合内初始状态下只有一个数,为 i。有 m 次操作。操作分为 3 种:

  • 1 a b 合并 a,b 所在集合

  • 2 k 回到第 k 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态

  • 3 a b 询问 a,b 是否属于同一集合,如果是则输出 1 ,否则输出 0

思路:可持久化并查集

#include <bits/stdc++.h>
#define pii pair<int,int>
#define fi first
#define se second
#define ll long long
using namespace std;
const int maxn=2e5+5;

int n,m;
int root[maxn],ls[maxn*20],rs[maxn*20],sz[maxn*20],fa[maxn*20],no;
int build(int L,int R)
{
    int rt=++no;
    if(L==R)
    {
        fa[rt]=L;
        sz[rt]=1;
        return rt;
    }
    int mid=(L+R)>>1;
    ls[rt]=build(L,mid);
    rs[rt]=build(mid+1,R);
    return rt;
}
int update(int pre,int p,int L,int R,int f,int s)
{
    int rt=++no;
    ls[rt]=ls[pre];
    rs[rt]=rs[pre];
    if(L==R)
    {
        fa[rt]=f;
        sz[rt]=s;
        return rt;
    }
    int mid=(L+R)>>1;
    if(p<=mid) ls[rt]=update(ls[pre],p,L,mid,f,s);
    if(p>mid) rs[rt]=update(rs[pre],p,mid+1,R,f,s);
    return rt;
}
pii query(int rt,int p,int L,int R)
{
    if(L==R) return {fa[rt],sz[rt]};
    int mid=(L+R)>>1;
    if(p<=mid) return query(ls[rt],p,L,mid);
    if(p>mid) return query(rs[rt],p,mid+1,R);
}
pii find(int rt,int x)
{
    pii res=query(rt,x,1,n);
    if(res.fi==x) return res;
    return find(rt,res.fi);
}
int main()
{
    scanf("%d%d",&n,&m);
    root[0]=build(1,n);
    int op,u,v,k;
    for(int i=1; i<=m; ++i)
    {
        scanf("%d",&op);
        if(op==1)
        {
            scanf("%d%d",&u,&v);
            root[i]=root[i-1];
            pii ru=find(root[i],u);
            pii rv=find(root[i],v);
            if(ru.fi==rv.fi) continue;
            if(ru.se>rv.se) swap(ru,rv);
            root[i]=update(root[i],ru.fi,1,n,rv.fi,0);
            root[i]=update(root[i],rv.fi,1,n,rv.fi,ru.se+rv.se);
        }
        else if(op==2)
        {
            scanf("%d",&k);
            root[i]=root[k];
        }
        else
        {
            scanf("%d%d",&u,&v);
            root[i]=root[i-1];
            printf("%d\n",find(root[i],u)==find(root[i],v));
        }
    }
    return 0;
}

P4768 [NOI2018]归程 (可持久化并查集)

链接:https://www.luogu.com.cn/problem/P4768

题意:给定 n 个点 m 条边的连通图,每条边带有高度 a ,长度 w 。每天的降水量为 h ,当边的高度不超过 h 时,会被淹没。如果边未被淹没可以开车前往,如果被淹没了就只能步行前往。现在 Yazid 需要从 v 节点回到 1 节点,他一开始是开着车的,下车步行之后,就不能再开车。给定 q 个询问,每个询问为 v 、q ,表示出发点 v 和今天的降水量,问从 v 到 1 的最短步行距离。 ( 1 ≤ n ≤ 2 × 1 0 5 , 1 ≤ m ≤ 4 × 1 0 5 ) (1\le n \le 2\times 10^5 ,1\le m \le 4\times 10^5) (1n2×105,1m4×105),强制在线

思路

#include <bits/stdc++.h>
#define fi first
#define se second
#define ll long long
using namespace std;
const int maxn=2e5+5,maxm=4e5+5,inf=2e9;

int t,n,m;
struct Edge
{
    int u,v,w,a;
    bool operator<(const Edge& b)const
    {
        return a<b.a;
    }
};
struct DSU
{
    int fa,sz,dis;
} dsu[maxm*20];
vector<pair<int,int> > e[maxn];
vector<int> b;
vector<Edge> edges;
int dis[maxn],visit[maxn];

void dijstra()
{
    for(int i=0; i<=n; ++i) dis[i]=inf,visit[i]=0;
    dis[1]=0;
    priority_queue<pair<int,int> > pq;
    pq.push({0,1});
    while(!pq.empty())
    {
        int u=pq.top().se;
        pq.pop();
        if(visit[u]) continue;
        visit[u]=1;
        for(auto x: e[u])
        {
            int v=x.fi,w=x.se;
            if(dis[u]+w<dis[v])
            {
                dis[v]=dis[u]+w;
                pq.push({-dis[v],v});
            }
        }
    }
}

int root[maxn],ls[maxm*20],rs[maxm*20],no;
int build(int L,int R)
{
    int rt=++no;
    if(L==R)
    {
        dsu[rt]= {L,1,dis[L]};
        return rt;
    }
    int mid=(L+R)>>1;
    ls[rt]=build(L,mid);
    rs[rt]=build(mid+1,R);
    return rt;
}
int update(int pre,int p,int L,int R,DSU val)
{
    int rt=++no;
    ls[rt]=ls[pre];
    rs[rt]=rs[pre];
    if(L==R)
    {
        dsu[rt]=val;
        return rt;
    }
    int mid=(L+R)>>1;
    if(p<=mid) ls[rt]=update(ls[pre],p,L,mid,val);
    if(p>mid) rs[rt]=update(rs[pre],p,mid+1,R,val);
    return rt;
}
DSU query(int rt,int p,int L,int R)
{
    if(L==R) return dsu[rt];
    int mid=(L+R)>>1;
    if(p<=mid) return query(ls[rt],p,L,mid);
    if(p>mid) return query(rs[rt],p,mid+1,R);
}
DSU find(int rt,int x)
{
    DSU res=query(rt,x,1,n);
    if(res.fa==x) return res;
    return find(rt,res.fa);
}

int main()
{
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        edges.clear();
        b.clear();
        for(int i=1; i<=n; ++i) e[i].clear();
        no=0;
        for(int i=1; i<=m; ++i)
        {
            int u,v,w,a;
            scanf("%d%d%d%d",&u,&v,&w,&a);
            e[u].push_back({v,w});
            e[v].push_back({u,w});
            b.push_back(a);
            edges.push_back({u,v,w,a});
        }
        dijstra();
        sort(b.begin(),b.end());
        sort(edges.begin(),edges.end());

        root[m+1]=build(1,n);
        for(int i=m; i>=1; --i)
        {
            root[i]=root[i+1];
            int u=edges[i-1].u,v=edges[i-1].v;
            DSU ru=find(root[i],u);
            DSU rv=find(root[i],v);
            if(ru.fa==rv.fa) continue;
            if(ru.sz>rv.sz) swap(ru,rv);
            root[i]=update(root[i],ru.fa,1,n, {rv.fa,0,0});
            root[i]=update(root[i],rv.fa,1,n, {rv.fa,rv.sz+ru.sz,min(ru.dis,rv.dis)});
        }

        int q,k,s,p,v,ans=0;
        scanf("%d%d%d",&q,&k,&s);
        while(q--)
        {
            scanf("%d%d",&v,&p);
            v=(v+k*ans-1)%n+1;
            p=(p+k*ans)%(s+1);
            int pos=upper_bound(b.begin(),b.end(),p)-b.begin()+1;
            DSU res=find(root[pos],v);
            ans=res.dis;
            printf("%d\n",res.dis);
        }
    }
    return 0;
}

zcmu1435. 盟国 (带删并查集)

链接:https://acm.zcmu.edu.cn/JudgeOnline/problem.php?id=1435

题意:给定 n 个国家 m 个操作。两种操作: M i j ,将 i j 变为同盟。S i ,将第 i 个国家从联盟中删去,问最后有多少个联盟

思路: 用 id 表示 u 真正的父节点

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5,maxm=2e6+5;

int n,m;
int id[maxm],pa[maxm],sz[maxm],no;
int visit[maxm];

int find(int x)
{
    return x==pa[x]?x:pa[x]=find(pa[x]);
}
void merge(int u,int v)
{
    int ru=find(u),rv=find(v);
    if(ru==rv) return;
    pa[ru]=rv;
}
void del(int u)
{
    id[u]=++no;
    pa[no]=no;
}
int main()
{
    int Case=0;
    while(scanf("%d%d",&n,&m)&&(n||m))
    {
        memset(visit,0,sizeof(visit));
        for(int i=1; i<=n; ++i) id[i]=pa[i]=i,sz[i]=1;
        no=n;
        char op[5];
        int u,v;
        while(m--)
        {
            scanf("%s%d",op,&u);
            u++;
            if(op[0]=='M')
            {
                scanf("%d",&v);
                v++;
                merge(id[u],id[v]);
            }
            else del(u);
        }
        int ans=0;
        for(int i=1; i<=n; ++i)
        {
            int ru=find(id[i]);
            if(visit[ru]) continue;
            visit[ru]=1;
            ans++;
        }
        printf("Case #%d: %d\n",++Case,ans);
    }
    return 0;
}

UVA-11987 Almost Union-Find (带删并查集)

链接:https://vjudge.net/problem/UVA-11987

题意:给定 n 个数,一开始每个数都在一个集合内。维护 m 个操作,有 3 种类型:

  • 1 p q :将 p 所在集合与 q 所在集合合并
  • 2 p q :将 p 合并到 q 所在集合
  • 3 p : 查询 p 所在集合的元素个数,以及所有元素的和

( 1 ≤ n , m ≤ 1 0 5 ) (1\le n,m\le 10^5) (1n,m105)

思路:对于操作二,需要用到删除操作。删除以后再将 p 合并到 q 的集合上

  • 用 id 数组表示元素真正的根所在的位置
  • 大概就是开 n + m 长度的数组,删点时,先将自己的贡献在原集合中删去,然后再新开一个点用来合并
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5;

int n,m;
int pa[maxn<<1],id[maxn<<1],sz[maxn<<1],no;
ll sum[maxn<<1];
int find(int x)
{
    return x==pa[x]?x:pa[x]=find(pa[x]);
}
void merge(int u,int v)
{
    int ru=find(u),rv=find(v);
    if(ru!=rv)
    {
        pa[ru]=rv;
        sz[rv]+=sz[ru];
        sum[rv]+=sum[ru];
    }
}
void move(int u,int v)
{
    int ru=find(id[u]);
    int rv=find(id[v]);
    if(ru==rv) return;
    sz[ru]--;
    sum[ru]-=u;
    id[u]=++no;
    pa[no]=no;
    sum[no]=u;
    sz[no]=1;
    merge(id[u],id[v]);
}
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=1; i<=max(no,n); ++i)
            sum[i]=pa[i]=id[i]=i,sz[i]=1;
        no=n;
        while(m--)
        {
            int op,p,q;
            scanf("%d",&op);
            if(op==1)
            {
                scanf("%d%d",&p,&q);
                merge(id[p],id[q]);
            }
            else if(op==2)
            {
                scanf("%d%d",&p,&q);
                move(p,q);
            }
            else if(op==3)
            {
                scanf("%d",&p);
                int ru=find(id[p]);
                printf("%d %lld\n",sz[ru],sum[ru]);
            }
        }
    }
    return 0;
}

CF891C. Envy (贪心 + 并查集 || 可撤销并查集)

链接:https://codeforces.com/contest/891/problem/C

题意:给定 n 个点 m 条边的连通图。q 次询问,每次询问一个边集,问是否存在一个最小生成树包含这些边集

思路一

  • 首先根据边权对原图重构,最小的边权贪心的来说是必选的,对边权相同的进行连边。后面就相当于连通块对连通块连边,就会发生重边,这些重边就是不可能同时选择的,也就不可能出现在同一个边集中。
  • 图重构完之后,就判断有没有形成环,形成了就不能再同一个边集中了。

思路二

  • 将同一个询问的多条边按权值分离,每次询问前将图中小于当前权值的边都通过并查集做一下联通,然后在判断当前询问的多条边能不能构成MST。查询完之后,再对并查集做撤销
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+5;
int n,m;
int pa[maxn];
int find(int x)
{
    return x==pa[x]?x:pa[x]=find(pa[x]);
}
struct Edge
{
    int u,v,w,id;
    bool operator<(const Edge& b) const
    {
        return w<b.w;
    }
} edges[maxn],p[maxn];
bool cmp(Edge a,Edge b)
{
    return a.id<b.id;
}
bool solve()
{
    int k,x;
    scanf("%d",&k);
    for(int i=1; i<=k; ++i)
        scanf("%d",&x),p[i]=edges[x];
    sort(p+1,p+1+k);
    int j;
    for(int i=1; i<=k; i=j)
    {
        j=i;
        while(j<=k&&p[i].w==p[j].w) j++;
        for(int x=i; x<j; ++x)
        {
            int u=p[x].u,v=p[x].v;
            pa[u]=u,pa[v]=v;
        }
        for(int x=i; x<j; ++x)
        {
            int u=p[x].u,v=p[x].v;
            int ru=find(u),rv=find(v);
            if(ru==rv) return 0;
            pa[ru]=rv;
        }
    }
    return 1;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; ++i) pa[i]=i;
    for(int i=1; i<=m; ++i)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        edges[i]= {u,v,w,i};
    }
    sort(edges+1,edges+1+m);
    int j;
    for(int i=1; i<=m; i=j)
    {
        j=i;
        while(j<=m&&edges[i].w==edges[j].w) j++;
        for(int k=i; k<j; ++k)
        {
            edges[k].u=find(edges[k].u);
            edges[k].v=find(edges[k].v);
        }
        for(int k=i; k<j; ++k)
        {
            int ru=find(edges[k].u),rv=find(edges[k].v);
            if(ru==rv) continue;
            pa[ru]=rv;
        }
    }
    sort(edges+1,edges+1+m,cmp);
    int q;
    scanf("%d",&q);
    while(q--)
    {
        puts(solve()?"YES":"NO");
    }
    return 0;
}
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+5,maxm=5e5;

int n,m;
int ans[maxn];
int pa[maxn],depth[maxn];
struct Edge
{
    int u,v,w;
} e2[maxn];
vector<Edge> e1[maxn];

struct Query
{
    int u,v,id;
};
vector<Query> qu[maxn];

struct Node
{
    int u,ru,v,dv;
};

int find(int x)
{
    return x==pa[x]?x:find(pa[x]);
}
void merge(int u,int v)
{
    int ru=find(u),rv=find(v);
    if(ru!=rv)
    {
        if(depth[ru]>depth[rv]) swap(ru,rv);
        if(depth[ru]+1>depth[rv]) depth[rv]++;
        pa[ru]=rv;
    }
}

stack<Node> sta;
int solve(int w,int l,int r)
{
    while(!sta.empty()) sta.pop();

    for(int i=l; i<=r; ++i)
    {
        int u=qu[w][i].u,v=qu[w][i].v;
        int ru=find(u),rv=find(v);
        if(ru==rv) ans[qu[w][i].id]=1;
        else
        {
            if(depth[ru]>depth[rv]) swap(ru,rv);
            sta.push({ru,pa[ru],rv,depth[rv]});
            if(depth[ru]+1>depth[rv]) depth[rv]++;
            pa[ru]=rv;
        }
    }
    while(!sta.empty())
    {
        int u=sta.top().u,ru=sta.top().ru,v=sta.top().v,dv=sta.top().dv;
        sta.pop();
        pa[u]=ru;
        depth[v]=dv;
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; ++i) pa[i]=i;
    for(int i=1; i<=m; ++i)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        e1[w].push_back({u,v,0});
        e2[i]= {u,v,w};
    }
    int q,k,x;
    scanf("%d",&q);
    for(int id=1; id<=q; ++id)
    {
        scanf("%d",&k);
        for(int i=1; i<=k; ++i)
        {
            scanf("%d",&x);
            qu[e2[x].w].push_back({e2[x].u,e2[x].v,id});
        }
    }

    for(int i=1; i<=maxm; ++i)
    {
        for(auto x: e1[i-1]) merge(x.u,x.v);
        int r;
        for(int l=0; l<qu[i].size(); l=r)
        {
            r=l;
            while(r<qu[i].size()&&qu[i][l].id==qu[i][r].id) r++;
            solve(i,l,r-1);
        }
    }
    for(int i=1; i<=q; ++i) puts(ans[i]?"NO":"YES");
    return 0;
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
持久化并查集是指在并查集的基础上,支持回退到任意历史版本。这个结构可以用来处理一些需要撤销或者回退操作的问题。以下是一个基本的可持久化并查集的实现。 ```python class Node: def __init__(self, parent=None, rank=0): self.parent = parent self.rank = rank class PersistentUnionFind: def __init__(self, size): self.n = size self.roots = [None] * (2 * size) self.ranks = [None] * (2 * size) def make_set(self, v): self.roots[v] = Node(v) self.ranks[v] = 0 def find(self, node, version): if node.parent is None: return node if node.parent != node: node.parent = self.find(node.parent, version) return node.parent def union(self, x, y, version): x_root = self.find(self.roots[x], version) y_root = self.find(self.roots[y], version) if x_root == y_root: return False if self.ranks[x_root] < self.ranks[y_root]: x_root, y_root = y_root, x_root new_root = Node(x_root, self.ranks[x_root] + (self.ranks[x_root] == self.ranks[y_root])) self.roots[x] = self.roots[y] = new_root self.ranks[x_root] = self.ranks[y_root] = new_root.rank return True def get_version(self): return len(self.roots) // self.n - 1 def get_root(self, v, version): return self.find(self.roots[v], version).parent.val ``` 这个代码中,我们使用了一个 `Node` 类来表示每个节点,其中 `parent` 表示节点的父亲,`rank` 表示节点的秩。我们需要用一个 `roots` 数组来保存所有版本的根节点,以及一个 `ranks` 数组来保存所有节点的秩。`make_set` 函数用来初始化一个新节点,这个节点的父亲指向自己,秩为 0。`find` 函数用来找到节点所在的集合的根节点。如果节点的父亲不是根节点,那么我们就递归地寻找它的父亲。在递归返回之前,我们将所有遍历过的节点的父亲都更新为根节点,这样可以加速下次查找。`union` 函数用来将两个节点所在的集合合并。首先找到两个节点所在集合的根节点,如果根节点相同,那么这两个节点已经在同一个集合中,不需要再次合并。否则,我们将秩较小的根节点挂在秩较大的根节点下面,同时更新秩。`get_version` 函数用来获取当前版本号,而 `get_root` 函数则用来获取节点在指定版本中所在的集合的根节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值