CDQ分治模型 / p3810


前言

CDQ分治是主要解决一类和点对相关的分治方法,并且要求离线,不太好支持修改,它和点分治有着异曲同工之妙,它主要处理线性数据,而点分治主要处理树上数据

核心思路是将线性数据 [ l , r ] [l, r] [l,r] 拆成两份: [ l , m i d ] , [ m i d + 1 , r ] [l, mid], [mid+1, r] [l,mid],[mid+1,r]递归处理两个区间,再找 ( i , j ) , i ∈ [ l , m i d ] , j ∈ [ m i d + 1 , r ] (i,j),i\in[l,mid],j\in[mid+1,r] (i,j),i[l,mid],j[mid+1,r]这样的点对,CDQ分治它是一种思想,不是具体算法

它主要可以解决以下三类问题:

  1. 解决和点对有关的问题
  2. 1维动态规划的优化与转移
  3. 通过 CDQ 分治,将一些动态问题转化为静态问题

注:2可以由1转化, d p i = 1 + max ⁡ j = 1 i − 1 d p j [ a j < a i   &   b j < b i ] dp_i = 1 + \max_{j=1}^{i-1}dp_j[a_j<a_i \space \& \space b_j<b_i] dpi=1+j=1maxi1dpj[aj<ai & bj<bi] 即只有 j < i & a j < a i & b j < b i j<i\&a_j<a_i\&b_j<b_i j<i&aj<ai&bj<bi,进而这个转移可以在亚线性时间内处理
3可以由1、2转化而成,不过得要求强制离线,类似于莫队的处理

以上三类CDQ分治相关问题其实可以抽象成一个三维偏序的结构,下面重点讲述这个三维偏序结构,前置知识为:折半排序(本身蕴含着分治思想),以例题为例 ↓ ↓ ↓ ,建立如下算法:

首先设计节点对数据进行存储,struct node 包含以下信息: a i , b i , c i , c n t , r e s a_i,b_i,c_i,cnt,res ai,bi,ci,cnt,res分别代表三个属性值,以及重复节点计数(节点有可能重复),和 f ( i ) f(i) f(i)

然后对读入的节点按照三个属性作为三个关键字进行排序,小在前,大在后,然后对重复节点进行预处理,此时我们便开始进行CDQ分治

假设我们要处理的区间为 [ l , r ] [l,r] [l,r],我们设 m i d = ( l + r ) / 2 mid = (l+r)/2 mid=(l+r)/2
先分治处理 [ l , m i d ] [ m i d + 1 , r ] [l,mid] [mid+1,r] [l,mid][mid+1,r],分段处理完以后它有着这样的性质:

i ∈ [ l , m i d ] , j ∈ [ m i d + 1 , r ] , n o d e [ i ] . a < n o d e [ j ] . a i\in[l,mid],j\in[mid+1,r],node[i].a<node[j].a i[l,mid],j[mid+1,r],node[i].a<node[j].a i , j ∈ [ l , m i d ] o r   i , j ∈ [ m i d + 1 , r ] , i f   i < j : n o d e [ i ] . b < n o d e [ j ] b i,j\in[l,mid]or\space i,j\in[mid+1,r],if\space i<j:node[i].b<node[j]b i,j[l,mid]or i,j[mid+1,r],if i<j:node[i].b<node[j]b
且已更新完 [ l , m i d ] [l,mid] [l,mid] [ m i d + 1 , r ] [mid+1,r] [mid+1,r]对区间内部 n o d e [ i ] . r e s node[i].res node[i].res的贡献,
f ( i ) f(i) f(i)受分治区间的贡献更新

然后我们更新 [ l , m i d ] [l,mid] [l,mid]内节点对 [ m i d + 1 , r ] [mid+1,r] [mid+1,r]内节点 f ( i ) f(i) f(i)的贡献更新
[ m i d + 1 , r ] [mid+1,r] [mid+1,r]内的节点的a属性值大于 [ l , m i d ] [l,mid] [l,mid]内的,因此不会产生贡献)

再后我们寻找这样的节点: i ∈ [ l , m i d ] , j ∈ [ m i d + 1 , r ] , n o d e [ i ] . b < n o d e [ j ] . b & n o d e [ i ] . c < n o d e [ j ] . c i\in[l,mid],j\in[mid+1,r],node[i].b<node[j].b\&node[i].c<node[j].c i[l,mid],j[mid+1,r],node[i].b<node[j].b&node[i].c<node[j].c
其实我们是边找边进行折半排序的,这样就保证了 i f   i < j : n o d e [ i ] . b < n o d e [ j ] . b if \space i<j:node[i].b<node[j].b if i<j:node[i].b<node[j].b

