1. 程式人生 > 其它 >學習筆記——莫隊進階

學習筆記——莫隊進階

前言

發現至今沒有系統地學過莫隊。。。

普通莫隊一般人都會,就一分塊暴力。

題單 以及 dx 的訓練題單 以及 dx 的雙倍經驗題單

奇怪的碎碎念
本文的題目基本來自於上面的題單,文末的 Tasks 模組是trashbin前面每個模組裡看起來比較綜合或者難寫的題。相當於作業?以及只有板子題和困難題的關鍵部分會放程式碼,不然就太長了/qd。

帶修莫隊

考慮怎麼樣使得莫隊帶修,事實上你加一維時間(也就是對於每次詢問記錄最近一次修改,然後作為關鍵字之一進行排序)。

考慮塊長,一般莫隊的塊長是 \(\sqrt{n}\) 的,但是在帶修的時候,由於多加了一維,\(\sqrt{n}\) 可能被卡到 \(O(n^2)\)

,所以塊長需要調到 \(n^{\frac{2}{3}}\)

醬紫就可以使莫隊支援修改了。

例題:

模板題。

My Code
const int MAXN=2e5+10;
int cnt[MAXN<<3],cur,a[MAXN];
void add(int i){if(!cnt[a[i]])cur++;cnt[a[i]]++;}
void dec(int i){cnt[a[i]]--;if(!cnt[a[i]])cur--;}
struct Update{int p,col;};
vector<Update> upd;
int B;
struct Query{
	int l,r,pre,id;
	bool friend operator<(Query a,Query b){
		return (a.l/B==b.l/B)?((a.r/B==b.r/B)?a.pre<b.pre:a.r<b.r):a.l<b.l;
	}
};
vector<Query> q;
int ans[MAXN];
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,m;cin>>n>>m;
	rep(i,1,n) cin>>a[i];
	rep(i,1,m){
		char op;cin>>op;
		if(op=='Q'){
			int l,r;cin>>l>>r;int id=q.size();
			q.pb((Query){l,r,upd.size()-1,id});
		}else{
			int p,col;cin>>p>>col;
			upd.pb((Update){p,col});
		}
	}
	B=pow(n,2.0/3);
	sort(q.begin(),q.end());
	int l=1,r=0,t=-1;
	for(auto c:q){
		while(l<c.l) dec(l++);
		while(r>c.r) dec(r--);
		while(l>c.l) add(--l);
		while(r<c.r) add(++r);
		while(t<c.pre){
			t++;if(upd[t].p>=l&&upd[t].p<=r) dec(upd[t].p);
			swap(upd[t].col,a[upd[t].p]);
			if(upd[t].p>=l&&upd[t].p<=r) add(upd[t].p);
		}while(t>c.pre){
			if(upd[t].p>=l&&upd[t].p<=r) dec(upd[t].p);
			swap(upd[t].col,a[upd[t].p]);
			if(upd[t].p>=l&&upd[t].p<=r) add(upd[t].p);t--;
		}ans[c.id]=cur;
	}
	rep(i,0,(int)q.size()-1) cout<<ans[i]<<'\n';
	return 0;
}

求區間出現次數的 mex,如果是暴力維護的話就是記錄一個 \(cnt_i\)\(cnt_{cnt_i}\),然後每次看是否要改當前答案,然後再暴力向上檢查是否可行。這樣的做法可以被卡掉,於是我們考慮優化這個做法。其實我們不一定要一邊更新一邊求答案,可以最後一起求。考慮這個一起求答案的複雜度,如果要 \(1\sim n\) 都出現,那實際上需要 \(r-l+1\ge \dfrac{n(n-1)}{2}\),也就是說,暴力次數是不超過 \(\sqrt{n}\) 的,不構成瓶頸。

考慮和上面那題一樣,維護一個出現 \(i\)

次的數有多少個,然後統計答案就是找到最接近的 \(l,r\) 使得 \(\sum\limits_{i=l}^rcnt_i\ge k\)。這時候容易想到可以用雙指標維護,但是如果直接暴力跑的話顯然是不行的。那從上一題得到啟發,\(cnt_i>0\) 的個數是不超過 \(\sqrt{n}\) 的,所以如果只遍歷 \(cnt_i>0\) 的複雜度就是對的了(反正等於 \(0\) 不產生貢獻)。於是我們用連結串列維護這個大於 \(0\)\(cnt_i\),每次向連結串列尾加入,然後每次遍歷連結串列摳出來,排序,直接尺取就可以了。

