「學習筆記」CDQ 分治
CDQ 分治
應用範圍
- 解決與點對相關的問題
- 優化 1D/1D 動態規劃的轉移
- 將一些動態問題轉化為靜態問題
解決點對相關問題
演算法流程
- 找到序列 \([l,r]\) 的中點 \(mid\)
- 將位於序列中的所有點對 \((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\)
- 分別遞迴 \([l,mid]\) 和 \([mid+1,r]\) 解決前兩類點對。
- 用某些方法解決最後一類點對。
整個演算法流程其實就是一種分而治之的思想,而第 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\) 逐漸增大的同時,\(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\)。我們統計的是每一時刻的總逆序對數,因此需要累加。