简要题意

你需要维护一个并查集,支持版本回退,查连通性,合并两个点。

特别的,没进行一次操作都要新建一个版本。

前置知识

找根(find)

现在我们来考虑如何find。

首先我们来研究以下正常的并查集是怎么写的(不加任何优化):

int find(int x){
if(fa[x]==x){
return x;
}
else{
return find(fa[x]);
}
}

事实上,我们只需要将 fa 替换成用可持久化数组维护,那么就可以实现可持久化了。(显然)

合并(merge)

同上,我们研究一下正常的并查集是怎么写的(同样,不加任何优化):

void merge(int x,int y){
fa[find(x)]=find(y); // 将 x 合并到 y 上。
}

大家可能回想,那我也吧 fa 改成可持久化的不就行了吗?

如果这样子……

合并的优化

如果直接暴力合并,那么会TLE。因为链就可以将你的 find 轻松卡到 \(O(n\log n)\)。

回忆一下并查集的优化,有下面这几种方式:

  • 路径压缩
  • 按秩合并

首先,不能路径压缩,因为路径压缩时间复杂度是均摊的,可以被人卡到 \(O(n\log n)\)。(同样,基于均摊复杂度的珂朵莉树、Splay 都不能简单的可持久化)

其次我们考虑按秩合并,其中的“秩”有下面几种:

  • 随机,等到合并时修改,小的合并到大的。
  • 按子树大小,小的合并到大的。
  • 按子树最大深度(就是子树树高),小的合并到大的。

随机方案貌似被人Hack了。我们就用第二个吧(因为第三个不会写)。

那么我们又要开一个可持久化数组(建议用结构体封装),维护子树大小,记得合并后我们更新一下(就是将新的根的子树大小加上旧的根的子树大小)。

对于证明过程,@chenxinyang2006 的题解已经写的很清楚了,我就不赘述了。

实现操作与时间复杂度分析

  • 合并就用上面介绍的。
  • 查连通性我们就找两个节点的根,看它们是不是一样的。
  • 回退版本我们就复制一个到当前版本即可。

时间复杂度,可持久化数组读写复杂度都是 \(O(\log n)\),那么:

  • 回退版本时间复杂度是 \(O(1)\) 的(因为只要普通数组赋值)。
  • 找根的时间复杂度本身是 \(O(\log n)\) (优化了),乘上可持久化的时间复杂度就是 \(O(\log^{2}n)\)。
  • 所以,查连通性时间复杂度是 \(O(\log^{2} n)\)。
  • 合并的时间复杂度也是 \(O(\log^{2}n)\)(瓶颈在找根)。

整体时间复杂度为 \(O(n+m\log^{2}n)\)。可以通过本题。

代码

本代码封装了可持久化数组和并查集,供大家参考。