回滾莫隊

考慮到莫隊可以做區間數顏色,那我們可不可以讓他像其他資料結構一樣呢?比如說,區間最值?

發現傳統莫隊在求最值的時候沒有辦法很好地刪除一個元素,所以我們考慮能不能讓莫隊不刪除呢?這就是回滾莫隊所解決的問題。

它的主要思想就是,對於每個塊內,所有詢問的右端點都是單調遞增的。這樣我們如果順次訪問這些詢問,右端點是不需要刪除的。於是我們就發明了回滾莫隊:首先對於每個塊,先預處理一下兩個端點都在這個塊內的情況,單次複雜度是 \(O(\sqrt n)\) 的。然後我們搞兩個指標 \(l,r\) 指在塊尾以及塊尾的下一個。然後對於一個右端點,左端點都暴力往前掃一遍,單次複雜度是 \(O(\sqrt n)\) 的。所以均攤下來複雜度還是 \(O(n\sqrt n)\)。這就是回滾莫隊之不刪除莫隊

例題:

先離散化然後記錄每個元素下標的最大和最小值就可以了。

My Code
const int MAXN=2e5+10;
int a[MAXN],lsh[MAXN],B;
inline int bl(int id){return (id-1)/B+1;}
struct Query{
	int l,r,id;
	void input(int i){cin>>l>>r;id=i;}
	bool friend operator<(Query x,Query y){
		return bl(x.l)==bl(y.l)?x.r<y.r:x.l<y.l;
	}
}q[MAXN];
int mx[MAXN],mn[MAXN],mx2[MAXN],mn2[MAXN],ans[MAXN];
int cur=0;
void add(int i){
	mx[a[i]]=max(mx[a[i]],i);
	mn[a[i]]=min(mn[a[i]],i);
	cur=max(cur,mx[a[i]]-mn[a[i]]);
}
struct Cancel{int i,mx,mn;};
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n;cin>>n;
	rep(i,1,n) cin>>a[i],lsh[i]=a[i];
	sort(lsh+1,lsh+1+n);
	int Cnt=unique(lsh+1,lsh+1+n)-lsh-1;
	rep(i,1,n) a[i]=lower_bound(lsh+1,lsh+1+Cnt,a[i])-lsh;
	B=sqrt(n);
	int m;cin>>m;
	rep(i,1,m) q[i].input(i);
	sort(q+1,q+1+m);
	for(int l=1,r;l<=m;l=r+1){
		r=l;while(r<m&&bl(q[r+1].l)==bl(q[l].l)) r++;
		memset(mn,0x3f,sizeof(mn));
		memset(mx,0,sizeof(mx));
		int blid=bl(q[l].l),pr=blid*B;
		cur=0;
		rep(i,l,r){
			if(bl(q[i].l)==bl(q[i].r)){
				rep(j,q[i].l,q[i].r) mx2[a[j]]=0,mn2[a[j]]=inf;
				ans[q[i].id]=0;
				rep(j,q[i].l,q[i].r){
					mx2[a[j]]=max(mx2[a[j]],j);
					mn2[a[j]]=min(mn2[a[j]],j);
					ans[q[i].id]=max(ans[q[i].id],mx2[a[j]]-mn2[a[j]]);
				}continue;
			}
			while(pr<q[i].r) add(++pr);
			int tmp=cur;
			int pl=blid*B+1;
			vector<Cancel> can;
			while(pl>q[i].l) --pl,can.pb((Cancel){a[pl],mx[a[pl]],mn[a[pl]]}),add(pl);
			ans[q[i].id]=cur;
			cur=tmp;
			reverse(can.begin(),can.end());
			for(auto s:can) mx[s.i]=s.mx,mn[s.i]=s.mn;
		}
	}
	rep(i,1,m) cout<<ans[i]<<'\n';
	return 0;
}

好像是回滾莫隊經典題。有點閱讀理解,就是求區間權值乘上出現次數的最大值。然後由於是最大值,沒有辦法刪除,所以直接滾就可以了。注意乘的時候要用離散化前的資料。

發現這題和之前的非常不一樣。之前求 max 的時候是插入容易刪除難,這題求 mex,刪除倒是隻要與當前答案比個大小就可以了,但是加入的時候需要一個一個向上去比較,是困難的。於是我們考慮另一種回滾莫隊——不加入莫隊!經過 \(\texttt{L}\color{red}\texttt{JC00175}\) 的指點迷津,我們發現不加入莫隊其實就是不刪除反過來——首先預處理出當前塊首到塊內最大右端點的答案。然後把左端點在同一塊內的詢問右端點按從大到小排序,然後每次左指標重新跑一遍就可以了。值得慶祝的是,不加入莫隊更為好寫,因為它不需要處理左右端點都在同一塊內的情況了!

