1. 程式人生 > >【總結】CDQ分治

【總結】CDQ分治

總的來說,CDQ分治與普通分治不一樣的地方在於,CDQ分治的物件是時間。即對於一個時間段[L, R],我們取mid = (L + R) / 2,(用資料結構題舉例)分治的每層只考慮mid之前的修改對mid之後的查詢的貢獻,然後遞迴到[L,mid],(mid,R]。

顯然,CDQ分治是一種離線演算法,我們需要將所有的修改/查詢存下來,一起進行操作。

同時,CDQ分治還需要滿足:操作之間相互獨立,即一個操作的存在不會影響到另一個操作的存在。

二維單點修改,矩形查詢。

首先修改/查詢之間的順序就是時間,我們按時間來分治。

但是由於是二維的,為了省空間不開二維的資料結構,我們一開始把所有操作按x軸排序,每次處理我們只需要掃描一下x軸,把y軸的修改/查詢放到一個一維樹狀數組裡就好了。

(另外,矩形查詢拆為4個字首和查詢)

具體步驟如下:

(1)把操作離線下來(離線的時候要記錄每個操作的時間,即操作的編號),然後按x軸排序。(必要時離散化)

(2)分治區間[L, R](L, R為時間,即操作的編號),取mid,從L到R遍歷操作,將mid之前的修改加入一維樹狀陣列,將mid之後的查詢在一維樹狀數組裡查詢。

(3)注意要還原修改操作,從L到R遍歷操作,遇到修改操作,將修改操作改回去。(如果是+c,那麼-c)

(4)開一個臨時陣列,兩個指標(一個指標指向左區間的左端點,另一個指標指向右區間的左端點),從L到R遍歷操作,將mid之前的操作放到左區間,將mid之後的操作放到右區間。最後把臨時陣列賦值給運算元組。

(5)遞迴[L, mid],[mid + 1, R],回到(2)。

遞迴的終點是L==R。

注意,在分治的每一層的區間內,x軸都是有序的。雖然時間無序,但是我們遍歷操作時按照mid來劃分了。(這個大概是CDQ分治比較難懂的地方)

看程式碼吧。

/* Pigonometry */
#include <cstdio>
#include <algorithm>
 
using namespace std;
 
const int maxn = 2000005, maxm = 640005, maxq = 10005;
 
int n, tr[maxn], ans[maxq];
 
struct _data {
    int opt, id, qid, x, y, c;
} c[maxm], tmp[maxm];
 
inline int iread() {
    int f = 1, x = 0; char ch = getchar();
    for(; ch < '0' || ch > '9'; ch = getchar()) f = ch == '-' ? -1 : 1;
    for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
    return f * x;
}
 
inline void add(int x, int c) {
    for(; x <= n; x += x & -x) tr[x] += c;
}
 
inline int sum(int x) {
    int res = 0;
    for(; x; x -= x & -x) res += tr[x];
    return res;
}
 
inline int cmp(_data a, _data b) {
    return a.x != b.x ? a.x < b.x : a.y != b.y ? a.y < b.y : a.opt < b.opt;
}
 
inline void cdq(int l, int r) {
    if(l == r) return;
    int mid = l + r >> 1;
    for(int i = l; i <= r; i++)
        if(c[i].opt == 1 && c[i].id <= mid) add(c[i].y, c[i].c);
        else if(c[i].opt == 2 && c[i].id > mid) ans[c[i].qid] += c[i].c * sum(c[i].y);
    for(int i = l; i <= r; i++)
        if(c[i].opt == 1 && c[i].id <= mid) add(c[i].y, -c[i].c);
    int h1 = l, h2 = mid;
    for(int i = l; i <= r; i++)
        if(c[i].id <= mid) tmp[h1++] = c[i];
        else tmp[++h2] = c[i];
    for(int i = l; i <= r; i++) c[i] = tmp[i];
    cdq(l, mid); cdq(mid + 1, r);
}
 