#include<bits/stdc++.h>
using namespace std; int n,m; namespace PersistentUnionFind{
struct PersistentArray {
#define mid ((l+r)>>1)
const static int SIZE = 1e5 + 5;
struct {
int l, r, v;
} t[SIZE * 25];
int top;
int root[SIZE * 25];
int a[SIZE];
int newnode(int i) {
t[++top] = t[i];
return top;
}
int build(int i, int l, int r) {
i = (++top);
if (l == r) {
t[i].v = a[l];
return i;
}
t[i].l = build(t[i].l, l, mid);
t[i].r = build(t[i].r, mid + 1, r);
return i;
}
int update(int i, int l, int r, int p, int val) {
i = newnode(i);
if (l == r) {
t[i].v = val;
return i;
}
if (p <= mid) {
t[i].l = update(t[i].l, l, mid, p, val);
} else {
t[i].r = update(t[i].r, mid + 1, r, p, val);
}
return i;
}
int query(int i, int l, int r, int p) {
if (l == r) {
return t[i].v;
}
if (p <= mid) {
return query(t[i].l, l, mid, p);
} else {
return query(t[i].r, mid + 1, r, p);
}
}
inline int Assign(int i, int version, int p, int val) {
return update(root[version], 1, n, p, val);
}
inline int Get(int i, int version, int p) {
return query(root[version], 1, n, p);
}
void copyVersion(int new_,const int dst){
root[new_]=root[dst];
}
void newVersionFromPoint(int pos,int val){
root[pos]=val;
}
} fa,siz;
int find(int x,int version){
if(fa.Get(114514,version,x)==x){
return x;
}
else{
return find(fa.Get(1919810,version,x),version);
}
}
void merge(int x,int y,int version){
int fx=find(x,version),fy=find(y,version);
if(fx==fy)return;
int xsiz=siz.Get(114514,version,fx),ysiz=siz.Get(1919810,version,fy);
if(xsiz<=ysiz){
fa.newVersionFromPoint(version,fa.Assign(114514,version,fx,fy));
siz.newVersionFromPoint(version,siz.Assign(114514,version,fy,xsiz+ysiz));
}
else{
fa.newVersionFromPoint(version,fa.Assign(114514,version,fy,fx));
siz.newVersionFromPoint(version,siz.Assign(114514,version,fx,xsiz+ysiz));
}
}
bool same(int x,int y,int version){
return find(x,version)==find(y,version);
}
} signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
PersistentUnionFind::fa.a[i]=i;
PersistentUnionFind::siz.a[i]=1;
}
PersistentUnionFind::fa.newVersionFromPoint(0,PersistentUnionFind::fa.build(1,1,n));
PersistentUnionFind::siz.newVersionFromPoint(0,PersistentUnionFind::siz.build(1,1,n));
for(int i=1;i<=m;i++){
int op,x,y;
cin>>op>>x;
PersistentUnionFind::fa.copyVersion(i,i-1);
PersistentUnionFind::siz.copyVersion(i,i-1);
if(op==1){
cin>>y;
PersistentUnionFind::merge(x,y,i);
}
if(op==2){
PersistentUnionFind::fa.copyVersion(i,x);
PersistentUnionFind::siz.copyVersion(i,x);
}
if(op==3){
cin>>y;
cout<<PersistentUnionFind::same(x,y,i)<<'\n';
}
}
return 0;
}

AC Record