滾子最後一題了。來點人話:每次詢問給出 \(l,r,x,y\),求 \([l,r]\) 有多少個子區間,使得其最小值不小於 \(x\) 並且最大值不超過 \(y\)。好睏難啊,想了好久了。哦是排列啊,那我們來考慮一下在值域上跑莫隊。也就是,我們令 \(b_i=[x\le b_i\le y]\),這樣問題就變成查詢一段區間內所有極長全 \(1\) 段的 \(\dfrac{len(len+1)}{2}\) 之和。那每次移動左右端點,只會改變一個 \(b_i\)。然後考慮怎麼查答案。線段樹當然可以做,但是修改變成 \(O(\log n)\) 的,大常數選手瑟瑟發抖。考慮到複雜度的平衡,能不能 \(O(1)\) 修改 \(O(\sqrt n)\) 查詢呢?這東西擺明了就是分塊啊。

你對於每個塊內維護塊內所有極長全 \(1\) 串對答案的貢獻,然後考慮合併相鄰兩個塊,你就對每個塊再維護一下字首極長 \(1\) 和字尾極長 \(1\),如果兩個塊前後綴相接了,那就把兩塊的答案分別減掉然後加上總的答案。這一部分實際上和線段樹是一樣的。那考慮怎麼方便地維護這個答案,用連結串列!這題的連結串列十分高妙,\(pre_i\) 表示 \(i\) 所在的極長全 \(1\) 的開頭,\(nxt_i\) 表示 \(i\) 所在的極長全 \(1\) 的結尾,然後實際上,我們在合併區間的時候,只需要考慮首尾兩個元素的 \(pre\)\(nxt\) 就可以了。然後對每個塊維護一下答案,然後如果詢問區間小於等於 \(2\sqrt n\) 就直接暴力就可以了。(感謝 \(\texttt{d}\color{red}\texttt{evinwang}\) 的指點)

……撤銷十分麻煩啊。

從題解那裡學到一個方便的寫法。就是你開 \(3\) 個數組,然後遇到要撤銷的時候,就掏出一個備用的陣列,然後直接用這個備用的複製永久化的那個陣列,然後之後不用管它就可以了。

上面說的寫法
template<int Size>
struct Mytype{
	int tmp[Size],cnt[Size],rel[Size];
	int t=1,vit=0;
	void Roll(){t++;}
	void Vit(){vit=1;t++;}
	void Real(){vit=0;}
	void Clear(){memset(rel,0,sizeof(rel));t++;}
	int& operator[](int i){
		if(vit){
			if(cnt[i]^t) cnt[i]=t,tmp[i]=rel[i];
			return tmp[i];
		}else return rel[i];
	}
};
Mytype<MAXN> a;
//用的時候就是,如果要撤銷的,就在做之前先用一次 Vit,然後如果是不撤銷的,那就呼叫 Real。

Summary:
回滾莫隊的重點就是發現什麼是很難維護的,也就是資料結構的選擇,注意複雜度的平衡和分析。關於臨時修改的方便寫法上面已經給出了。

樹上莫隊

莫隊上♂樹?然而並不是,是樹上♂莫隊。並非在樹上分塊,而是把樹變成序列……

樹上問題一般分成幾類,現在我們分別來分析一下,怎麼把樹攤成序列來解決。(當然,對於這種問題,dsu on tree 或者點分是更好的選擇,但也許有的時候不得不用莫隊呢……)

Case 1:子樹內資訊統計

這個是比較好想的,直接用 dfn 展開,那子樹一定是連續的區間,就做完了。

Case 2:路徑上的資訊統計

到路徑普通的 dfn 就做不到了,這時候我們掏出一個尤拉序,具體地,我們在進入和退出某子樹的時候都把根加入到序列中。

尤拉序有什麼用?考慮每個節點一定是被加入兩次的,並且夾在這兩次中間的都是這個點子樹中的點。這樣一來,我們就解決了我們上面的問題,為什麼 dfn 做不到?因為只用 dfn 可能會把某個節點向外的子樹中的節點也算進去。然後有了尤拉序之後,我們可以通過選擇第一次出現還是第二次出現來避開子樹中的節點或者使它們出現兩次從而人為地剔除掉。