int main() {
    int s = iread(); n = iread(); int tot = 0, id = 0;
    while(1) {
        int opt = iread();
        if(opt == 3) break;
 
        if(opt == 1) {
            int x = iread(), y = iread(), w = iread();
            c[++tot] = (_data){1, tot, 0, x, y, w};
        } else {
            int x1 = iread(), y1 = iread(), x2 = iread(), y2 = iread(); id++;
            c[++tot] = (_data){2, tot, id, x2, y2, 1};
            c[++tot] = (_data){2, tot, id, x1 - 1, y2, -1};
            c[++tot] = (_data){2, tot, id, x2, y1 - 1, -1};
            c[++tot] = (_data){2, tot, id, x1 - 1, y1 - 1, 1};
        }
    }
    sort(c + 1, c + 1 + tot, cmp);
    cdq(1, tot);
    for(int i = 1; i <= id; i++) printf("%d\n", ans[i]);
    return 0;
}
此題的S並沒有用。

另外CDQ分治還可以處理偏序問題。

有n個物品,每個物品有三個屬性a, b, c。對於物品X,需要查詢有多少物品Y滿足Xa >= Ya, Xb >= Yb, Xc >= Yc。物品Y的個數即為X的等級。

我們發現,這個問題裡並沒有時間,但是我們可以轉化一下,比如將屬性a當做時間(時間本來也是一種序)。

所以思路是,先按a排序,將a編號,把a當做時間。然後按b排序(如果不理解,多多理解上個題),那麼我們遍歷操作的時候,只需要在一維樹狀數組裡面查詢有多少比c小的物品就行了。

下面是程式碼(很久之前的)

#include <cstdio>
#include <algorithm>
#define rec(i, x, n) for(int i = x; i <= n; i++)
const int maxn = 100005, maxk = 200005;
int n, k, tot, tr[maxk], ans[maxn];
struct _flower 
{
	int a, b, c, cnt, ans;
	bool operator != (const _flower &x) const 
	{
		return a != x.a || b != x.b || c != x.c;
	}
} f[maxn], c[maxn];
inline int iread() 
{
	int f = 1, x = 0; char ch = getchar();
	for(; ch < '0' || ch > '9'; ch = getchar()) f = ch == '-' ? -1 : 1;
	for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
	return f * x;
}
bool cmp1(_flower x, _flower y) 
{
return x.a != y.a ? x.a < y.a : x.b != y.b ? x.b < y.b : x.c < y.c;
}
bool cmp2(_flower x, _flower y) 
{
return x.b != y.b ? x.b < y.b : x.a < y.a;
}
void add(int x, int w) 
{
	for(; x <= k; x += x & -x) tr[x] += w;
}
int sum(int x) 
{
	int res = 0;
	for(; x; x -= x & -x) res += tr[x];
	return res;
}
void cdq(int l, int r) 
{
	if(l == r) 
	{
		f[l].ans += f[l].cnt - 1;
		return;
	}
	int mid = l + r >> 1, q1 = l, q2 = mid;
	rec(i, l, r) f[i].a <= mid ? c[q1++] = f[i] : c[++q2] = f[i];
	rec(i, l, r) f[i] = c[i];
	for(int i = mid + 1, j = l; i <= r; i++) 
	{
		for(; j <= mid && f[j].b <= f[i].b; j++)
			add(f[j].c, f[j].cnt);
		f[i].ans += sum(f[i].c);
	}
	for(int i = l; i <= mid && f[i].b <= f[r].b; i++) add(f[i].c, -f[i].cnt);
	cdq(l, mid); cdq(mid + 1, r);
}
int main() 
{
	n = iread(); k = iread();
	rec(i, 1, n) f[i].a = iread(), f[i].b = iread(), f[i].c = iread();
	std::sort(f + 1, f + 1 + n, cmp1);
	rec(i, 1, n) 
	{
		if(f[i] != f[i - 1]) f[++tot] = f[i];
		++f[tot].cnt;
	}
	rec(i, 1, tot) f[i].a = i;
	std::sort(f + 1, f + 1 + tot, cmp2);
	cdq(1, tot);
	rec(i, 1, tot) ans[f[i].ans] += f[i].cnt;
	rec(i, 0, n - 1) printf("%d\n", ans[i]);
	return 0;
}
注意給a編號那句。

另外還有四維偏序

現在有了四個屬性a, b, c, d,對於物品X,問是否有物品Y滿足Xa >= Ya, Xb >= Yb, Xc <= Yc, Xd <= Yd。

