算法学习笔记(1):CDQ分治

CDQ分治

对比普通分治

把一种问题划分成不同子问题, 递归处理子问题内部的答案, 再考虑合并子问题的答案。

再看CDQ分治

有广泛的应用, 但我不会。 但在接下来的题目体会到大概: 将可能产生的对于答案的贡献分为两类:

  1. \(f(l, mid)\)\(f(mid + 1, r)\) 内部产生的贡献, 其贡献已在递归内部计算。
  2. \(f(l, mid)\)\(f(mid + 1, r)\) 之间可能产生的贡献, 这就是我们思考的重点。

题目理解

分治

P7883 平面最近点对(加强加强版
题目大意:在同一平面内, 给定一些点,求最近的点对。
题解:
\(O(n^2)\)解法:
一眼看出\(for\)循环枚举每个点对, 算出最大值。
(代码就不必贴了吧)
\(O(nlogn)\)解法:
根据人类启发式智慧, 和对于事物的普遍认知规律, 我们考虑如何对暴力算法进一步降低时间复杂度。不难想到, 若平面内只有两个点, 那最近点对便可以瞬间求出, 那三个呢?四个呢?我们如何利用已知的信息来更快的得到我们的答案?
分治法就来了, 我们将平面一直折半划分, 划分到只有一个或两个点, 可以快速得到答案, 对于更大区间的答案我们就考虑如何利用的这些小区间的答案来优化。 假设:我们已经算出 \(f(l, mid)\)\(f(mid + 1, r)\) 的平面最近点对的距离 \(d\) , 并且接下来更新 \(f(l, r)\) 的答案, 考虑没有计算过的点对一定一个在左区间, 一个在右区间, 那么我们可以一眼看出, 只有 \((mid - d, mid + d)\) 内的点对之间的距离才有可能比 \(d\) 小。 这样就可以不用计算也可以知道, \(mid - d\)\((mid + d)\) 以外的一定不会是最近点对, 节省了大量时间。
\(AC\) 代码

#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
constexpr int N = 4e5 + 10;   
inline int read() {
	int x = 0, f = 1; char c = getchar();
	while (c < '0' || c > '9') {
		if (c == '-') f = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9') {
		x = (x << 3) + (x << 1) + (c ^ 48);
		c = getchar(); 
	}
	return x * f;
}
int n;
struct Node{
	int x, y;
}P[N];
bool cmpx(Node a, Node b) { 
	if (a.x == b.x) return a.y < b.y;
	return a.x < b.x; 
}
inline bool cmpy(int a, int b) { return P[a].y < P[b].y; }
inline long long min(long long x, long long y) { return x < y ? x : y; }
//inline double fabs(double x) { return x < 0 ? -x : x; }
inline long long dist(int i, int j) { return 1ll * (P[i].x - P[j].x) * (P[i].x - P[j].x) + 1ll * (P[i].y - P[j].y) * (P[i].y - P[j].y); }
int tmp[N];
long long merge(int l, int r) {
	int mid = l + r >> 1, cnt = 0;
	long long d = 1e18; 
	if (l == r) return d;
	if (l + 1 == r) return dist(l, r);
	d = min(merge(l, mid), merge(mid + 1, r));
	for (int i = l; i <= r; i++) 
		if (1ll * (P[mid].x - P[i].x) * (P[mid].x - P[i].x) < d) tmp[++cnt] = i;
	sort(tmp + 1, tmp + 1 + cnt, cmpy);
	for (int i = 1; i < cnt; i++) 
		for (int j = i + 1; j <= cnt && 1ll * (P[tmp[j]].y - P[tmp[i]].y) * (P[tmp[j]].y - P[tmp[i]].y) < d; j++) 
			d = min(d, dist(tmp[i], tmp[j]));
	return d;
}
int main() {
	n = read();
	for (int i = 1; i <= n; i++) P[i].x = read(), P[i].y = read(); 
	sort(P + 1, P + 1 + n, cmpx);
	printf("%lld\n", merge(1, n));
	return 0;
} 

CDQ分治

P3810 【模板】三维偏序(陌上花开)
前置知识——逆序对
如果你会求逆序对的话, 那么对于三维偏序, 我们可以先 \(sort\) 排一次序, 直接消掉一维, 剩余两维, CDQ分治解决。
流程: 1. \((l, mid)\)\((mid + 1, r)\) 递归计算。
2. 计算 \((l, mid)\) 对右区间 \((mid + 1, r)\) 的贡献, 将 \((l, mid)\) 与 $ (mid + 1, r) $ 分别排序, 使其内部第二维有序, 利用单调性和双指针, 和值域树状数组就可以计算所有贡献了。
这样排序加上分治时间复杂度带两个log, 但分治的过程和归并排序及其相似, 所以我们不用 $ sort $ , 直接上 $ inplace_merge $ 就可以做到 \(O(nlogn)\) 了!!
附上代码

#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
const int MAXN = 2e5 + 10;
int n, k;
struct Ele{
	int a, b, c;
}S[N];
struct P{
	int b, c, num, ans;
}S1[N];
bool Ecmp(int i, int j) {
	return S[i].a == S[j].a && S[i].b == S[j].b && S[i].c == S[j].c;
}
bool cmpa(Ele x, Ele y) { 
	if (x.a == y.a) {
		if (x.b == y.b) return x.c < y.c;
		return x.b < y.b;
	}
	return x.a < y.a;
}
bool cmpb(P x, P y) {
	if (x.b == y.b) return x.c < y.c;
	return x.b < y.b; 
}

int cntS = 0;
struct B_tr{
	int tr[MAXN];
	int lowbit(int x) { return x & -x; }
	void modify(int x, int y) {
		while (x <= k) {
			tr[x] += y;
			x += lowbit(x);
		}
		return;
	}
	int query(int x) {
		int res = 0;
		while (x > 0) {
			res += tr[x];
			x -= lowbit(x);
		}
		return res;
	}
}t;
void Solve(int l, int r) {
	int mid = l + r >> 1;
	if (l == r) return;
	Solve(l, mid); Solve(mid + 1, r);
	int i = l, j = mid + 1;
	while (j <= r) {
		while (i <= mid && S1[i].b <= S1[j].b) {
			t.modify(S1[i].c, S1[i].num);
			i++;
		}
		S1[j].ans += t.query(S1[j].c);
		j++;
	}
	for (j = l; j < i; j++) t.modify(S1[j].c, -S1[j].num);
	inplace_merge(S1 + l, S1 + mid + 1, S1 + r + 1, cmpb);
	return;
}
int f[N];
int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; i++) {
		int a, b, c; scanf("%d%d%d", &a, &b, &c);
		S[i] = (Ele){a, b, c};
	}
	sort(S + 1, S + 1 + n, cmpa);
	int cnt = 0;
	for (int i = 1; i <= n; i++) {
		cnt++;
		if (!Ecmp(i, i + 1) || i + 1 > n) {
			S1[++cntS] = (P){S[i].b, S[i].c, cnt, 0};
			cnt = 0;
		} 
	}
	Solve(1, cntS);
	for (int i = 1; i <= cntS; i++) f[S1[i].ans + S1[i].num - 1] += S1[i].num;
	for (int i = 0; i < n; i++) printf("%d\n", f[i]);
	return 0;
}

