CDQ分治

CDQ 分治

简介

CDQ 分治是一种思想而不是具体的算法,与动态规划类似。目前这个思想的拓展十分广泛,依原理与写法的不同,大致分为三类:

  • 解决和点对有关的问题。
  • 1D 动态规划的优化与转移。
  • 通过 CDQ 分治,将一些动态问题转化为静态问题。
    CDQ 分治的思想最早由 IOI2008 金牌得主陈丹琦在高中时整理并总结,它也因此得名。

解决和点对有关的问题

这类问题多数类似于「给定一个长度为 \(n\) 的序列,统计有一些特性的点对 \(\left(i,j\right)\) 的数量/找到一对点 \(\left(i,j\right)\) 使得一些函数的值最大」。
CDQ 分治解决这类问题的算法流程如下:

  1. 找到这个序列的中点 \(mid\)

  2. 将所有点对 \(\left(i,j\right)\) 划分为 3 类:\(1\le i \le mid,1\le j\le mid\) 的点对;\(1\le i\le mid,mid+1\le j\le n\) 的点对;\(mid+1\le i\le n,mid+1\le j\le n\) 的点对。
  3. \(\left(1,n\right)\) 这个序列拆成两个序列 \(\left(1,mid\right)\)\(\left(mid+1,n\right)\)。此时第一类点对和第三类点对都在这两个序列之中;
  4. 递归地处理这两类点对;
  5. 设法处理第二类点对。

可以看到 CDQ 分治的思想就是不断地把点对通过递归的方式分给左右两个区间。在实际应用时,我们通常使用一个函数 \(solve\left(l,r\right)\) 处理 \(l\le i\le r,l\le j\le r\) 的点对。上述算法流程中的递归部分便是通过 \(solve\left(l,mid\right)\)\(solve\left(mid,r\right)\) 来实现的。剩下的第二类点对则需要额外设计算法解决。

例题

三位偏序

解题思路

三维偏序是 CDQ 分治的经典问题。
题目要求统计序列里点对的个数,那试一下用 CDQ 分治。
首先将序列按 \(a\) 排序。
假设我们现在写好了 \(solve\left(l,r\right)\),并且通过递归搞定了 \(solve\left(l,mid\right)\)\(solve\left(mid+1,r\right)\)。现在我们要做的,就是统计满足 \(l\le i\le mid,mid+1\le j\le r\) 的点对 \(\left(i,j\right)\) 中,有多个点对还满足 \(a_i\le a_j,b_i\le b_j,c_i\le c_j\) 的限制条件。
稍微思考一下就会发现,那个 $a_i $ 的限制条件没啥用了:已经将序列按 \(a\) 排序,则 \(a_i\le a_j\) 可转化为 \(i\le j\)\(i\)\(mid\) 小,\(j\)\(mid\) 大,那 \(i\) 肯定比 \(j\) 要小。现在还剩下两个限制条件:\(b_i\le b_j\)\(c_i\le c_j\) , 根据这个限制条件我们就可以枚举 \(j\), 求出有多少个满足条件的 \(i\)
为了方便枚举,我们把 \(\left(l,mid\right)\)\(\left(mid+1,r\right)\) 中的点全部按照 \(b\) 的值从小到大排个序。之后我们依次枚举每一个 \(j\) , 把所有 \(b_i\le b_j\) 的点 \(i\) 全部插入到某种数据结构里(这里我们选择树状数组)。此时只要查询树状数组里有多少个点的 \(c\) 值是小于 \(c_j\) 的,我们就求出了对于这个点 \(j\),有多少个 \(i\) 可以合法匹配它了。
当我们插入一个 \(c\) 值等于 \(x\) 的点时,我们就令树状数组的 \(x\) 这个位置单点 \(+ 1\),而查询树状数组里有多少个点小于 \(x\) 的操作实际上就是在求前缀和,只要我们事先对于所有的 \(c\) 值做了离散化,我们的复杂度就是对的。
对于每一个 \(j\),我们都需要将所有 \(b_i\le b_j\) 的点 \(i\) 插入树状数组中。由于所有的 \(i\)\(j\) 都已事先按照 \(b\) 值排好序,这样的话只要以双指针的方式在树状数组里插入点,则对树状数组的插入操作就能从 \(O\left(n^2\right)\) 次降到 \(O\left(n\right)\) 次。
通过这样一个算法流程,我们就用 \(O\left(n\log n\right)\) 的时间处理完了关于第二类点对的信息了。

示例代码
#include <algorithm>
#include <cstdio>

const int maxN = 1e5 + 10;
const int maxK = 2e5 + 10;

int n, k;

struct Element {
  int a, b, c;
  int cnt;
  int res;

  bool operator!=(Element other) {
    if (a != other.a) return true;
    if (b != other.b) return true;
    if (c != other.c) return true;
    return false;
  }
};

Element e[maxN];
Element ue[maxN];
int m, t;
int res[maxN];

struct BinaryIndexedTree {
  int node[maxK];

  int lowbit(int x) { return x & -x; }

  void Add(int pos, int val) {
    while (pos <= k) {
      node[pos] += val;
      pos += lowbit(pos);
    }
    return;
  }

  int Ask(int pos) {
    int res = 0;
    while (pos) {
      res += node[pos];
      pos -= lowbit(pos);
    }
    return res;
  }
} BIT;

bool cmpA(Element x, Element y) {
  if (x.a != y.a) return x.a < y.a;
  if (x.b != y.b) return x.b < y.b;
  return x.c < y.c;
}

bool cmpB(Element x, Element y) {
  if (x.b != y.b) return x.b < y.b;
  return x.c < y.c;
}

void CDQ(int l, int r) {
  if (l == r) return;
  int mid = (l + r) / 2;
  CDQ(l, mid);
  CDQ(mid + 1, r);
  std::sort(ue + l, ue + mid + 1, cmpB);
  std::sort(ue + mid + 1, ue + r + 1, cmpB);
  int i = l;
  int j = mid + 1;
  while (j <= r) {
    while (i <= mid && ue[i].b <= ue[j].b) {
      BIT.Add(ue[i].c, ue[i].cnt);
      i++;
    }
    ue[j].res += BIT.Ask(ue[j].c);
    j++;
  }
  for (int k = l; k < i; k++) BIT.Add(ue[k].c, -ue[k].cnt);
  return;
}

int main() {
  scanf("%d%d", &n, &k);
  for (int i = 1; i <= n; i++) scanf("%d%d%d", &e[i].a, &e[i].b, &e[i].c);
  std::sort(e + 1, e + n + 1, cmpA);
  for (int i = 1; i <= n; i++) {
    t++;
    if (e[i] != e[i + 1]) {
      m++;
      ue[m].a = e[i].a;
      ue[m].b = e[i].b;
      ue[m].c = e[i].c;
      ue[m].cnt = t;
      t = 0;
    }
  }
  CDQ(1, m);
  for (int i = 1; i <= m; i++) res[ue[i].res + ue[i].cnt - 1] += ue[i].cnt;
  for (int i = 0; i < n; i++) printf("%d\n", res[i]);
  return 0;
}
posted @ 2024-05-27 14:09  小熊涛涛  阅读(26)  评论(0编辑  收藏  举报