我們沿用上題的思路,還是將一維當做時間。但注意,我們這裡選擇將c作為時間軸,原因在下面。

將c排序,編號,當做時間。然後將d排序。

對於a,b的處理,我們發現我們只需要知道“存在性”即可,那麼對於物品X,我們就需要在[1, Xa]裡查詢是否有物品Y滿足Xb >= Yb,這是個RMQ問題,我們用線段樹維護。

即把a當做線段樹的下標,把b當做線段樹的權值,線段樹維護區間最小值。

程式碼(還是很久之前的)

#include <cstdio>
#include <cstring>
#include <algorithm>
#define rec(i, x, n) for(int i = x; i <= n; i++)
const int maxn = 200005, inf = 0x3f3f3f3f;
int n, tot, rank[maxn], tr[maxn << 2];
struct _mat {
	int a, b, c, d, ans; // a up, b up, c down, d down
} c[maxn], tmp[maxn];
inline int iread() {
	int f = 1, x = 0; char ch = getchar();
	for(; ch < '0' || ch > '9'; ch = getchar()) f = ch == '-' ? -1 : 1;
	for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
	return f * x;
}
void pushup(int p) {
	tr[p] = std::min(tr[p << 1], tr[p << 1 | 1]);
}
bool cmp1(_mat x, _mat y) {return x.c > y.c;}
bool cmp2(_mat x, _mat y) {return x.d > y.d;}
void change(int p, int l, int r, int x, int w) {
	if(l == r && r == x) {
		tr[p] = w;
		return;
	}
	int mid = l + r >> 1;
	if(x <= mid) change(p << 1, l, mid, x, w);
	else change(p << 1 | 1, mid + 1, r, x, w);
	pushup(p);
}
int query(int p, int l, int r, int x, int y) {
	if(x > y) return inf;
	if(x <= l && r <= y) return tr[p];
	int mid = l + r >> 1;
	int res = inf;
	if(x <= mid) 
		res = std::min(res, query(p << 1, l, mid, x, y));
	if(y > mid) 
		res = std::min(res, query(p << 1 | 1, mid + 1, r, x, y));
	return res;
}
void cdq(int l, int r) {
	if(l == r) return;
	int mid = l + r >> 1, q1 = l, q2 = mid;
	rec(i, l, r) tmp[c[i].c <= mid ? q1++ : ++q2] = c[i];
	rec(i, l, r) c[i] = tmp[i];
	for(int i = mid + 1, j = l; i <= r; i++) {
		for(; j <= mid && c[j].d > c[i].d; j++) change(1, 1, tot, c[j].a, c[j].b);
		if(c[i].b > query(1, 1, tot, 1, c[i].a - 1)) c[i].ans = 1;
	}
	for(int i = l; i <= mid && c[i].d > c[r].d; i++) change(1, 1, tot, c[i].a, inf);
	cdq(l, mid); cdq(mid + 1, r);
}
int main() {
	n = iread();
	rec(i, 1, n) {
		int x1 = iread(), y1 = iread(), x2 = iread(), y2 = iread();
		c[i] = (_mat){rank[i] = x1, y1, x2, y2};
	}
	std::sort(rank + 1, rank + 1 + n);
	tot = std::unique(rank + 1, rank + 1 + n) - rank - 1;
	std::sort(c + 1, c + 1 + n, cmp1);
	rec(i, 1, n) c[i].c = i, c[i].a = std::lower_bound(rank + 1, rank + 1 + tot, c[i].a) - rank;
	std::sort(c + 1, c + 1 + n, cmp2);
	memset(tr, 0x3f, sizeof(tr));	
	cdq(1, n);
	int ans = 0;
	rec(i, 1, n) ans += c[i].ans;
	printf("%d\n", ans);
	return 0;
}
回顧一下為什麼把c當做時間。

如果我們把a當做時間,把b排序,那麼我們就得在[Xc, inf)裡尋找是否有物品Y滿足Xd <= Yd。這個太複雜了,inf有可能非常大,即使離散化了,常數也會比較大。

還有一個題,和Mokia一樣,而且資料範圍更小,當做CDQ分治的練習吧。