从一维偏序开始,浅谈cdq分治的降维思想


前言

我在刚学cdq分治的时候总是没搞懂cdq分治是怎么降维的,网上的博客大都只是告诉我们要这么做,导致我只学会cdq分治的模板,却没理解cdq分治的降维思想,于是就有了这一篇博客。

一、一维偏序

x x x 轴上有 n n n 个不同的点,第 i i i 个点在 x i x_i xi 的位置,对于 1 ≤ i ≤ n 1\le i\le n 1in,求满足 x j ≤ x i x_j\le x_i xjxi j ≠ i j\neq i j=i j j j 的个数。

一维偏序还是比较简单的,对于一个数,比它小的数的个数,恰好是对整个数组排完序后,在它前面的数的个数。

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 2e5 + 5;
int n, x[N], id[N], ans[N];
void div1(int l, int r) {
  sort(id + l, id + r + 1, [](int a, int b) { return x[a] < x[b]; });
  for (int i = l; i <= r; i++)
    ans[id[i]] += i - l;
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> x[i], id[i] = i;
  div1(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  cout << endl;
  return 0;
}

二、二维偏序

在平面上有 n n n 个不同的点,第 i i i 个点在 ( x i , y i ) (x_i, y_i) (xi,yi) 的位置,对于 1 ≤ i ≤ n 1\le i\le n 1in,求满足 x j ≤ x i x_j\le x_i xjxi y j ≤ y i y_j\le y_i yjyi j ≠ i j\neq i j=i j j j 的个数。

二维偏序显然没一维偏序那么简单,但是如果可以将二维偏序变成一维偏序,那就好办多了。

任意取出两个不同的点 i , j i,j i,j,如果 x j ≤ x i x_j\le x_i xjxi y j ≤ y i y_j\le y_i yjyi,我们就说 j j j i i i 有贡献。

我们先将所有点按 x x x 值从小到大排序,新数组第 i i i 个点对应原数组第 i d i id_i idi 个点,假设从新数组中任意取出两个点 i , j i,j i,j,那么只有在 j < i j<i j<i y i d j ≤ y i d i y_{id_j}\le y_{id_i} yidjyidi 时, i d j id_j idj i d i id_i idi 有贡献,也就是说新数组左边的点对右边的点可能有贡献,而右边的点对左边的点不可能有贡献。

d i v 2 ( l , r ) div2(l, r) div2(l,r) 表示计算"第 l l l 个点到第 r r r 个点"对"第 l l l 个点对第 r r r 个点的贡献",也就是区间 [ l , r ] [l, r] [l,r] 内所有点的相互贡献。

我们取 l , r l,r l,r 的中点 m i d = ⌊ l + r 2 ⌋ mid=\left \lfloor \frac{l+r}{2} \right \rfloor mid=2l+r,然后将 d i v 2 ( l , r ) div2(l, r) div2(l,r) 分解为区间 [ l , m i d ] [l, mid] [l,mid] 内所有点的相互贡献 ( d i v 2 ( l , m i d ) ) (div2(l, mid)) (div2(l,mid)),区间 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 内所有点的相互贡献 ( d i v 2 ( m i d + 1 , r ) ) (div2(mid+1, r)) (div2(mid+1,r)) 和区间 [ l , m i d ] [l, mid] [l,mid] 内所有点对区间 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 内所有点的贡献(区间 [ l , r ] [l,r] [l,r] 已经按 x x x 值排好序且右边的点对左边的点没有贡献)。

前面两个 d i v 2 ( l , m i d ) div2(l, mid) div2(l,mid) d i v 2 ( m i d + 1 , r ) div2(mid+1, r) div2(mid+1,r) 可以递归处理,最后一个需要单独处理,而这恰好是降维的关键。

我们不妨将 [ l , m i d ] [l, mid] [l,mid] 内所有点的 x x x 值置为 0 0 0 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 内所有点的 x x x 值置为 1 1 1,表示只有 x x x 值是 0 0 0 的点对 x x x 值是 1 1 1 的点有贡献。

现在问题变成对于 l ≤ i ≤ r l\le i\le r lir x i = 1 x_i=1 xi=1,求满足 y j ≤ y i y_j\le y_i yjyi j ≠ i j\neq i j=i x j = 0 x_j=0 xj=0 j j j 的个数,至此,我们成功地将二维偏序降维变成一维偏序。

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 2e5 + 5;
int n, x[N], x2[N], y[N], id[N], ans[N];
void div1(int l, int r) {
  sort(id + l, id + r + 1, [](int a, int b) { return pii(y[a], x2[a]) < pii(y[b], x2[b]); });
  int cnt = 0;
  for (int i = l; i <= r; i++) {
    if (x2[id[i]] == 0)
      cnt++;
    else
      ans[id[i]] += cnt;
  }
}
void div2(int l, int r) {
  if (l == r) return;
  sort(id + l, id + r + 1, [](int a, int b) { return pii(x[a], y[a]) < pii(x[b], y[b]); });
  int mid = (l + r) >> 1;
  div2(l, mid), div2(mid + 1, r);
  for (int i = l; i <= mid; i++)
    x2[id[i]] = 0;
  for (int i = mid + 1; i <= r; i++)
    x2[id[i]] = 1;
  div1(l, r);
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> x[i] >> y[i], id[i] = i;
  div2(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  cout << endl;
  return 0;
}

三、 三维偏序

在平面上有 n n n 个不同的点,第 i i i 个点在 ( x i , y i , z i ) (x_i, y_i, z_i) (xi,yi,zi) 的位置,对于 1 ≤ i ≤ n 1\le i\le n 1in ,求满足 x j ≤ x i x_j\le x_i xjxi y j ≤ y i y_j\le y_i yjyi z j ≤ z i z_j\le z_i zjzi j ≠ i j\neq i j=i j j j 的个数。

我们来套一下二维偏序降维变成一维偏序的思路。(套娃?没错)

三维偏序显然没二维偏序那么简单,但是如果可以将三维偏序变成二维偏序,那就又好办多了。

我们先将所有点按 x x x 值从小到大排序,设 d i v 3 ( l , r ) div3(l, r) div3(l,r) 表示计算"第 l l l 个点到第 r r r 个点"对"第 l l l 个点对第 r r r 个点的贡献",也就是区间 [ l , r ] [l, r] [l,r] 内所有点的相互贡献。

我们取 l , r l,r l,r 的中点 m i d = ⌊ l + r 2 ⌋ mid=\left \lfloor \frac{l+r}{2} \right \rfloor mid=2l+r,然后将 d i v 3 ( l , r ) div3(l, r) div3(l,r) 分解为区间 [ l , m i d ] [l, mid] [l,mid] 内所有点的相互贡献 ( d i v 3 ( l , m i d ) ) (div3(l, mid)) (div3(l,mid)),区间 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 内所有点的相互贡献 ( d i v 3 ( m i d + 1 , r ) ) (div3(mid+1, r)) (div3(mid+1,r)) 和区间 [ l , m i d ] [l, mid] [l,mid] 内所有点对区间 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 内所有点的贡献(区间 [ l , r ] [l,r] [l,r] 已经按 x x x 值排好序且右边的点对左边的点没有贡献)。

前面两个 d i v 3 ( l , m i d ) div3(l, mid) div3(l,mid) d i v 3 ( m i d + 1 , r ) div3(mid+1, r) div3(mid+1,r) 可以递归处理,最后一个需要单独处理,而这又恰好是降维的关键。

我们不妨将 [ l , m i d ] [l, mid] [l,mid] 内所有点的 x x x 值置为 0 0 0 [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 内所有点的 x x x 值置为 1 1 1,表示只有 x x x 值是 0 0 0 的点对 x x x 值是 1 1 1 的点有贡献。

现在问题变成对于 l ≤ i ≤ r l\le i\le r lir x i = 1 x_i=1 xi=1,求满足 y j ≤ y i y_j\le y_i yjyi z j ≤ z i z_j\le z_i zjzi j ≠ i j\neq i j=i x j = 0 x_j=0 xj=0 j j j 的个数,至此,我们成功地将三维偏序降维变成二维偏序。

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 2e5 + 5;
int n, x[N], x2[N], y[N], y2[N], z[N], id[N], ans[N];
void div1(int l, int r) {
  sort(id + l, id + r + 1, [](int a, int b) { return tie(z[a], x2[a], y2[a]) < tie(z[b], x2[b], y2[b]); });
  int cnt = 0;
  for (int i = l; i <= r; i++) {
    if (x2[id[i]] == 0 && y2[id[i]] == 0)
      cnt++;
    if (x2[id[i]] == 1 && y2[id[i]] == 1)
      ans[id[i]] += cnt;
  }
}
void div2(int l, int r) {
  if (l == r) return;
  sort(id + l, id + r + 1, [](int a, int b) { return tie(y[a], z[a], x2[a]) < tie(y[b], z[b], x2[b]); });
  int mid = (l + r) >> 1;
  div2(l, mid), div2(mid + 1, r);
  for (int i = l; i <= mid; i++)
    y2[id[i]] = 0;
  for (int i = mid + 1; i <= r; i++)
    y2[id[i]] = 1;
  div1(l, r);
}
void div3(int l, int r) {
  if (l == r) return;
  sort(id + l, id + r + 1, [](int a, int b) { return tie(x[a], y[a], z[a]) < tie(x[b], y[b], z[b]); });
  int mid = (l + r) >> 1;
  div3(l, mid), div3(mid + 1, r);
  for (int i = l; i <= mid; i++)
    x2[id[i]] = 0;
  for (int i = mid + 1; i <= r; i++)
    x2[id[i]] = 1;
  div2(l, r);
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> x[i] >> y[i] >> z[i], id[i] = i;
  div3(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  cout << endl;
  return 0;
}

四、四维偏序

在平面上有 n n n 个不同的点,第 i i i 个点在 ( x i , y i , z i , w i ) (x_i, y_i, z_i, w_i) (xi,yi,zi,wi) 的位置,对于 1 ≤ i ≤ n 1\le i\le n 1in,求满足 x j ≤ x i x_j\le x_i xjxi y j ≤ y i y_j\le y_i yjyi z j ≤ z i z_j\le z_i zjzi w j ≤ w i w_j\le w_i wjwi j ≠ i j\neq i j=i j j j 的个数。

四维偏序显然…呃…和上面的降维过程差不多(看了这么多,别说你还不会,否则我就生气了(bushi)

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 2e5 + 5;
int n, x[N], x2[N], y[N], y2[N], z[N], z2[N], w[N], id[N], ans[N];
void div1(int l, int r) {
  sort(id + l, id + r + 1, [](int a, int b) { return tie(w[a], x2[a], y2[a], z2[a]) < tie(w[b], x2[b], y2[b], z2[b]); });
  int cnt = 0;
  for (int i = l; i <= r; i++) {
    if (x2[id[i]] == 0 && y2[id[i]] == 0 && z2[id[i]] == 0)
      cnt++;
    if (x2[id[i]] == 1 && y2[id[i]] == 1 && z2[id[i]] == 1)
      ans[id[i]] += cnt;
  }
}
void div2(int l, int r) {
  if (l == r) return;
  sort(id + l, id + r + 1, [](int a, int b) { return tie(z[a], w[a], x2[a], y2[a]) < tie(z[b], w[b], x2[b], y2[b]); });
  int mid = (l + r) >> 1;
  div2(l, mid), div2(mid + 1, r);
  for (int i = l; i <= mid; i++)
    z2[id[i]] = 0;
  for (int i = mid + 1; i <= r; i++)
    z2[id[i]] = 1;
  div1(l, r);
}
void div3(int l, int r) {
  if (l == r) return;
  sort(id + l, id + r + 1, [](int a, int b) { return tie(y[a], z[a], w[a], x2[a]) < tie(y[b], z[b], w[b], x2[b]); });
  int mid = (l + r) >> 1;
  div3(l, mid), div3(mid + 1, r);
  for (int i = l; i <= mid; i++)
    y2[id[i]] = 0;
  for (int i = mid + 1; i <= r; i++)
    y2[id[i]] = 1;
  div2(l, r);
}
void div4(int l, int r) {
  if (l == r) return;
  sort(id + l, id + r + 1, [](int a, int b) { return tie(x[a], y[a], z[a], w[a]) < tie(x[b], y[b], z[b], w[b]); });
  int mid = (l + r) >> 1;
  div4(l, mid), div4(mid + 1, r);
  for (int i = l; i <= mid; i++)
    x2[id[i]] = 0;
  for (int i = mid + 1; i <= r; i++)
    x2[id[i]] = 1;
  div3(l, r);
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> x[i] >> y[i] >> z[i] >> w[i], id[i] = i;
  div4(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  cout << endl;
  return 0;
}

五、多维偏序

只要掌握了降维的思想,无论维数有多高都可以解出来,呃…虽然能解出来,但如果维数过高会比暴力还慢。
我们设有 n n n 个点, m m m 个维度, m ≥ 2 m\ge 2 m2,cdq分治时间复杂度是 O ( n ( l o g 2 n ) m − 1 ) O(n(log_2n)^{m-1}) O(n(log2n)m1),暴力时间复杂度是 O ( m n 2 ) O(mn^2) O(mn2),当维数过高会出现 O ( n ( l o g 2 n ) m − 1 ) > O ( m n 2 ) O(n(log_2n)^{m-1})>O(mn^2) O(n(log2n)m1)>O(mn2)

六、二维偏序归并排序优化

上面的代码在每次进入 d i v 2 ( l , r ) div2(l, r) div2(l,r) 都要重新排序,但我们不用这么麻烦,假设在每次进入 d i v 2 ( l , r ) div2(l, r) div2(l,r) 前, [ l , r ] [l,r] [l,r] 已经按 x x x 值排好序,并且从 d i v 2 ( l , m i d ) div2(l, mid) div2(l,mid) d i v 2 ( m i d + 1 , r ) div2(mid+1, r) div2(mid+1,r) 出来的时候, [ l , m i d ] [l, mid] [l,mid] [ m i d + 1 , r ] [mid+1, r] [mid+1,r] 已经按 y y y 值排好序,我们希望 [ l , r ] [l, r] [l,r] y y y 值排序,只需要将两部分合并起来即可,在合并的过程中,如果新加入的是左边的点,那么让计数器加 1 1 1 ,如果新加入的是右边的点,那么当前计数器表示的恰好是左边的点中小于等于当前点的点的数量,然后累加到答案中即可。

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 2e5 + 5;
int n, x[N], y[N], id[N], id2[N], ans[N];
void div2(int l, int r) {
  if (l == r) return;
  int mid = (l + r) >> 1;
  div2(l, mid), div2(mid + 1, r);
  int i = l, j = mid + 1, k = l, cnt = 0;
  while (i <= mid || j <= r) {
    if (j == r + 1 || (i <= mid && y[id[i]] <= y[id[j]]))
      id2[k] = id[i], cnt++, i++, k++;
    else
      id2[k] = id[j], ans[id[j]] += cnt, j++, k++;
  }
  for (int i = l; i <= r; i++)
    id[i] = id2[i];
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> x[i] >> y[i], id[i] = i;
  sort(id + 1, id + n + 1, [](int a, int b) { return pii(x[a], y[a]) < pii(x[b], y[b]); });
  div2(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  cout << endl;
  return 0;
}

七、三维偏序归并排序优化+树状数组优化

我们将上面提到的计数器变成树状数组,在合并的过程中,如果新加入的是左边的点 i i i,那么让计数器下标 z [ i d [ i ] ] z[id[i]] z[id[i]] 1 1 1,如果新加入的是右边的点 i i i,那么当前计数器表示的是左边的所有 y y y 值小于等于 y [ i d [ i ] ] y[id[i]] y[id[i]] z z z 值是计数器下标的点的数量,我们将计数器中 z z z 值小于等于 z [ i d [ i ] ] z[id[i]] z[id[i]] 的点加起来,然后累加到答案中即可。

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 2e5 + 5;
struct BIT {
  int n;
  vector<int> a;
  void init(int n) {
    this->n = n;
    a.assign(n + 5, 0);
  }
  void modify(int p, int v) {
    while (p <= n)
      a[p] += v, p += p & (-p);
  }
  int query(int p) {
    int x = 0;
    while (p)
      x += a[p], p -= p & (-p);
    return x;
  }
  int query(int l, int r) {
    return l <= r ? query(r) - query(l - 1) : 0;
  }
};
int n, x[N], y[N], z[N], id[N], id2[N], ans[N];
BIT bt;
void div3(int l, int r) {
  if (l == r) return;
  int mid = (l + r) >> 1;
  div3(l, mid), div3(mid + 1, r);
  int i = l, j = mid + 1, k = l;
  while (i <= mid || j <= r) {
    if (j == r + 1 || (i <= mid && y[id[i]] <= y[id[j]]))
      id2[k] = id[i], bt.modify(z[id[i]], 1), i++, k++;
    else
      id2[k] = id[j], ans[id[j]] += bt.query(1, z[id[j]]), j++, k++;
  }
  for (int i = l; i <= mid; i++)
    bt.modify(z[id[i]], -1);
  for (int i = l; i <= r; i++)
    id[i] = id2[i];
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> x[i] >> y[i] >> z[i], id[i] = i;
  bt.init(1e5);
  sort(id + 1, id + n + 1, [](int a, int b) { return tie(x[a], y[a], z[a]) < tie(x[b], y[b], z[b]); });
  div3(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  cout << endl;
  return 0;
}

八、四维偏序降维+归并排序优化+树状数组优化

哼哼,重头戏来了,我们先将四维偏序降维变成三维偏序,再套用三维偏序的解法就解决了,怎么样,是不是很简单?

#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 2e5 + 5;
struct BIT {
  int n;
  vector<int> a;
  void init(int n) {
    this->n = n;
    a.assign(n + 5, 0);
  }
  void modify(int p, int v) {
    while (p <= n)
      a[p] += v, p += p & (-p);
  }
  int query(int p) {
    int x = 0;
    while (p)
      x += a[p], p -= p & (-p);
    return x;
  }
  int query(int l, int r) {
    return l <= r ? query(r) - query(l - 1) : 0;
  }
};
int n, x[N], x2[N], y[N], z[N], w[N], id[N], id2[N], ans[N];
BIT bt;
void div3(int l, int r) {
  if (l == r) return;
  int mid = (l + r) >> 1;
  div3(l, mid), div3(mid + 1, r);
  int i = l, j = mid + 1, k = l;
  while (i <= mid || j <= r) {
    if (j == r + 1 || (i <= mid && z[id[i]] <= z[id[j]])) {
      id2[k] = id[i];
      if (x2[id[i]] == 0)
        bt.modify(w[id[i]], 1);
      i++, k++;
    }
    else {
      id2[k] = id[j];
      if (x2[id[j]] == 1)
        ans[id[j]] += bt.query(1, w[id[j]]);
      j++, k++;
    }
  }
  for (int i = l; i <= mid; i++)
    if (x2[id[i]] == 0)
      bt.modify(w[id[i]], -1);
  for (int i = l; i <= r; i++)
    id[i] = id2[i];
}
void div4(int l, int r) {
  if (l == r) return;
  int mid = (l + r) >> 1;
  div4(l, mid), div4(mid + 1, r);
  for (int i = l; i <= mid; i++)
    x2[id[i]] = 0;
  for (int i = mid + 1; i <= r; i++)
    x2[id[i]] = 1;
  sort(id + l, id + r + 1, [](int a, int b) { return tie(y[a], z[a], w[a], x2[a]) < tie(y[b], z[b], w[b], x2[b]); });
  div3(l, r);
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++)
    cin >> x[i] >> y[i] >> z[i] >> w[i], id[i] = i;
  bt.init(1e5);
  sort(id + 1, id + n + 1, [](int a, int b) { return tie(x[a], y[a], z[a], w[a]) < tie(x[b], y[b], z[b], w[b]); });
  div4(1, n);
  for (int i = 1; i <= n; i++)
    cout << ans[i] << ' ';
  cout << endl;
  return 0;
}

九、总结

看到这里,想必大家已经对cdq分治的降维思想烂熟于心了,最后,完结撒花。

十、例题

P3810 【模板】三维偏序(陌上花开)

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值