CDQ分治

其本质是对分治的进一步理解。

先来看一个问题。

二维偏序

给定 n 个二元组,第 i 个二元组 pi=(xi,yi), 求顺序对个数。
即求满足 xi<xjyi<yj(i,j) 对数

很容易想到以 x 为第一关键字从小到大排序,y 用树状数组维护计数即可。


现在我们将该问题升高难度

三维偏序

n 个元素,第 i 个元素有 ai,bi,ci 三个属性,设 f(i) 表示满足 ajaibjbicjcijij 的数量。

对于 d[0,n),求 f(i)=d 的数量。

我们发现使用排序和树状数组处理前两位后,最后一位无法处理。

这是我们要用到 CDQ分治。

CDQ分治

假设当前我们在处理 [l,r] 这个区间里的结果,并且我们已经处理完其两个子区间 [l,mid][mid+1,r] 的结果。

也就是说,我们现在需要在两个子区间中各选一个元素,并计算结果。

由于我们在分治前将原序列以 a 为第一关键字从小到大排序,所以 [l,mid] 中所有元素的 a 均不大于 [mid+1,r] 中元素的 a

我们再将两个子区间按照 b 值排序,然后使用双指针 + 树状数组 处理最后两维。

大致长这样:

  // 排序
  sort(v + l, v + mid + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
  sort(v + mid + 1, v + r + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
  // 双指针
  int j = l;  
  for (int i = mid + 1; i <= r; i++) {
    for (; j <= mid && v[j].y <= v[i].y; j++) {
      Modify(v[j].z, 1); // 树状数组修改
    }
    v[i].res += Query(v[i].z);  // 累加答案
  }

但是当前区间所进行的修改会对其父亲区间产生后效,所以我们需要将它撤回(或者清空)。

  for (j--; j >= l; j--) {
    Modify(v[j].z, -1);
  }

还有一点,由于处理当前区间前先处理两个子区间后在进行了排序,所以子区间的排序不会对当前区间产生影响。

整个代码长这样:

void Cdq(int l, int r) {
  if (l == r) return; // 边界,只有一个元素,不会产生答案,直接返回
  int mid = (l + r) >> 1;
  // 先处理好子区间
  Cdq(l, mid);
  Cdq(mid + 1, r);
  sort(v + l, v + mid + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
  sort(v + mid + 1, v + r + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
  int j = l;
  for (int i = mid + 1; i <= r; i++) {
    for (; j <= mid && v[j].y <= v[i].y; j++) {
      Modify(v[j].z, v[j].s);
    }
    v[i].res += Query(v[i].z);
  }
  // 撤回当前区间进行的操作
  for (j--; j >= l; j--) {
    Modify(v[j].z, -v[j].s);
  }
}

纵观整个过程,我们处理的是 aj<aibj<bicj<ci,而原题可以取等。所以我们需要将原来的元素集合去重。

至于为什么不直接在分治里取等,原因大致是分类情况太多了。(猜的)

完整代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 2e5 + 3;

// 元素,包含三维,数量,结果
struct V {
  int x, y, z, s;
  int res;
} v[kmax];

int n, m, k;
int c[kmax];
long long res[kmax];

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

void Modify(int x, int v) {
  for (; x < kmax; x += Lowbit(x)) {
    c[x] += v;
  }
}

int Query(int x) {
  int tot = 0;
  for (; x; x -= Lowbit(x)) {
    tot += c[x];
  }
  return tot;
}

void Cdq(int l, int r) {
  if (l == r) return;
  int mid = (l + r) >> 1;
  Cdq(l, mid);
  Cdq(mid + 1, r);
  sort(v + l, v + mid + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
  sort(v + mid + 1, v + r + 1, [](V p, V q) { return p.y < q.y || p.y == q.y && p.z < q.z; });
  int j = l;
  for (int i = mid + 1; i <= r; i++) {
    for (; j <= mid && v[j].y <= v[i].y; j++) {
      Modify(v[j].z, v[j].s); // 注意现在修改的值是该元素的数量,不一定是1
    }
    v[i].res += Query(v[i].z);
  }
  for (j--; j >= l; j--) {
    Modify(v[j].z, -v[j].s);
  }
}

int main() {
  scanf("%d%d", &n, &k);
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d", &v[i].x, &v[i].y, &v[i].z);
  }
  // 以x为第一关键字排序
  sort(v + 1, v + n + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y || p.x == q.x && p.y == q.y && p.z < q.z; });
  // 元素去重
  for (int i = 1; i <= n; i++) {
    if (v[i].x != v[m].x || v[i].y != v[m].y || v[i].z != v[m].z) {
      v[++m] = {v[i].x, v[i].y, v[i].z, 1};
    } else {
      v[m].s++;
    }
  }
  // 分治
  Cdq(1, m);
  // 统计最终答案
  for (int i = 1; i <= m; i++) {
    int num = v[i].res + v[i].s - 1;
    res[num] += v[i].s;
  }
  for (int i = 0; i < n; i++) {
    printf("%lld\n", res[i]);
  }
  return 0;
}

接下来我们看几道练习题

Mokia

我们尝试将一次询问的区间拆分成四个小询问。即对于一组询问 (x1,y1,x2,y2) 拆分成 (0,0)(x2,y2)(0,0)(x11,y11)(0,0)(x11,y2)(0,0)(x2,y11),前者对答案做出 1 的贡献,后者做出 1 贡献。

我们发现每组小询问的起始点都是 (0,0),所以我们只需记录其终点的位置 (x,y),这是二维的。

然后由于加入了修改,我们还要考虑时间这一维,总共就是三维。

不需要去重,直接套用分治即可。

注意 x1y1 可能为 0,树状数组会卡死。所以需要将所有元素的 xy 各偏移 1

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 2e6 + 3;

struct V {
  int t, x, y;
  int op, res;
} p[kmax];

int n, pc;
int c[kmax];

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

void Modify(int x, int v) {
  for (; x <= n; x += Lowbit(x)) {
    c[x] += v;
  }
}

int Query(int x) {
  int tot = 0;
  for (; x; x -= Lowbit(x)) {
    tot += c[x];
  }
  return tot;
}

void Cdq(int l, int r) {
  if (l == r)
    return;
  int mid = (l + r) >> 1;
  Cdq(l, mid), Cdq(mid + 1, r);
  sort(p + l, p + mid + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y; });
  sort(p + mid + 1, p + r + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y; });
  int j = l;
  for (int i = mid + 1; i <= r; i++) {
    for (; j <= mid && p[j].x <= p[i].x; j++) {
      if (!p[j].op) {
        Modify(p[j].y, p[j].res);
      }
    }
    if (p[i].op) {
      p[i].res += Query(p[i].y);
    }
  }
  for (j--; j >= l; j--) {
    if (!p[j].op) {
      Modify(p[j].y, -p[j].res);
    }
  }
}

int main() {
  scanf("%d%d", &n, &n);
  n++;
  for (int op, x, y, _x, _y, v; scanf("%d", &op);) {
    if (op == 3)
      break;
    if (op == 1) {
      scanf("%d%d%d", &x, &y, &v);
      p[++pc] = {pc, ++x, ++y, 0, v};
    } else {
      scanf("%d%d%d%d", &x, &y, &_x, &_y);
      // 拆分成四个元素
      p[++pc] = {pc, x, y, 1}, p[++pc] = {pc, ++_x, ++_y, 1};
      p[++pc] = {pc, _x, y, 1}, p[++pc] = {pc, x, _y, 1};
    }
  }
  Cdq(1, pc);
  // 以时间排序
  sort(p + 1, p + pc + 1, [](V p, V q) { return p.t < q.t; });
  for (int i = 1; i <= pc; i++) {
    if (!p[i].op)
      continue;
    printf("%d\n", p[i].res + p[++i].res - p[++i].res - p[++i].res);
    // 四个元素的答案统一到一起
  }
  return 0;
}

CQOI2011 动态逆序对

我们发现=只要求出初始逆序对,然后每次计算出减少的逆序对数量即可。

对于每一个被删的元素, 其减少的逆序对数量由两部分组成。

  1. 位于该元素前面,删除时间比该元素晚,权值比该元素大的元素数量。

  2. 位于该元素后面,删除时间比该元素晚,权值比该元素小的元素数量。

由此不难得出一个元素三个维度的属性,分别是位置删除时间权值

使用CDQ分治即可。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 1e5 + 3;

struct Node {
    int e, v, d;
    int o, t;
} c[kmax << 1];

int n, m, ct;
int p[kmax], a[kmax], t[kmax];
long long ans[kmax];

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

void Update(int x, int k) {
    for (; x <= n; x += Lowbit(x)) {
        t[x] += k;
    }
}

int Query(int x) {
    int tot = 0;
    for (; x; x -= Lowbit(x)) {
        tot += t[x];
    }
    return tot;
}

void Cdq(int l, int r) {
    if (l == r)
        return;
    int mid = (l + r) >> 1;
    int j = l;
    Cdq(l, mid);
    Cdq(mid + 1, r);
    sort(c + l, c + mid + 1, [](Node p, Node q) { return p.d < q.d; });
    sort(c + mid + 1, c + r + 1, [](Node p, Node q) { return p.d < q.d; });
    for (int i = mid + 1; i <= r; i++) {
        for (; j <= mid && c[j].d <= c[i].d; j++) {
            Update(c[j].v, c[j].e);
        }
        ans[c[i].o] += c[i].e * (Query(n) - Query(c[i].v));
    }
    for (int i = l; i < j; i++) {
        Update(c[i].v, -c[i].e);
    }
    j = mid;
    for (int i = r; i > mid; i--) {
        for (; j >= l && c[j].d >= c[i].d; j--) {
            Update(c[j].v, c[j].e);
        }
        ans[c[i].o] += c[i].e * Query(c[i].v - 1);
    }
    for (int i = mid; i > j; i--) {
        Update(c[i].v, -c[i].e);
    }
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        p[a[i]] = i;
        ct++;
        c[ct] = { 1, a[i], i, 0, ct };
    }
    for (int i = 1, x; i <= m; i++) {
        cin >> x;
        ct++;
        c[ct] = { -1, x, p[x], i, ct };
    }
    Cdq(1, ct);
    for (int i = 0; i < m; i++) {
        if (i)
            ans[i] += ans[i - 1];
        cout << ans[i] << '\n';
    }
    return 0;
}