P3157 [CQOI2011] 动态逆序对
如果说上一道题你体会不到CDQ分治与普通分治的区别, 那么这道题, CDQ分治的特点会更明显。
题目大意:
现在给出 $ 1∼n $ 的一个排列,按照某种顺序依次删除 $ m $ 个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序对数。
其实就是加上了时间这一维, 我们考虑删掉一个数时, 会对逆序对数产生多少负贡献, 也就是消失了几对逆序对。 分为两类:
定义删掉的数为 \(a_i\) ,$ a_i $ 被删掉的时间为 $ c_i $, 删掉 $ a_i $ 后会影响

  1. $ a_j $ 满足 $ j < i, a_j > a_i, c_j > c_i $ 。
  2. $ a_k $ 满足 $ k > i, a_k < a_i, c_k > c_i $ 。
    所以CDQ分治时不仅要考虑左区间对右区间的贡献, 还要考虑右区间对左区间的贡献。
    贴代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
#define int long long
using namespace std;
const int N = 1e5 + 10;
const int M = 5e4 + 10;
int n, m;
struct Ele{
	int v, t = 5e4 + 10, cnt;
}S[N];
int pos[N];
bool cmpv(Ele x, Ele y) {
	if (x.v == y.v) return x.t < y.t;
	return x.v < y.v;
}
bool cmpt(Ele x, Ele y) {
	if (x.t == y.t) return x.v < y.v;
	return x.t < y.t;
}
struct B_tr {
	int tr[N << 1];
	void clear() { memset(tr, 0, sizeof(tr)); }
	int lowbit(int x) { return x & -x; }
	void modify(int x, int y) {
		while (x <= N) {
			tr[x] += y;
			x += lowbit(x); 
		} 
		return;
	}
	int query(int x) {
		int res = 0;
		while (x > 0) {
			res += tr[x];
			x -= lowbit(x);
		}
		return res;
	}
}t;
int ans = 0;
void input() {
	for (int i = 1; i <= n; i++) {
		ans += t.query(n) - t.query(S[i].v);
		t.modify(S[i].v, 1);
	}
	t.clear();
	return;
}
void Solve(int l, int r) {
	int mid = l + r >> 1;
	if (l == r) return;
	Solve(l, mid); Solve(mid + 1, r);
	sort(S + l, S + mid + 1, cmpv);
	sort(S + mid + 1, S + r + 1, cmpv);
	int i = r, j = mid;
	while (i >= mid + 1) {
		while (j >= l && S[i].v < S[j].v) {
			t.modify(S[j].t, 1);
			j--;
		}
		S[i].cnt += t.query(N) - t.query(S[i].t);
		i--;
	}
	for (i = mid; i > j; i--) t.modify(S[i].t, -1);
	i = l, j = mid + 1;
	while (i <= mid) {
		while (j <= r && S[i].v > S[j].v) {
			t.modify(S[j].t, 1);
			j++;
		}
		S[i].cnt += t.query(N) - t.query(S[i].t);
		i++;
	}
	for (i = mid + 1; i < j; i++) t.modify(S[i].t, -1);
	return;
}
int sum[M];
signed main() {
	scanf("%lld%lld", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &S[i].v);
		pos[S[i].v] = i;
	}
	input();
	for (int i = 1; i <= m; i++) {
		int x; scanf("%lld", &x);
		S[pos[x]].t = i;
	}
	Solve(1, n);
	sort(S + 1, S + 1 + n, cmpt);
	for (int i = 1; i <= m; i++) sum[i] = sum[i - 1] + S[i].cnt;
	for (int i = 1; i <= m; i++) printf("%lld\n", ans - sum[i - 1]);	
	return 0;
}
posted @ 2023-11-17 11:39  qqrj  阅读(86)  评论(0)    收藏  举报