可持久化并查集学习笔记 | 题解P3402 可持久化并查集的更多相关文章

  1. Mysql学习笔记(六)增删改查

    PS:数据库最基本的操作就是增删改查了... 学习内容: 数据库的增删改查 1.增...其实就是向数据库中插入数据.. 插入语句 insert into table_name values(" ...

  2. Redis 学习笔记4: Redis 3.2.1 集群搭建

    在CenOS 6.7 linux环境下搭建Redis 集群环境 1.下载最新的Redis版本 本人下载的Redis版本是3.2.1版本,下载之后,解压,编译(make): 具体操作可以参考我的博文:R ...

  3. 一步一步学习Unity3d学习笔记系1.3 英雄联盟服务器集群架构猜想

    说到了网游那就涉及到服务器了,时下最火的属英雄联盟了,我也是它的粉丝,每周必撸一把,都说小撸怡情,大撸伤身,强撸灰飞烟灭,也告诫一下同仁们,注意身体,那么他的服务器架构是什么呢,给大家分享一下, 具体 ...

  4. Dubbo入门到精通学习笔记(十五):Redis集群的安装(Redis3+CentOS)、Redis集群的高可用测试(含Jedis客户端的使用)、Redis集群的扩展测试

    文章目录 Redis集群的安装(Redis3+CentOS) 参考文档 Redis 集群介绍.特性.规范等(可看提供的参考文档+视频解说) Redis 集群的安装(Redis3.0.3 + CentO ...

  5. 论文学习笔记 - 高光谱 和 LiDAR 融合分类合集

    A³CLNN: Spatial, Spectral and Multiscale Attention ConvLSTM Neural Network for Multisource Remote Se ...

  6. hibernate学习笔记(2)持久化类测试

    持久化类的创建: 创建一个共有的不带参数的构造方法: public void Students(){ } 创建一个带参数的构造方法: (快捷键创建) 生成get,set方法: *可以不用此方法创建持久 ...

  7. Dubbo入门到精通学习笔记(十四):ActiveMQ集群的安装、配置、高可用测试,ActiveMQ高可用+负载均衡集群的安装、配置、高可用测试

    文章目录 ActiveMQ 高可用集群安装.配置.高可用测试( ZooKeeper + LevelDB) ActiveMQ高可用+负载均衡集群的安装.配置.高可用测试 准备 正式开始 ActiveMQ ...

  8. MongoDB 学习笔记(三) MongoDB (replica set) 集群配置

    MongoDB Replica Sets的结构类似于以集群,完全可以把他当成一个集群,因为他确实与集群实现的作用是一样的:如果其中一个节点出现故障,其他的节点会马上将业务接管过来.而无需停机操作 Mo ...

  9. kettle学习笔记(九)——子转换、集群与变量

    一.概述 kettle中3个重要的步骤: 子转换/映射 在转换里调用一个子转换,便于封装和重用. 集群 集群模式 变量和参数 变量和参数的用法 二.子转换 1.定义子转换 主要由映射输入与映射输出定义 ...

  10. 续并查集学习笔记——Closing the farm题解

    在很多时候,并查集并不是一个完整的解题方法,而是一种思路. 通过以下题目来体会并查集逆向运用的思想. Description Farmer John and his cows are planning ...

随机推荐

  1. 图解 | 聊聊 MyBatis 缓存

    首发公众号-悟空聊架构:图解 | 聊聊 MyBatis 缓存 你好,我是悟空. 本文主要内容如下: 一.MyBatis 缓存中的常用概念 MyBatis 缓存:它用来优化 SQL 数据库查询的,但是可 ...

  2. 39.BasicAuthentication认证

    BasicAuthentication认证介绍 BasicAuthentication使用HTTP基本的认证机制 通过用户名/密码的方式验证,通常用于测试工作,尽量不要线上使用 用户名和密码必须在HT ...

  3. 1.pytest入门

    一.pytest单元测试框架 概念:单元测试是指在软件开发中,针对软件的最小单位(函数.方法等)进行正确性的检查测试          单元测试框架是自动化测试框架中的组成部分之一           ...

  4. XAF新手入门 - 前言

    很多小伙伴在第一次接触XAF时,会被它的丰富功能及开箱即用的特点所吸引,即使在不了解XAF的情况下,也能够依葫芦画瓢创建一个功能丰富的应用,但当应用到实际项目中时,你会发现与之前的愿景差距很大,很多都 ...

  5. Jquery对类的操作

    Jquery对类的基本操作 $("#Div_BillSon div").on('click', function (e) { $("#Div_BillSon") ...

  6. nrf52——DFU升级USB/UART升级方式详解(基于SDK开发例程)

    摘要:在前面的nrf52--DFU升级OTA升级方式详解(基于SDK开发例程)一文中我测试了基于蓝牙的OTA,本文将开始基于UART和USB(USB_CDC_)进行升级测试. 整体升级流程: 整个过程 ...

  7. 嵌入式-C语言基础:指针数组(和数组指针区分开来)

    指针数组:一个数组,若其元素均为指针类型的数据,称为指针数组,指针数组存放的是指针类型的数据,也就是指针数组的每个元素都存放一个地址.下面定义一个指针数组: int * p[4];//[]的优先级是比 ...

  8. Prometheus 监测 RocketMQ 最佳实践

    本文作者:郭雨杰,阿里云智能技术专家. Prometheus 集成的 50 多款云产品中,RocketMQ 在可观测方面实现了非常完善的功能,是一个特别具有代表性的云产品. 01 RocketMQ如何 ...

  9. 深入理解Golang 闭包,直通面试

    大家好 今天为大家讲解的面试专题是: 闭包. 定义 闭包在计算机科学中的定义是:在函数内部引用了函数内部变量的函数. 看完定义后,我陷入了沉思...确实,如果之前没有接触过闭包或者对闭包不理解的话,这 ...

  10. C#使用Task在Winform建立控件上的提示等待窗口,实现局部等待加载,不影响主线程(二)

    效果图: 源码:(处理了亿点点细节) 链接:https://pan.baidu.com/s/18S1IgQBOyXgeGvhnU3nrKQ?pwd=jpq9提取码:jpq9 作者:兮去博客出处:htt ...