資料結構專題-學習筆記:莫隊#3(回滾莫隊,莫隊二次離線)
回顧:
一個莫隊,六種方法(普通莫隊、帶修莫隊、樹上莫隊、樹上帶修莫隊、回滾莫隊/不刪除莫隊、莫隊二次離線/第十四分塊(前體)),連續寫了三篇博文來講述。本篇博文是最後一篇,將會講述最後兩種莫隊:[回滾莫隊/不刪除莫隊] [莫隊二次離線/第十四分快(前體)],同時將會總結六種莫隊演算法。
3.練習題
題單:(前面的就略去了)
- 回滾莫隊/不刪除莫隊
- AT1219 歴史の研究
- 莫隊二次離線/第十四分塊(前體)
- P4887 【模板】莫隊二次離線(第十四分塊(前體))
回滾莫隊/不刪除莫隊
學到了這裡,各位會發現:莫隊支援查詢,修改,也支援刪除(當然不是直接刪除一個數)。但是,對於一些題目,普通莫隊就過不去了,因為你會發現:del 函式非常難寫,可能寫著寫著就變成了 \(O(n)\)
事實上,我個人認為這道題更像回滾莫隊/不刪除莫隊的模板題,因此我拿它來做講解。
這題的 add 函式很好打,但是 del 函式很不好打,因此我們要另尋他法。
我們回顧一下莫隊的三個優化:
優化一:使用 \(cnt\) 陣列省略值域。貌似沒有什麼用處qwq。
優化二:使用 \(l,r\) 指標移來移去。帶修莫隊已經用過了。
優化三:應用分塊思想進行排序,不使用奇偶性排序的情況下左端點在同一塊的詢問右端點按照從小到大排序。唉等等,這個優化好像還沒有用過,我們是不是可以使用這個優化寫出對回滾莫隊/不刪除莫隊呢?當然可以!
考慮分塊思想。設 \(block\) 表示塊長,\(bnum\) 表示塊的數量,那麼對於每一塊塊內的詢問,直接暴力出奇跡即可。
那麼針對於那些跨越塊數的呢?
想想當初分塊的時候,我們順序處理整塊,那麼這裡我們也順序處理一下:
對於第 \(i\) 快,我們指定 \(l=i*block+1,r=l-1\) 。根據上面所述, \(r\) 只需要往右邊移動即可,但是不能保證 \(l\) 只往左移動,因此我們還得想辦法解決 \(l\) 的問題。
這裡就是一個指標移動順序的問題了。
我們有四個順序:\(l++,l--,r--,r++\) ,前面四種莫隊隨便亂搞都能過,但是回滾莫隊/不刪除莫隊不能隨便亂搞。首先,\(r--\)
因為這裡我們要保證 “不刪除”,所以我們不能在 \(l++\) 的時候維護答案。最好的辦法就是將 \(l\) 移回到 \(i*block+1\) ,同時由於我們先處理了 \(r++\) ,因此我們可以保證 \(tmp\) 是 \([i*block+1,r]\) 之間的答案。因此,將 \(l\) 移回去,同時路上消除 \(cnt\) 的影響(要統計個數),而 \(total=tmp\) 能夠保證在 “不刪除” 的情況下儲存答案。
以上就是回滾莫隊/不刪除莫隊的主要思路。
程式碼裡面需要注意:暴力小塊時用的 \(cnt\) 不能與莫隊大塊時用的 \(cnt\) 一起,否則很容易導致 WA。同時絕對不能使用奇偶性排序!
說每暴力一次都 memset 的人可以看一看這組資料:
n=10000,block=sqrt(n)=100;
詢問:[1,2][2,3]······
詢問 9999 個,每一次都memset一遍,時間複雜度直接飆升到 O(n^2)
TLE 在向你招手!
不要忘記 long long ,離散化。
程式碼:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+10;
typedef long long LL;
int n,m,a[MAXN],b[MAXN],block,ys[MAXN],lastn,bnum;
LL total,ans[MAXN];
map<int,int>cnt,cnt2;//懶得寫離散化,用 map 代替
struct node
{
int l,r,id;
}q[MAXN];
int read()
{
int sum=0;char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
return sum;
}
bool cmp(const node &fir,const node &sec)
{
if(ys[fir.l]^ys[sec.l]) return ys[fir.l]<ys[sec.l];
return fir.r<sec.r;
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;i++) cnt[a[i]=read()]=0;
block=sqrt(n);bnum=ceil((double)n/block);
for(int i=1;i<=n;i++) ys[i]=(i-1)/block+1;
for(int i=1;i<=m;i++) {q[i].l=read();q[i].r=read();q[i].id=i;}
sort(q+1,q+m+1,cmp);
int j=1;
for(int i=1;i<=bnum;i++)
{
cnt.clear();
int l=i*block+1,r=l-1;
LL tmp=0;total=0;
for(;ys[q[j].l]==i;j++)
{
if(ys[q[j].l]==ys[q[j].r])
{
cnt2.clear();tmp=0;
for(int k=q[j].l;k<=q[j].r;k++) {cnt2[a[k]]++;tmp=max(tmp,1ll*a[k]*cnt2[a[k]]);}
ans[q[j].id]=tmp;
continue;
}
while(r<q[j].r) {++r;cnt[a[r]]++;total=max(total,1ll*a[r]*cnt[a[r]]);}
tmp=total;
while(l>q[j].l) {--l;cnt[a[l]]++;total=max(total,1ll*a[l]*cnt[a[l]]);}
ans[q[j].id]=total;
while(l<i*block+1) {cnt[a[l]]--;l++;}
total=tmp;
}
}
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
莫隊二次離線/第十四分塊(前體)
參照這篇博文:演算法學習筆記:再談莫隊二次離線
4.總結
終於到總結了qwq。
如果真的掌握了莫隊,莫隊其實還是很簡單的。
這裡再放一放 6 種莫隊的主要思路吧!
- 普通莫隊:兩個指標 \(l,r\) 在序列上動,排序以左端點所在塊為第一關鍵字,右端點為第二關鍵字排序,可以使用奇偶性優化加快排序。
- 帶修莫隊:加一維指標 \(t\) ,讓第三個指標在時間軸上動。
- 樹上莫隊:用尤拉序將樹上問題轉變成區間問題
- 樹上帶修莫隊:前兩者的結合,用尤拉序轉變成區間問題後使用帶修莫隊的套路。
- 回滾莫隊/不刪除莫隊:藉助分塊思路,塊內暴力,塊外指標移動,同時記錄 \([i*block+1,r]\) 的答案,但是絕對不能使用奇偶性排序。
- 莫隊二次離線/第十四分塊(前體):答案可以轉換為 \(h([l,r])=h([1,r])-h([1,l-1])-g([1,l-1],[l,r])\)。
最後再放一下三篇博文的連結:
莫隊演算法總結&專題訓練1:普通莫隊。
莫隊演算法總結&專題訓練2:帶修莫隊,樹上莫隊,樹上帶修莫隊。
莫隊演算法總結&專題訓練3:回滾莫隊/不刪除莫隊,莫隊二次離線/第十四分塊(前體)。
有興趣的讀者也可以看一下 洛穀日報 #183 期:你以為莫隊只能離線?莫隊的線上化改造,增強對莫隊的理解與自己的水平。(所以以後很多樹套樹的題目都能用線上莫隊切掉了?)