CDQ分治入門
阿新 • • 發佈:2018-10-28
要求 因此 區間 經典 .cn 一個 vat 可能 排序
前言
\(CDQ\)分治是一個神奇的算法。
它有著廣泛的用途,甚至在某些題目中還能取代\(KD-Tree\)、樹套樹等惡心的數據結構成為正解,而且常數還小得多。
不過它也有一定的缺點,如必須離線操作,遇到強制在線的題目還是老老實實打樹套樹吧... ...
核心思想
\(CDQ\)分治的核心思想真的是非常簡單,也就是分與治二字(其實所有分治算法都是這樣)。
- 分: 與常見的二分一樣,將\([l,r]\)區間內的問題分成兩個區間\([l,mid]\)和\([mid+1,r]\)解決。
- 治: \(CDQ\)分治中的治這一部分就十分玄學了,它的思想是利用左區間來求解右區間,這與普通的分治就大不一樣了。
這樣講畢竟還是十分抽象,讓我們來借助一道經典例題,來粗略地見識一下\(CDQ\)分治的神奇所在。
經典例題:【BZOJ3262】陌上花開
這道題目大致題意就是要你求三維偏序。
關於二維偏序
談到三維偏序,我們可能首先會想到二維偏序。
或許有些人不知道什麽是二維偏序,但它的另一個名稱——逆序對你總知道吧。
二維偏序一般可以用樹狀數組或歸並排序來解決。
關於用樹狀數組,其實我們接下來還要用到。
而對於歸並排序,可以發現它其實也是借助了左區間來求解右區間,或許也能算作一個比較\(Simple\)的\(CDQ\)分治?(大霧)
好了,關於逆序對我們就扯到這裏,下面我們來看看如何用\(CDQ\)分治求解三維偏序。
如何求解三維偏序
- 對於第一維
- 首先第一步是將數據按照第一維\(x\)進行排序。
- 這樣就能保證第一維是有序的了。
- 對於第二維
- 接下來,在每一次處理完兩個子區間的答案後(註意,一定要先處理子區間,因為接下來的排序會打亂元素的順序),我們再將這兩個子區間分別按照第二維\(y\)排序。
- 此時,我們依然可以保證,左區間內每個元素的第一維始終小於右區間內每個元素的第一維。(這應該是顯然的吧)
- 對於第三維
- 我們可以用\(i\)和\(j\)分別記錄右區間和左區間當前處理到的點。
- 對於每一個\(y_j\le y_i\)的\(j\),我們可以將其第三維\(z\)加入樹狀數組。
- 由於兩個區間經過排序,\(y\)
- 現在,我們已經保證在樹狀數組中的所有元素,它的前兩維皆\(\le\)當前\(i\)的前兩維。因此,我們只要求出有多少個\(z\le z_i\)即可,用樹狀數組可以快速做到這一點。
- 這樣一來,就能計算出\(i\)的三維偏序數量了。
大致就是這樣一個過程,一次沒有看明白的可以再多看幾遍理解一下。
最後註意一個細節:千萬記得清空樹狀數組!而且千萬記得不要直接\(memset\)!
代碼
#include<bits/stdc++.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define min(x,y) ((x)<(y)?(x):(y))
#define uint unsigned int
#define LL long long
#define ull unsigned long long
#define swap(x,y) (x^=y,y^=x,x^=y)
#define abs(x) ((x)<0?-(x):(x))
#define INF 1e9
#define Inc(x,y) ((x+=(y))>=MOD&&(x-=MOD))
#define ten(x) (((x)<<3)+((x)<<1))
#define N 100000
using namespace std;
int n,m,nn;
struct value
{
int x,y,z,v,tot;
inline friend bool operator == (value x,value y) {return !(x.x^y.x||x.y^y.y||x.z^y.z);}
}s[N+5];
class FIO
{
private:
#define Fsize 100000
#define tc() (FinNow==FinEnd&&(FinEnd=(FinNow=Fin)+fread(Fin,1,Fsize,stdin),FinNow==FinEnd)?EOF:*FinNow++)
#define pc(ch) (FoutSize<Fsize?Fout[FoutSize++]=ch:(fwrite(Fout,1,FoutSize,stdout),Fout[(FoutSize=0)++]=ch))
int f,FoutSize,OutputTop;char ch,Fin[Fsize],*FinNow,*FinEnd,Fout[Fsize],OutputStack[Fsize];
public:
FIO() {FinNow=FinEnd=Fin;}
inline void read(int &x) {x=0,f=1;while(!isdigit(ch=tc())) f=ch^‘-‘?1:-1;while(x=ten(x)+(ch&15),isdigit(ch=tc()));x*=f;}
inline void read_char(char &x) {while(isspace(x=tc()));}
inline void read_string(string &x) {x="";while(isspace(ch=tc()));while(x+=ch,!isspace(ch=tc())) if(!~ch) return;}
inline void write(int x) {if(!x) return (void)pc(‘0‘);if(x<0) pc(‘-‘),x=-x;while(x) OutputStack[++OutputTop]=x%10+48,x/=10;while(OutputTop) pc(OutputStack[OutputTop]),--OutputTop;}
inline void write_char(char x) {pc(x);}
inline void write_string(string x) {register int i,len=x.length();for(i=0;i<len;++i) pc(x[i]);}
inline void end() {fwrite(Fout,1,FoutSize,stdout);}
}F;
inline bool cmp_x(value x,value y) {return x.x^y.x?x.x<y.x:(x.y^y.y?x.y<y.y:x.z<y.z);}//按第一維排序
inline bool cmp_y(value x,value y) {return x.y^y.y?x.y<y.y:x.z<y.z;}//按第二維排序
class Class_CDQ//CDQ分治
{
private:
int ans[N+5];//最後統計答案
class Class_BIT//樹狀數組
{
private:
#define M 200000
#define lowbit(x) ((x)&-(x))
int data[M+5];
public:
inline void Add(int x,int v) {while(x<=m) data[x]+=v,x+=lowbit(x);}//插入元素
inline int Query(int x,int ans=0) {while(x) ans+=data[x],x-=lowbit(x);return ans;}//詢問≤x的數的和
}BIT;
public:
inline void Solve(int l,int r)//求解l到r這段區間內的答案
{
if(l>=r) return;
register int mid=l+r>>1,i,j=l;
Solve(l,mid),Solve(mid+1,r),sort(s+l,s+mid+1,cmp_y),sort(s+mid+1,s+r+1,cmp_y);//切記先求解子區間,然後再排序,排序之後依然能保證右區間第一維大於左區間第一維
for(i=mid+1;i<=r;++i)
{
while(j<=mid&&s[j].y<=s[i].y) BIT.Add(s[j].z,s[j].v),++j;//對於每一個y[j]≤y[i]的j,將z[j]插入樹狀數組
s[i].tot+=BIT.Query(s[i].z);//求出樹狀數組中≤z[i]的所有元素之和,從而更新i的三維偏序個數
}
for(i=l;i<j;++i) BIT.Add(s[i].z,-s[i].v);//切記要這樣清空樹狀數組,memset會T飛(親身實踐)
}
inline void PrintAns()
{
register int i;
for(i=1;i<=n;++i) ans[s[i].tot+s[i].v-1]+=s[i].v;//統計答案
for(i=0;i<nn;++i) F.write(ans[i]),F.write_char(‘\n‘);//輸出
}
}CDQ;
int main()
{
register int i;
for(F.read(nn),F.read(m),i=1;i<=nn;++i) F.read(s[i].x),F.read(s[i].y),F.read(s[i].z),s[i].v=1;
for(sort(s+1,s+nn+1,cmp_x),i=1;i<=nn;++i) n&&s[n]==s[i]?++s[n].v:(s[++n]=s[i],0);//按照第一維排序,然後去重,從而提高時間效率
return CDQ.Solve(1,n),CDQ.PrintAns(),F.end(),0;//用CDQ分治求解
}
後記
關於\(CDQ\)分治求解三維偏序,還有一道比較好的題目:【洛谷3157】[CQOI2011] 動態逆序對,可以去做一做。
(這道題卡樹套樹,我的 線段樹套\(Treap\) 只得了\(80\)分,於是為做這題來學了\(CDQ\)分治)
CDQ分治入門