具體做的時候,我們對於詢問 \(l,r\),找出它們的 \(lca\)。然後我們使 \(first_l\le first_r\)。然後如果 \(lca=l\),則 \(l\to r\) 的路徑就是序列上的 \([first_l,first_r]\)。否則,就是序列上的 \([last_x,first_y]\),注意這種情況要帶一個 \(lca\)。然後做的時候,記一個 \(vis_i\),表示訪問了幾次,當前是該加入還是刪除,然後做一次就把它異或上 \(1\)。這樣一來,我們就不需要一個 \(ins\)\(del\),用這個方法可以把兩個函式整合在一起。正確性顯然,因為加入後才可能刪除,必然是偶數次是刪除。

我踩的坑:\(vis_i\) 標記的一定是下標而不是權值,因為權值可能相同。

例題:

模板題,直接按上面的套路做就可以了。

My Code
const int MAXN=1e5+10;
vector<int> e[MAXN];
int a[MAXN],lsh[MAXN];
int f[20][MAXN],fst[MAXN],lst[MAXN],tot;
int stk[MAXN<<1],dep[MAXN];
void dfs(int x,int fa){
	fst[x]=++tot;stk[tot]=x;
	f[0][x]=fa;
	rep(i,1,19) f[i][x]=f[i-1][f[i-1][x]];
	for(int s:e[x]){
		if(s==fa) continue;
		dep[s]=dep[x]+1;
		dfs(s,x);
	}lst[x]=++tot;stk[tot]=x;
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	int dlt=dep[x]-dep[y];
	rep(i,0,19) if((1<<i)&dlt) x=f[i][x];
	if(x==y) return x;
	per(i,19,0) if(f[i][x]^f[i][y]) x=f[i][x],y=f[i][y];
	return f[0][x];
}
int B;
struct Query{
	int l,r,id,lca;
	bool friend operator<(Query x,Query y)
	{return x.l/B==y.l/B?x.r<y.r:x.l<y.l;}
}q[MAXN];
bool vis[MAXN];
int cnt[MAXN],cur;
void calc(int x){
	if(vis[x]){
		cnt[a[x]]--;
		if(!cnt[a[x]]) cur--;
	}else{
		if(!cnt[a[x]]) cur++;
		cnt[a[x]]++;
	}vis[x]^=1;
}
int ans[MAXN];
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,m;cin>>n>>m;
	rep(i,1,n) cin>>a[i],lsh[i]=a[i];
	sort(lsh+1,lsh+1+n);
	int Cnt=unique(lsh+1,lsh+1+n)-lsh-1;
	rep(i,1,n) a[i]=lower_bound(lsh+1,lsh+1+Cnt,a[i])-lsh;
	rep(i,2,n){
		int u,v;cin>>u>>v;
		e[u].pb(v);e[v].pb(u);
	}dfs(1,0);
	rep(i,1,m){
		int l,r,lca;q[i].id=i;
		cin>>l>>r;lca=LCA(l,r);
		if(fst[l]>fst[r]) swap(l,r);
		if(lca==l) q[i].l=fst[l],q[i].r=fst[r],q[i].lca=0;
		else q[i].l=lst[l],q[i].r=fst[r],q[i].lca=lca;
	}
	B=sqrt(tot);
	sort(q+1,q+1+m);
	int l=1,r=0;
	rep(i,1,m){
		while(l<q[i].l) calc(stk[l++]);
		while(r>q[i].r) calc(stk[r--]);
		while(l>q[i].l) calc(stk[--l]);
		while(r<q[i].r) calc(stk[++r]);
		if(q[i].lca) calc(q[i].lca);
		ans[q[i].id]=cur;
		if(q[i].lca) calc(q[i].lca);
	}
	rep(i,1,m) cout<<ans[i]<<'\n';
	return 0;
}

(指著電腦)這是我自己的發明!——zmf
摘自《神仙語錄》第 234 條

我們考慮怎麼跟上時代。

需要換根。如果不用,那麼直接 dfn 展開就可以做了——嗎?詢問是兩個子樹中分別統計顏色,然後求 \(\sum cnt_{x,col}\times cnt_{y,col}\)。不需要換根都不太會的樣子。事實上轉化成序列就是求兩段取數然後相等的方案數。看了眼題解,怎麼又是人類智慧容斥啊。我們令 \(f(l_1,r_1,l_2,r_2)\) 表示兩段區間是 \([l_1,r_1]\)\([l_2,r_2]\) 的答案。那就有:

\[f(l_1,r_1,l_2,r_2)=f(1,r_1,1,r_2)-f(1,r_1,1,l_1-1)-f(1,r_2,1,l_2-1)+f(1,l_1-1,1,l_2-1) \]

仔細一想這個容斥好像挺套路的/qd。然後我們令 \(g(l,r)=f(1,l,1,r)\),那麼就有:

\[f(l_1,r_1,l_2,r_2)=g(r_1,r_2)-g(r_2,l_1-1)-g(r_1,l_2-1)+g(l_1-1,l_2-1) \]

\(g(l,r)\) 就不難用莫隊維護了。

然後你考慮換根,事實上這個換根是假的,如果換的根是在 \(x\) 的子樹內,那麼 \(x\) 的「子樹」就變成去除原來的子樹中根所在的子樹剩下的部分。如果換的根就是 \(x\),那 \(x\) 的「子樹」就是整棵樹了。否則 \(x\) 的子樹還是原來的子樹。這樣一來,任意一段詢問可以被分成最多 \(2\) 份,然後上面容斥最多拆成 \(4\) 段,所以每一次詢問最多拆成 \(8\) 個區間,然後一次詢問兩個點,所以總共是 \(16\) 個區間,這是好的(笑)。接下來就是裸的莫隊了,看題解裡說,要卡常,塊長要設 \(\dfrac{n}{\sqrt n}\),以及用奇偶優化。

這真是可悲的啊,寫掛了 114514 次了/ll。一定一定要注意是讀入的編號還是樹上的 \(dfn\)!!!
(感謝 \(\texttt{r}\color{red}\texttt{walxfhg}\) 的帖子,我犯了同樣的錯誤並捏捏 cmll)

並:這題有個非樹上的弱化版,放在 Tasks 裡了。

Summary:

實際上主要考察的還是碼力和細節,以及其它的莫隊。但是第二道例題拆貢獻的思想是非常重要的。

莫隊二次離線

我們考慮到普通莫隊在移動端點的時候,需要保證複雜度是 \(O(1)\),這樣才能保證整個莫隊複雜度是 \(O(n\sqrt n)\)(這裡預設 \(n,q\) 同階),但是有的時候,無論是插入還是刪除都很難做到 \(O(1)\),這時候,我們大力觀察題目性質,如果說一個數 \(x\) 對區間 \([l,r]\) 的貢獻是 \(f(x,l,r)\),並且這個東西是滿足 \(f(x,l,r)=f(x,1,r)-f(x,1,l-1)\)(就是說可以差分)的,那麼我們就可以使用莫隊二次離線。

所謂二次離線,顧名思義就是離線兩次。莫隊本身就是一離線演算法,在離線詢問的基礎上,我們把每一段區間的答案也預處理出來,這樣整體複雜度就 \(O(n\sqrt nk)\to O(n\sqrt n+nk)\),其中 \(k\) 是移動端點的複雜度。

那具體是怎麼做的呢?事實上我們已經做過相似的工作了——拆貢獻。我們來康康移動端點的過程,比如說,我們把區間從 \([l,r]\) 移動成 \([l,r+x]\)。那我們要在答案中加上:

\[\forall p\in [r+1,r+x],f(p,l,p-1) \]

然後我們的貢獻是可以差分的:

\[f(p,l,p-1)=f(p,1,p-1)-f(p,1,l-1) \]

\(f(a,1,b)=g(a,b)\),則:

\[f(p,l,p-1)=g(p,p-1)-g(p,l-1) \]

就變成了字首的貢獻了,貌似可以預處理了。然後這部分處理得最好就是,你對於每個詢問,只儲存詢問需要的 \(g(l,r)\),那總量也有 \(n\sqrt n\) 個,也就是空間複雜度是 \(O(n\sqrt n)\) 的,顯然還不夠優秀。

但其實進一步的優化也非常簡單,你發現 \(g(p,p-1)\) 的數量是 \(O(n)\) 級別的。然後對於另一部分,總的答案實際上是:

\[-\sum_{p=r+1}^{r+x} g(p,l-1) \]

這東西可以歸納成一個五元組:\((l,r,a,fl,id)\) 表示區間 \([l,r]\) 內的每個數對區間 \([1,a]\) 的答案和乘上 \(fl\)(表示正負)對第 \(id\) 個詢問的貢獻。然後這裡是若干個數對同一段區間的貢獻,我們把它記在 \(a\) 處,用一個 vector 記錄一下。離線完之後暴力掃一遍算這個東西就可以了。然後你考慮存下來的詢問中長度和就是莫隊的複雜度 \(O(n\sqrt n)\),所以是可以直接暴力算的。