折半排序过程中,当我们处理到当前节点为 p ∈ [ l , m i d ] p\in[l,mid] p[l,mid] q ∈ [ m i d + 1 , r ] q\in[mid+1,r] q[mid+1,r] 时,那么它们是 b 属性比所有已处理完的节点都要大,

那么假如 n o d e [ p ] . b < n o d e [ q ] . b node[p].b<node[q].b node[p].b<node[q].b的话,那么将 p p p插入当前新序列,而由于 p ∈ [ l , m i d ] p\in[l,mid] p[l,mid]会产生贡献,那我们也将它插入树状数组中,并且不会 被贡献,也就不用 q u e r y query query

反之,假如 n o d e [ p ] . b > n o d e [ q ] . b node[p].b>node[q].b node[p].b>node[q].b的话,那么将 q q q插入当前新序列,而由于 q ∈ [ m i d + 1 , r ] q\in[mid+1,r] q[mid+1,r]不会产生贡献,但会被已有贡献更新 f ( i ) f(i) f(i),那我们就在树状数组中 q u e r y query query它所受的贡献

如此下去,折半排序结束后,节点的 b 属性排序成功, a 属性混乱,但没影响,清空树状数组,至此,我们便完成了整个CDQ分治过程

一、例题 p3810


题目链接:洛谷p3810

二、代码及思路

1.思路

CDQ分治的核心模型,套模型即可

2.代码

代码如下:

#include <algorithm>
#include <iostream>
using namespace std;
const int maxn = 2e5 + 10;
struct node {
  int x, y, z;
  int cnt, res;
} a[maxn], b[maxn];
int n, m, bit[maxn], ans[maxn], cnt;
bool cmp(node a, node b) {
  if (a.x != b.x) return a.x < b.x;
  if (a.y != b.y) return a.y < b.y;
  return a.z < b.z;
}
int lowbit(int x) { return x & -x; }
void add(int x, int val) {
  for (int i = x; i <= m; i += lowbit(i)) bit[i] += val;
}
int query(int x) {
  int res = 0;
  for (int i = x; i; i -= lowbit(i)) res += bit[i];
  return res;
}
void cdq(int l, int r) {
  if (l == r) return;
  int mid = (l + r) >> 1;
  cdq(l, mid);      // 此时 [l,r] 已按照 y 升序排列
  cdq(mid + 1, r);  // [l,mid], [mid+1,r] 对 a[i].res 的贡献更新完毕
  int p = l, q = mid + 1, tot = l;
  while (p <= mid && q <= r) {  // 归并排序
    if (a[p].y <= a[q].y)       // 此时仍有 a[p].x <= a[q].x
      add(a[p].z, a[p].cnt), b[tot++] = a[p++];  // 将满足要求的z加入树状数组
    else
      a[q].res += query(a[q].z), b[tot++] = a[q++];  // 利用树状数组累加z贡献
  }
  while (p <= mid) add(a[p].z, a[p].cnt), b[tot++] = a[p++];
  while (q <= r) a[q].res += query(a[q].z), b[tot++] = a[q++];  // 归并完毕
  for (int i = l; i <= mid; i++) add(a[i].z, -a[i].cnt);  // 清空树状数组
  for (int i = l; i <= r; i++)
    a[i] = b[i];  // 此时 [l,r] 已按照 y 升序排列,x 序已被破坏
}
signed main() {
  //   freopen("in.txt", "r", stdin);
  //   freopen("out.txt", "w", stdout);
  scanf("%d%d", &n, &m);
  for (int i = 1; i <= n; i++)
    scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].z), a[i].cnt = 1;
  sort(a + 1, a + n + 1, cmp);
  cnt = 1;
  for (int i = 2; i <= n; i++) {
    if (a[i].x == a[cnt].x && a[i].y == a[cnt].y && a[i].z == a[cnt].z)
      a[cnt].cnt++;
    else
      a[++cnt] = a[i];
  }
  cdq(1, cnt);  // a[i].res = \sum_{j=1}^{i-1} [a[j] is satisfied]
  for (int i = 1; i <= cnt; i++) ans[a[i].res + a[i].cnt - 1] += a[i].cnt;
  for (int i = 0; i < n; i++) printf("%d\n", ans[i]);
  return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值