LIS2

先看看普通的 LIS

对于当前考虑的位置 i, 我们需要找到一个 j<iaj<aifj 的最大值。

我们尝试用树状数组维护。(我们不会删除元素,故最大值不会减少)

复杂度 O(nlogn)

但此题是二维的,我们可以尝试分治。

与普通的分治不同的是,我们这里要先处理左子区间,再处理当前区间,最后处理右子区间。因为 dp 数组不能记录当前的答案从哪一位置转移得到,我们只知道这个答案以当前元素结尾。

反之,如果处理完右子区间后在合并处理当前区间,右子区间的答案会算重。

实现细节:

  1. 离散化。
  2. 树状数组撤回操作改清空。
  3. 当前 右的顺序。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 1e5 + 3;

struct V {
    int x, y, id;
} p[kmax];

int n, f[kmax];
int b[kmax], m;
int c[kmax];
int res;

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

void Modify(int x, int v) {
  for (; x <= m; x += Lowbit(x)) {
    c[x] = max(c[x], v);
  }
}

void Clear(int x) {
  for (; x <= m; x += Lowbit(x)) {
    c[x] = 0;
  }
}

int Query(int x) {
  int tot = 0;
  for (; x; x -= Lowbit(x)) {
    tot = max(tot, c[x]);
  }
  return tot;
}

