1. 程式人生 > 其它 >「學習筆記」CDQ 分治

「學習筆記」CDQ 分治

這裡是尚未完工的 CDQ 分治學習筆記~

CDQ 分治

應用範圍

  1. 解決與點對相關的問題
  2. 優化 1D/1D 動態規劃的轉移
  3. 將一些動態問題轉化為靜態問題

解決點對相關問題

演算法流程

  1. 找到序列 \([l,r]\) 的中點 \(mid\)
  2. 將位於序列中的所有點對 \((i,j)\) 進行分類:
    • \(l\le i\le mid\)\(l \le j\le mid\)
    • \(mid+1\le i\le r\)\(mid+1\le j\le r\)
    • \(l\le i\le mid\)\(mid+1\le j\le r\)
  3. 分別遞迴 \([l,mid]\)\([mid+1,r]\) 解決前兩類點對。
  4. 用某些方法解決最後一類點對。

整個演算法流程其實就是一種分而治之的思想,而第 4 部分的資訊處理則是我們應該去設計的演算法。

例題1

三維偏序

給定 \(n\) 個點 \((a_i,b_i)\),求滿足 \(i< j,a_i < a_j,b_i < b_j\) 的點對 \((i,j)\) 數。

我們嘗試解決第 4 部分的問題。發現由於 \(l\le i\le mid\)\(mid+1\le j\le r\),已經滿足 \(i<j\) 的條件,只需再統計滿足 \(a_i< a_j,b_i<b_j\) 的點對數即可,將問題轉化為了二維偏序問題。只需將 \([l,mid]\)\([mid+1,r]\)

部分都按 \(a\) 的值從小到大排序,然後列舉 \(j\),將滿足 \(a_i<a_j\)\(b_i\) 全部加入權值樹狀陣列,然後統計 \(<b_j\) 的值的個數即可。

由於已經按 \(a\) 的值排序,在 \(j\) 逐漸增大的同時,\(i\)​ 必然也在逐漸增大。(可能不變)因此只需要雙指標即可解決。由於當前序列 \([l,r]\) 的統計問題與其他序列無關,我們需要完成操作後清空樹狀陣列。直接用 memset 顯然是不現實的,我們可以直接在修改的地方重新改回來即可,即撤銷操作。

因此解決第 4 部分問題的複雜度為 \(O(n\log n)\)​,總時間複雜度就為 \(O(n\log^2 n)\)

void CDQ(int l,int r)
{
	if(l==r)return;
	/*遞迴邊界返回*/
	int mid=(l+r)>>1;
	/*找到序列中點mid*/
	CDQ(l,mid),CDQ(mid+1,r);
	/*遞迴解決前兩類點對*/
	sort(P+l,P+mid+1);
	/*將[l,mid]間的點對按照 a 值排序*/
	sort(P+mid+1,P+r+1);
	/*將[mid+1,r]間的點對按照 a 值排序*/ 
	int i=l,j=mid+1;
	/*
	雙指標:i為[l,mid]部分的,j為[mid+1,r]部分的 
	*/ 
	while(j<=r)
	{
		while(i<=mid&&P[i].a<P[j].a)
			T.add(P[i].b,1),i++;
		/*
		i 移動的前提是得在 [l,mid] 內,不能跑出去了
		如果滿足 a_i<a_j 就加入 b_j,並將j向右移動 
		*/
		P[j].sum+=T.query(P[j].b-1),j++;
		/*
			樹狀陣列統計的是<=的個數,
			因此 -1 統計的是< 的個數。
			注意要將當前指標 j 移動 
		*/
	}
	for(int d=l;d<i;d++)
		T.add(P[d].b,-1);
	/*
	i在符合條件時先加入再移動
	因此 [l,i-1]  即為修改的點 
	*/
}

例題2

陌上花開

給定 \(n\)​ 個元素,第 \(i\)​ 個元素的屬性為 \((a_i,b_i,c_i)\)​,設 \(f(i)\)​ 表示滿足 \(j\ne i,a_j \le a_i,b_j \le b_i,c_j\le c_i\)​ 的 \(j\)​​ 的個數。

對於 \(d\in [0,n)\),求 \(f(i)=d\) 的數量。

同樣是偏序問題,上題是嚴格偏序,本題是非嚴格偏序。看似只需要簡單轉化:在主函式中先按 \(c\) 排序去掉一維,然後再套路地套上 CDQ 分治,將 P[i].a<P[j].a 更改為 P[i].a<=P[j].a,將查詢從 T.query(P[j].b-1) 更改為 T.query(P[j].b)

其實不然。若存在點對 \((i,j)\) 滿足 \(i\ne j,a_i=a_j,b_i=b_j,c_i=c_j\),在 CDQ 分治中由於排序被固定了左右順序,只能由一者貢獻給另一者,而事實上它們之間是相互貢獻的。如何解決這一問題?

將序列去重即可。同時記錄和去重後的元素 \(i\) 相等的元素數目 \(v\)(包括 \(i\)​),將 \(v\) 作為元素 \(i\) 的權值,修改時改為 T.add(P[i].b,P[i].v),撤銷時也相應改為 T.add(P[i].b,-P[i].v) 即可。在最後統計答案時,再加上一個 \(v-1\),即相同元素間的貢獻即可。

具體程式碼實現

例題3

動態逆序對

現在給出 \(1\sim n\) 的一個排列,按照某種順序依次刪除 \(m\) 個元素,你的任務是在每次刪除一個元素之前統計整個序列的逆序對數。

本題的線上演算法:樹套樹。可以用樹狀陣列套權值線段樹,或者用分塊套樹狀陣列都可以實現。

現在我們來看看本題的離線演算法。雖然沒有強制離線這種玩意兒,但總還是要學的。

正序刪除難以操作,怎麼辦?變為倒序插入呀!將刪除時間 \(t\) 作為一維,統計滿足 \(t_i\ge t_j\) 的逆序對數。CDQ 分治是以一個元素為根本,然後尋找滿足條件的另一個元素。因此逆序對數需要由兩部分構成:\(i<j\)\(v_i>v_j\) 或是 \(i>j\)\(v_i<v_j\)。我們需要做兩次 CDQ 分治。在這裡為了更加順眼,我將每個元素的刪除時間改為 \(m-t+1\),將不動的元素時間設定為 \(0\),這樣就變為統計滿足 \(t_i\le t_j\) 的逆序對數了。

這裡就有兩種寫法了。一種是兩次合為一次(因為大體框架相同),另一種是做兩次。而做兩次的時候有一個坑點需要注意:如果用 \(ans[t]\) 記錄 \(t\) 時刻插入元素後新增的逆序對,那麼初始的逆序對數 \(ans[0]\) 會被統計兩次,因此需要除以 \(2\)。我們統計的是每一時刻的總逆序對數,因此需要累加。

具體程式碼實現