【無旋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會出錯這件事