【无旋treap hash】匹配(2022.5.21)
上题目!
题目
1.1 题目描述
给定一个仅含小写字母的字符串 S[0..n-1],对于一个询问 (p, q, len),我们想知道它的两个子串 S[p..p+len-1]、S[q..q+len-1] 是否相同。更多地,我们希望在对串 S 完成一些操作之后还能高效地得到这个结果。 我们具体要维护以下几个操作(其中 L 为操作之前的串长):
● 1 p c:在下标 p 之前插入一个小写字母 c,0<=p<=L,p=0 意味在头插入,p=L意味在尾插入;
● 2 p: 删除 S[p],其中 0<=p<L;
● 3 p q: 将 S[p..q] 翻转为 S[q..p],例如 batta 经过 “3 1 3” 之后为 bttaa;
● 4 p q len: 表示一次询问操作,询问 S[p..p+len-1] 与 S[q..q+len-1] 是否相同,其中 0<=p<=p+len-1<L,0<=q<=q+len-1<L。
1.2 输入格式
第 1 行为两个整数 n, m,分别表示字符串的初始长度和操作次数;
第 2 行为一个长度为 n 的字符串;
接下来 m 行,每行为一个操作。
1.3 输出格式
仅一行一个整数,表示询问相同的次数。
1.4 样例输入
4 4
aacb
2 2 // aab
1 2 b // aabb
3 1 2 // abab
4 0 2 2 // ab == ab
1.5 样例输出
1.6 数据范围与约定
对于前20%的数据满足n,m≤1000;
对于前70%的数据满足仅含有3、4 两种操作;
对于100%的数据满足1≤n, m≤200000。`
第一眼看就知道是打treap或者splay的模板题由于本人过菜根本不想打又臭又长的旋转树
于是!就有了这么一个FHQ无旋treap树!(FHQ大佬%%%)
FHQ无旋treap
其树的核心在于split(分裂)和merge(合并)两个操作
先来看以val(树的大小)划分的分裂:
inline void split(int nown,int &l,int &r,int val){
if(!nown){
l=r=0;//分到底
return ;
}
if(size[ls[nown]]+1<=val)l=nown,split(rs[nown],rs[nown],r,val-size[ls[nown]]-1);//当前节点的size小于等于分裂值,分到左边
else r=nown,split(ls[nown],l,ls[nown],val);//同上,分到右边
update(nown);
return ;
}
注意这么一个回传值的写法
再看合并:
inline void merge(int &k,int l,int r){
if(l==0||r==0){
k=l+r;//到底,选择其中不为0的节点回传
return ;
}
if(num[l]>num[r])k=l,merge(rs[l],rs[l],r);//num即满足节点大根堆性质的rand()值
else k=r,merge(ls[r],l,ls[r]);
update(k);
return ;
}
在某一个位置加入或删除节点即按照其节点位置前后分裂,加入或删除这个节点后合并
其他的操作都和普通treap大同小异
关于HASH
此题需要我们同时维护两个hash值,因为需要交换当前节点的子节点,所以要维护当前节点未交换和交换了的hash值,即:
维护以该结点为根的子树的中序字符串的哈希值(记为hash[u][0]),以及该字符串翻转后的哈希值(记为hash[u][1]),
则有:hash[u][0] = hash[child[u][0]][0]*pow[size[child[u][1]]+1]+
character[u]*pow[size[child[u][1]]]+hash[child[u][1]][0],
其中child[u][0/1]表示u的左/右儿子,size[u]表示以u为根的子树大小,pow[i]表示哈希的进制BASE的i次方。
hash[u][1]的计算同理,有:hash[u][1] = hash[child[u][0]][1]
+character[u]*pow[size[child[u][0]]]+hash[child[u][1]][1]*pow[size[child[u][0]]+1]。
交换一个节点的左右儿子时,不仅要交换它的左右儿子,也要交换它的hash[u][0]和hash[u][1]。
一些小细节
关于写tag标记是否旋转tag最好记录当前节点的子节点是否需要旋转否则计算hash会出错这件事