void Cdq(int l, int r) {
  if (l == r)  return;
  int mid = (l + r) >> 1;
  Cdq(l, mid);
  sort(p + l, p + mid + 1, [](V p, V q) { return p.x < q.x; });
  sort(p + mid + 1, p + r + 1, [](V p, V q) { return p.x < q.x; });
  int j = l;
  for (int i = mid + 1; i <= r; i++) {
    for (; j <= mid && p[j].x < p[i].x; j++) {
      Modify(p[j].y, f[p[j].id]);
    }
    f[p[i].id] = max(f[p[i].id], Query(p[i].y - 1) + 1);
  }
  for (j--; j >= l; j--) {
    Clear(p[j].y);
  }
  sort(p + mid + 1, p + r + 1, [](V p, V q) { return p.id < q.id; });
  Cdq(mid + 1, r);
}

int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) {
    scanf("%d%d", &p[i].x, &p[i].y);
    p[i].id = i;
  }
  for (int i = 1; i <= n; i++) {
    f[i] = 1;
    b[i] = p[i].y;
  }
  sort(b + 1, b + n + 1);
  m = unique(b + 1, b + n + 1) - b - 1;
  for (int i = 1; i <= n; i++) {
    p[i].y = lower_bound(b + 1, b + m + 1, p[i].y) - b;
  }
  Cdq(1, n);
  for (int i = 1; i <= n; i++) {
    res = max(res, f[i]);
  }
  printf("%d\n", res);
  return 0;
}

