浅谈 OI 中各种合并操作
前言
合并操作一直是 OI 中一大考点,今天请各位跟着笔者来梳理一下各种合并操作。
启发式合并
几乎可以说是最经典的合并了。
假定我们可以在 \(O(k)\) 的时间内往某个集合中插入一个数,那么我们就可以在 \(O(n \log n k)\) 的时间内合并若干个元素总量为 \(n\) 的集合。
集合启发式合并
[NOI2022] 众数
看到查询绝对众数我们便想到一个方法:
用桶记录每个元素出现次数,查询时从序列中随机抽取 \(\log q\) 个数验证是否是绝对众数。
易证这种做法期望是正确的。这里略去。
然后对于在末尾插入删除以及拼接多个序列,我们可以用双端队列维护。
但是在拼接序列是怎么插入元素,暴力插入元素是 \(O(nq)\) 的。我们可以把较短的序列中的元素暴力插入到较长的序列中。
但是这么做的复杂度有保证吗?
注意到每次把一个元素插入到另一个序列中,花费了 \(O(1)\) 的时间(哈希表和双端队列均可以 \(O(1)\) 插入),而且这个操作使这个元素所在的序列长度至少翻了一倍,又因为总共只有 \(n\) 各元素,所以序列长度至多为 \(n\),所以一个元素最多被插入 \(\log n\) 次。
那么合并操作的总复杂度就是 \(O(n \log n)\)
参考代码
#include<bits/stdc++.h>
#include<bits/extc++.h>
using namespace std;
namespace IO{
const int SIZE=1<<21;
static char ibuf[SIZE],obuf[SIZE],*iS,*iT,*oS=obuf,*oT=oS+SIZE-1;
int qr;
char qu[55],c;
bool f;
#define getchar() (IO::iS==IO::iT?(IO::iT=(IO::iS=IO::ibuf)+fread(IO::ibuf,1,IO::SIZE,stdin),(IO::iS==IO::iT?EOF:*IO::iS++)):*IO::iS++)
#define putchar(x) *IO::oS++=x,IO::oS==IO::oT?flush():0
#define flush() fwrite(IO::obuf,1,IO::oS-IO::obuf,stdout),IO::oS=IO::obuf
#define puts(x) IO::Puts(x)
template<typename T>
inline void read(T&x){
for(f=1,c=getchar();c<48||c>57;c=getchar())f^=c=='-';
for(x=0;c<=57&&c>=48;c=getchar()) x=(x<<1)+(x<<3)+(c&15);
x=f?x:-x;
}
template<typename T>
inline void write(T x){
if(!x) putchar(48); if(x<0) putchar('-'),x=-x;
while(x) qu[++qr]=x%10^48,x/=10;
while(qr) putchar(qu[qr--]);
}
inline void Puts(const char*s){
for(int i=0;s[i];i++)
putchar(s[i]);
putchar('\n');
}
struct Flusher_{~Flusher_(){flush();}}io_flusher_;
}
using IO::read;
using IO::write;
const int maxn = 5e5+1;
const int zbz = 22;
int tot;
int n,q;
class hhx{
public:
__gnu_pbds::gp_hash_table<int,int> warma;
int L,R;
inline void push_back(int x);
inline void push_front(int x);
inline void pop_back();
inline void pop_front();
inline int back();
inline int front();
inline int rd();
inline int size();
};
inline void hhx::push_back(int x){
warma[++R]=x;
}
inline void hhx::push_front(int x){
warma[--L]=x;
}
inline void hhx::pop_back(){
--R;
}
inline void hhx::pop_front(){
++L;
}
inline int hhx::back(){
return warma[R];
}
inline int hhx::front(){
return warma[L];
}
inline int hhx::rd(){
return warma[L+rand()%(R-L+1)];
}
inline int hhx::size(){
return R-L+1;
}
struct Node{
__gnu_pbds::gp_hash_table<int,int> cnt;//出现次数
hhx lwx;
}chifan[maxn];
__gnu_pbds::gp_hash_table<int,int> xzy;
inline void insert(int pos,int x,bool type){
++chifan[pos].cnt[x];
if(type==0)
chifan[pos].lwx.push_back(x);
else
chifan[pos].lwx.push_front(x);
}
inline void del(int pos){
int d=chifan[pos].lwx.back();
chifan[pos].lwx.pop_back();
--chifan[pos].cnt[d];
}
inline void merge(int x1,int x2,int x3){
int f=0;
if(chifan[x1].lwx.size()<chifan[x2].lwx.size()){
f=1;
swap(x1,x2);
}
for(int u,i=chifan[x2].lwx.L;i<=chifan[x2].lwx.R;i++){
u=chifan[x2].lwx.warma[i];
insert(x1,u,f);
}
xzy[x3]=x1;
}//这里是启发式合并
vector<int> X;
vector<int> wyb;
inline int query(){
int m;
read(m);
X.clear();
wyb.clear();
for(int i=1;i<=m;i++){
int x;
read(x);
x=xzy[x];
X.push_back(x);
}
int sum=0;
for(int u:X){
sum+=chifan[u].lwx.size();
}
for(int i=1;i<=zbz;i++){
int pos=rand()%sum+1;
int v=0,s=0;
for(int v1:X){
s+=chifan[v1].lwx.size();
if(s>=pos){
v=v1;
break;
}
}
wyb.push_back(chifan[v].lwx.rd());
}
for(int u:wyb){
int s=0;
for(int v:X){
s+=chifan[v].cnt[u];
}
if((s<<1)>sum){
return u;
}
}
return -1;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
srand(time(0));
read(n);
read(q);
for(int i=0;i<=n;i++) chifan[i].lwx.L=1,chifan[i].lwx.R=0;
for(int i=1;i<=n;++i){
xzy[i]=i;
int m;
read(m);
for(int j=1;j<=m;++j){
int x;
read(x);
insert(i,x,0);
}
}
for(int i=1;i<=q;++i){
int opt;
read(opt);
if(opt==1){
int x,y;
read(x);
read(y);
x=xzy[x];
insert(x,y,0);
}
else if(opt==2){
int x;
read(x);
x=xzy[x];
del(x);
}
else if(opt==3){
write(query());
putchar('\n');
}
else{
int x1,x2,x3;
read(x1);
read(x2);
read(x3);
x1=xzy[x1];
x2=xzy[x2];
merge(x1,x2,x3);
}
}
}
树上启发式合并
树上启发式合并多用于解决对子树的询问。
这个虽然本质上与集合启发式合并一致,但是在实现上却有很大的不同。
具体的思路是让父节点继承节点最多的儿子(重儿子)的信息,在把其他的儿子(轻儿子)的信息暴力插入。
但是这么做空间复杂度是 \(O(n \log n)\) 怎么办?
答案是让全局节点信息公用一个集合,每次按如下流程操作:
先递归求解这个点的所有轻儿子并求解对于它们的询问。不保留它们的信息。
递归求解这个点的重儿子并求解对于它们的询问。保留它们的信息。
将这个节点的轻儿子子树内的信息插入集合并回答对于当前节点的询问。
这么做时间复杂度还是 \(O(n \log n)\) 但是空间复杂度却变成了 \(O(n)\)。
树形结构合并
线段树合并
[Vani有约会]雨天的尾巴 /【模板】线段树合并
我们通过差分可以把问题转变为子树内众数(注意,这里可能会有某个点需要删除一个数,所以不可以单纯地用桶维护),这个可以用权值线段树维护,可是怎么讲子节点的信息合并到父节点呢?
当然,你可以直接树上启发式合并,这么做是 \(O(n \log^2 n)\) 的,有没有更好的解法?
首先,我们可以将权值线段树变成动态开点权值线段树(不同的同学请先学习动态开点)。这样就保证一个大小为 \(u\) 的集合线段树上至多有 \(u \log n\) 个节点。
然后考虑怎么合并两棵线段树。
我们可以递归进行,假设我们要合并两个节点,先分别合并这两个节点的左右儿子,再更新这个节点的信息。
以及假若这两个节点有一个节点为空,直接返回另一个节点作为合并结果。
写出代码就是这样:
int merge(int a,int b,int l,int r){
if(a==0||b==0) return a+b;
if(l==r){
tree[a].cnt+=tree[b].cnt;
tree[a].val=l;
return a;
}
int mid=(l+r)/2;
tree[a].ls=merge(tree[a].ls,tree[b].ls,l,mid);
tree[a].rs=merge(tree[a].rs,tree[b].rs,mid+1,r);
pushup(a);
return a;
}
这个复杂度看上去很鬼畜,但是对的,为啥?
我们发现每调用一次 \(merge\) 函数那么就会合并两个不同的节点,也就是说节点总数就会减一。
那么又因为总共只有 \(n \log n\) 个节点,所有这个函数至多被调用 \(n \log n\) 次。
那么我们就 \(O(n \log n)\) 地做完了。
参考代码
#include<bits/stdc++.h>
using namespace std;
const int inf = 2e5;
int n,q;
const int maxn = 2e5+114;
vector<int> Add[maxn*2],Del[maxn*2];
int ans[maxn];
int tot;
int root[maxn];
int fa[maxn][18];
int depth[maxn];
int lg[maxn];
vector<int> edge[maxn];
struct Node{
int ls,rs,val,cnt;
}tree[maxn * 20];
void pushup(int &cur){
if(tree[tree[cur].ls].cnt<tree[tree[cur].rs].cnt){
tree[cur].cnt=tree[tree[cur].rs].cnt;
tree[cur].val=tree[tree[cur].rs].val;
}
else if(tree[tree[cur].rs].cnt<tree[tree[cur].ls].cnt){
tree[cur].cnt=tree[tree[cur].ls].cnt;
tree[cur].val=tree[tree[cur].ls].val;
}
else{
tree[cur].cnt=tree[tree[cur].ls].cnt;
tree[cur].val=min(tree[tree[cur].ls].val,tree[tree[cur].rs].val);
}
}
void addtag(int &cur,int lt,int rt,int l,int r,int v){
if(lt>r||rt<l) return ;
if(cur==0){
cur=++tot;
}
if(lt==rt){
tree[cur].cnt+=v;
tree[cur].val=lt;
return ;
}
int mid = (lt+rt)/2;
addtag(tree[cur].ls,lt,mid,l,r,v);
addtag(tree[cur].rs,mid+1,rt,l,r,v);
pushup(cur);
}
int merge(int a,int b,int l,int r){
if(a==0||b==0) return a+b;
if(l==r){
tree[a].cnt+=tree[b].cnt;
tree[a].val=l;
return a;
}
int mid=(l+r)/2;
tree[a].ls=merge(tree[a].ls,tree[b].ls,l,mid);
tree[a].rs=merge(tree[a].rs,tree[b].rs,mid+1,r);
pushup(a);
return a;
}
inline void add(int u,int v){
edge[u].push_back(v);
edge[v].push_back(u);
}
inline void dfs1(int now,int fath){
fa[now][0]=fath;
depth[now]=depth[fath] + 1;
for(int i=1;i<=lg[depth[now]];++i)
fa[now][i] = fa[fa[now][i-1]][i-1];
for(int nxt:edge[now]){
if(nxt==fath) continue;
dfs1(nxt,now);
}
}
int LCA(int x,int y){
if(depth[x] < depth[y])
swap(x, y);
while(depth[x] > depth[y])
x=fa[x][lg[depth[x]-depth[y]]- 1];
if(x==y)
return x;
for(int k=lg[depth[x]]-1; k>=0; --k)
if(fa[x][k] != fa[y][k])
x=fa[x][k],y=fa[y][k];
return fa[x][0];
}
void change(int u,int v,int z){
Add[u].push_back(z);
Add[v].push_back(z);
int w=LCA(u,v);
Del[w].push_back(z);
Del[fa[w][0]].push_back(z);
}
void dfs2(int now,int fa){
for(int nxt:edge[now]){
if(nxt==fa) continue;
dfs2(nxt,now);
root[now]=merge(root[now],root[nxt],1,inf);
}
pushup(root[now]);
for(int c:Add[now]){
addtag(root[now],1,inf,c,c,1);
}
for(int c:Del[now]){
addtag(root[now],1,inf,c,c,-1);
}
ans[now]=tree[root[now]].val;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>q;
for(int i = 1; i <= n; ++i)
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
add(u,v);
}
dfs1(1,0);
for(int i=1;i<=q;i++){
int u,v,z;
cin>>u>>v>>z;
change(u,v,z);
}
dfs2(1,0);
for(int i=1;i<=n;i++) cout<<ans[i]<<'\n';
}
Trie 合并
[省选联考 2020 A 卷] 树
转化题意便知道我们需要在每个点上用一种数据结构维护全局加一和异或和,很自然地想到用 01trie 维护,具体怎么维护限于篇幅就不赘述了,现在我们只考虑怎么把子树内 01trie 合并到父节点的问题。
类似于线段树合并一样的思想,首先我们要让 01trie 变成动态开点式的,然后在合并时依然是先合并左右儿子的信息,再更新节点本身的信息。
复杂度分析和线段树合并类似,都是 \(O(n \log n)\) 的。
不过这里给读者多留一个问题:压位 trie 合并能否实现?倘若能实现,其复杂度是否是 \(O(n \log_{w} n)\)?
参考代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e6+114;
int anser,tot;
int col[maxn];
int n;
vector<int> edge[maxn];
struct Node{
int ls,rs,v,val;
int cnt;
}trie[maxn*40];
queue<int> is;
int root[maxn];
void pushup(int &cur,int pos){
if(cur!=pos)
trie[cur].val=((trie[cur].cnt&1)?trie[cur].v:0)+((trie[trie[cur].ls].val^trie[trie[cur].rs].val)<<1);
else
trie[cur].val=(trie[trie[cur].ls].val^trie[trie[cur].rs].val);
}
void insert(int &cur,int pos){
if(is.size()==0) return;
if(cur==0){
cur=++tot;
}
if(cur!=pos){
trie[cur].v=(is.front()&1),trie[cur].cnt++;
is.pop();
}
if(!(is.front()&1)) insert(trie[cur].ls,pos);
else insert(trie[cur].rs,pos);
pushup(cur,pos);
}
int merge(int &a,int &b,int pos){
if(a==0||b==0) return a+b;
trie[a].cnt+=trie[b].cnt;
trie[a].ls=merge(trie[a].ls,trie[b].ls,pos);
trie[a].rs=merge(trie[a].rs,trie[b].rs,pos);
pushup(a,pos);
return a;
}
void add(int &cur,int pos){
if(cur==0){
return ;
}
swap(trie[cur].ls,trie[cur].rs);
if(trie[cur].ls!=0)
trie[trie[cur].ls].v=0;
if(trie[cur].rs!=0)
trie[trie[cur].rs].v=1;
add(trie[cur].ls,pos);
pushup(trie[cur].ls,pos);
pushup(trie[cur].rs,pos);
pushup(cur,pos);
return ;
}
void chifan(int x){
while(is.size()>0) is.pop();
while(x!=0){
is.push(x&1);
x>>=1;
}
while(is.size()<22) is.push(0);
return ;
}
void dfs(int u,int fa){
for(int v:edge[u]){
if(v==fa) continue;
dfs(v,u);
merge(root[u],root[v],u);
}
chifan(col[u]);
insert(root[u],u);
anser+=trie[root[u]].val;
add(root[u],u);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i=-~i){
cin>>col[i];
root[i]=++tot;
}
for(int i=2;i<=n;i=-~i){
int v;
cin>>v;
edge[i].push_back(v);
edge[v].push_back(i);
}
dfs(1,0);
cout<<anser;
}
浅谈 OI 中各种合并操作的更多相关文章
- [技术]浅谈OI中矩阵快速幂的用法
前言 矩阵是高等代数学中的常见工具,也常见于统计分析等应用数学学科中,矩阵的运算是数值分析领域的重要问题. 基本介绍 (该部分为入门向,非入门选手可以跳过) 由 m行n列元素排列成的矩形阵列.矩阵里的 ...
- 浅谈OI中的提交答案
在OI中,题目有三类: 传统题 交互题 提交答案题 今天来了解一下第三类 概述 传统题:给你一个题面,你需要交一个程序,评测姬会用你的程序运行你看不到的一些测试点,用输出和正确答案比较 提交答案题:给 ...
- 浅谈OI中的底层优化!
众所周知,OI中其实就是算法竞赛,所以时间复杂度非常重要,一个是否优秀的算法或许就决定了人生,而在大多数情况下,我们想出的算法或许并不那么尽如人意,所以这时候就需要一中神奇的的东西,就是底层优化: 其 ...
- 浅谈JavaScript中的字符串操作
我想,最为一名开发人员,最实际开发过程中,任何一门语言在开发实际的项目的过程中,都是逃不开字符串的操作的下面笔者就自己日常开发过程中所用到的一些字符串的操作方法做一些陈述和总结,当然,如若读者觉得 ...
- 浅谈.net中数据库操作事务
.net中的事务 关键几点 概念:1:什么是事务 2:什么时候用事务 3:基本的语法 (1): 事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit).事务通常 ...
- 浅谈Linux中的信号处理机制(二)
首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...
- 浅谈Java中的对象和引用
浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...
- 浅谈C中的malloc和free
转自http://bbs.bccn.net/thread-82212-1-1.html非常感谢作者 浅谈C中的malloc和free 在C语言的学习中,对内存管理这部分的知识掌握尤其重要!之前对C中的 ...
- 【转】浅谈Java中的hashcode方法(这个demo可以多看看)
浅谈Java中的hashcode方法 哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native i ...
- 【转】浅谈Java中的hashcode方法
哈希表这个数据结构想必大多数人都不陌生,而且在很多地方都会利用到hash表来提高查找效率.在Java的Object类中有一个方法: public native int hashCode(); 根据这个 ...
随机推荐
- mfc edit只允许输入数字
1.给EDIT控件添加 EN_CHANGE 事件 2.事件中的代码如下: 1 CString strEditVidoe; 2 GetDlgItem( iId )->GetWindowText( ...
- Sitecore 应用与介绍
前言 因为工作需要,开始了 sitecore 之旅,在使用之中碰到了许多问题,后续开始写一下关于 sitecore 的文章. sitecore 官网:https://www.sitecore.com/ ...
- DVWA-Command Injection(命令执行)
命令执行漏洞,顾名思义,服务端在进行一些网站的操作.管理的时候,需要调用系统命令,如果对传入的命令参数没有进行一些过滤,可以直接执行服务器系统的命令终端 LOW 审计源码 <?php // 判断 ...
- 22.this指针
1.this指针工作原理 我们知道,c++的数据和操作也是分开存储,并且每一个非内联成员函数(non-inline member function)只会诞生一份函数实例,也就是说多个同类型的对象会共用 ...
- 2022年8月学科能力综合测试(TACA)试题解答 Mathemaitca练习
目录 试题地址 1 4 5 10 13 你让我猜我肯定这么猜 试题地址 https://k.sina.com.cn/article_2897328623_acb1b9ef019011qen.html ...
- MySQL 主从复制的问题及解决方案
更多内容,前往 IT-BLOG 复制功能是构建 MySQL 的大规模.高性能的基础,也就是所谓的 "水平扩展" 架构.我们可以通过为服务器配置一个或多个备库.同时,复制也是高可用性 ...
- 移动端测试辅助工具 - adb
1. 概念: adb(android debug bridge)是android提供的基于CS架构的命令行调试工具,使PC与安卓设备之间实现通信 2. 基础原理: 交互图: 主要由三部分组成: adb ...
- SQL Server底层架构技术对比
背景 数据库是信息化的基石,支撑着整个业务系统,发挥着非常重要的作用,被喻为"IT的心脏".因此,让数据库安全.稳定.高效地运行已经成为IT管理者必须要面对的问题.数据库在底层架构 ...
- 【Deep Learning】DDPM
DDPM 1. 大致流程 1.1 宏观流程 1.2 训练过程 1.3 推理过程 2. 对比GAN 2.1 GAN流程 2.2 相比GAN优点 训练过程更稳定,损失函数指向性更强(loss数值大小指示训 ...
- 【Note】倍增
真的不会.QAQ 目录 简介 大家都见过的应用:倍增求 \(\text{LCA}\) 倍增求 \(\text{LCA}\) ,但是动态加点,但是不会 \(lct\) 例题:[ZJOI2012]灾难(D ...