最後注意一下,每次答案算出來的是前一次處理詢問的增量,所以需要求一下字首和再輸出。
以及,這裡分析是 \(r\) 指標增大時的拆分過程,剩下的 \(3\) 種情況手推應該不難了。
哦對了,一般來說題目中 \(g(p,p-1)=g(p,p)\),如果不一樣,預處理的時候要分開。

例題:

前方大量 lxl。

就是模板,然後具體做法就是你考慮 \(a\operatorname{xor} b=c\) 等價於 \(a=b\operatorname{xor}c\),然後你用一個 \(cnt_i\) 記錄與 \(i\) 異或之後有 \(k\)\(1\) 的數的個數。然後你預處理出 \(popcount=k\) 的所有數就好了。

My Code
const int MAXN=1e5+10;
int a[MAXN],cnt[MAXN],B;
struct Query{
	int l,r,id,ans;Query(){}
	Query(int _l,int _r,int _id){l=_l,r=_r;id=_id;}
	bool friend operator<(Query x,Query y){return x.l/B==y.l/B?x.r<y.r:x.l<y.l;}
}q[MAXN];
struct Info{
	int l,r,fl,id;Info(){}
	Info(int _l,int _r,int _fl,int _id){l=_l,r=_r,fl=_fl,id=_id;}
};vector<Info> Q[MAXN];
vector<int> pkq;
int pre[MAXN],ans[MAXN];
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int n,m,k;
	cin>>n>>m>>k;
	rep(i,1,n) cin>>a[i];
	rep(i,0,16383) if(__builtin_popcount(i)==k) pkq.pb(i);
	rep(i,1,m) cin>>q[i].l>>q[i].r,q[i].id=i;
	rep(i,1,n){
		pre[i]=cnt[a[i]];
		for(int s:pkq) cnt[a[i]^s]++;
	}memset(cnt,0,sizeof(cnt));
	B=sqrt(n);
	sort(q+1,q+1+m);
	int l=1,r=0;
	rep(i,1,m){
		if(r<q[i].r) Q[l-1].pb(Info(r+1,q[i].r,-1,i));
		while(r<q[i].r) q[i].ans+=pre[++r];
		if(l>q[i].l) Q[r].pb(Info(q[i].l,l-1,1,i));
		while(l>q[i].l) q[i].ans-=pre[--l];
		if(r>q[i].r) Q[l-1].pb(Info(q[i].r+1,r,1,i));
		while(r>q[i].r) q[i].ans-=pre[r--];
		if(l<q[i].l) Q[r].pb(Info(l,q[i].l-1,-1,i));
		while(l<q[i].l) q[i].ans+=pre[l++];
	}
	rep(i,1,n){
		for(int s:pkq) cnt[a[i]^s]++;
		for(auto qr:Q[i]){
			rep(j,qr.l,qr.r){
				int tmp=cnt[a[j]]-(k==0&&j<=i);//k==0 要特判/qd
				q[qr.id].ans+=qr.fl*tmp;
			}
		}
	}
	rep(i,2,m) q[i].ans+=q[i-1].ans;
	rep(i,1,m) ans[q[i].id]=q[i].ans;
	rep(i,1,m) cout<<ans[i]<<'\n';
	return 0;
}

求區間逆序對,同樣我們拆出貢獻之後,對於 \(g(p,p)\) 只要字首樹狀陣列處理一下就可以了。然後對於一個區間內的數對一段字首的貢獻和,二次離線,然後你之後需要一個 \(O(1)\) 查詢的資料結構,然後不會了,看題解。。。怎麼又是值域分塊/qd完全不會分塊怎麼辦。用來代替樹狀陣列,具體地:
\(\texttt{A}\color{red}\texttt{xDea}\) 說,你分塊維護字首和不就好了。
然後我聽從神的指令~/youl然後這題有個特別噁心的地方就是,你逆序對是有位置關係的,所以你查詢的時候要查詢下標比它小但是值比它大以及下標比它大但是值比它小的所有答案和。所以你需要碼量乘二。

要奇偶排序。(以後能用奇偶排序就一定要用,雖然理論無用但是實際上快到飛起)

Summary:

這一科技的運用還是比較牛逼的,對於不同的題目,需要重新分析每一個端點移動對答案的影響,然後考慮儲存與計算。

Tasks

一些看起來比較困難並且有可能會咕掉的題(絕對不咕)。當成作業/qd。