斜率升级

初始有一个空集,有 m 个操作:

  1. x, y :向点集中加入一个点 (x,y)

  2. k :询问一个 k,求当前点集中,最大的 k×x+y

CDQ + 斜率即可, 二分查找答案。

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <stack>

using namespace std;

const int kmax = 1e5 + 3;

struct V {
  int x, y, id;
  long long res;
  int op;
} v[kmax], stk[kmax];

int n, tp;
 
// 计算斜率
double Slope(V p, V q) {
  return 1.0 * (q.y - p.y) / (q.x - p.x);
}

// 计算结果
long long Calc(int x, int k) {
  return 1ll * k * stk[x].x + stk[x].y;
}

bool Check(V p, V q, int k) {
  return 1.0 * (q.y - p.y) / (q.x - p.x) >= k;
}

void Cdq(int l, int r) {
  if (l == r) return;
  int mid = (l + r) >> 1;
  Cdq(l, mid), Cdq(mid + 1, r);
  tp = 0;
  sort(v + l, v + mid + 1, [](V p, V q) { return p.x < q.x || p.x == q.x && p.y < q.y; });
  for (int i = l; i <= mid; i++) {
    if (v[i].op) continue;
    for (; tp >= 2 && Slope(stk[tp], v[i]) >= Slope(stk[tp - 1], stk[tp]); tp--) {
    }
    stk[++tp] = v[i];
  }
  if (!tp) return;
  for (int i = mid + 1, _l, _r; i <= r; i++) {
    if (!v[i].op) continue;
    // 二分查找答案
    for (_l = 1, _r = tp - 1; _l <= _r;) {
      int mid = (_l + _r) >> 1;
      if (Calc(mid, v[i].x) >= Calc(mid + 1, v[i].x)) {
        _r = mid - 1;
      } else {
        _l = mid + 1;
      }
    }
    v[i].res = max(v[i].res, Calc(_r + 1, v[i].x));
  }
}

int main() {
  scanf("%d", &n);
  for (int i = 1; i <= n; i++) {
    scanf("%d%d", &v[i].op, &v[i].x);
    v[i].id = i;
    if (--v[i].op == 0) {
      scanf("%d", &v[i].y);
    }
  }
  // sort(v + 1, v + n + 1, [](V p, V q) { return p.op < q.op || p.op == q.op && p.x < q.x; });
  Cdq(1, n);
  sort(v + 1, v + n + 1, [](V p, V q) { return p.id < q.id; });
  for (int i = 1; i <= n; i++) {
    if (!v[i].op) continue;
    printf("%lld\n", v[i].res);
  }
  return 0;
}

完结撒花 \ / \ / \ /

posted @   ereoth  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示