「学习笔记」CDQ 分治

CDQ 分治

应用范围

  1. 解决与点对相关的问题
  2. 优化 1D/1D 动态规划的转移
  3. 将一些动态问题转化为静态问题

Part 1 解决点对相关问题

算法流程

  1. 找到序列 \([l,r]\) 的中点 \(mid\)
  2. 将位于序列中的所有点对 \((i,j)\) 进行分类:
    • \(l\le i\le mid\)\(l \le j\le mid\)
    • \(mid+1\le i\le r\)\(mid+1\le j\le r\)
    • \(l\le i\le mid\)\(mid+1\le j\le r\)
  3. 分别递归 \([l,mid]\)\([mid+1,r]\) 解决前两类点对。
  4. 用某些方法解决最后一类点对。

整个算法流程其实就是一种分而治之的思想,而第 4 部分的信息处理则是我们应该去设计的算法。

例题1

三维偏序

给定 \(n\)​ 个点 \((a_i,b_i)\)​,求满足 \(i< j,a_i < a_j,b_i < b_j\)​ 的点对 \((i,j)\)​ 数。

我们尝试解决第 4 部分的问题。发现由于 \(l\le i\le mid\)\(mid+1\le j\le r\),已经满足 \(i<j\) 的条件,只需再统计满足 \(a_i< a_j,b_i<b_j\) 的点对数即可,将问题转化为了二维偏序问题。只需将 \([l,mid]\)\([mid+1,r]\) 部分都按 \(a\) 的值从小到大排序,然后枚举 \(j\),将满足 \(a_i<a_j\)\(b_i\) 全部加入权值树状数组,然后统计 \(<b_j\) 的值的个数即可。

由于已经按 \(a\) 的值排序,在 \(j\) 逐渐增大的同时,\(i\)​ 必然也在逐渐增大。(可能不变)因此只需要双指针即可解决。由于当前序列 \([l,r]\) 的统计问题与其他序列无关,我们需要完成操作后清空树状数组。直接用 memset 显然是不现实的,我们可以直接在修改的地方重新改回来即可,即撤销操作。

因此解决第 4 部分问题的复杂度为 \(O(n\log n)\)​,总时间复杂度就为 \(O(n\log^2 n)\)

void CDQ(int l,int r)
{
	if(l==r)return;
	/*递归边界返回*/
	int mid=(l+r)>>1;
	/*找到序列中点mid*/
	CDQ(l,mid),CDQ(mid+1,r);
	/*递归解决前两类点对*/
	sort(P+l,P+mid+1);
	/*将[l,mid]间的点对按照 a 值排序*/
	sort(P+mid+1,P+r+1);
	/*将[mid+1,r]间的点对按照 a 值排序*/ 
	int i=l,j=mid+1;
	/*
	双指针:i为[l,mid]部分的,j为[mid+1,r]部分的 
	*/ 
	while(j<=r)
	{
		while(i<=mid&&P[i].a<P[j].a)
			T.add(P[i].b,1),i++;
		/*
		i 移动的前提是得在 [l,mid] 内,不能跑出去了
		如果满足 a_i<a_j 就加入 b_j,并将j向右移动 
		*/
		P[j].sum+=T.query(P[j].b-1),j++;
		/*
			树状数组统计的是<=的个数,
			因此 -1 统计的是< 的个数。
			注意要将当前指针 j 移动 
		*/
	}
	for(int d=l;d<i;d++)
		T.add(P[d].b,-1);
	/*
	i在符合条件时先加入再移动
	因此 [l,i-1]  即为修改的点 
	*/
}

例题2

陌上花开

给定 \(n\)​ 个元素,第 \(i\)​ 个元素的属性为 \((a_i,b_i,c_i)\)​,设 \(f(i)\)​ 表示满足 \(j\ne i,a_j \le a_i,b_j \le b_i,c_j\le c_i\)​ 的 \(j\)​​ 的个数。

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

题目链接

同样是偏序问题,上题是严格偏序,本题是非严格偏序。看似只需要简单转化:在主函数中先按 \(c\) 排序去掉一维,然后再套路地套上 CDQ 分治,将 P[i].a<P[j].a 更改为 P[i].a<=P[j].a,将查询从 T.query(P[j].b-1) 更改为 T.query(P[j].b)

其实不然。若存在点对 \((i,j)\) 满足 \(i\ne j,a_i=a_j,b_i=b_j,c_i=c_j\),在 CDQ 分治中由于排序被固定了左右顺序,只能由一者贡献给另一者,而事实上它们之间是相互贡献的。如何解决这一问题?

将序列去重即可。同时记录和去重后的元素 \(i\) 相等的元素数目 \(v\)(包括 \(i\)​),将 \(v\) 作为元素 \(i\) 的权值,修改时改为 T.add(P[i].b,P[i].v),撤销时也相应改为 T.add(P[i].b,-P[i].v) 即可。在最后统计答案时,再加上一个 \(v-1\),即相同元素间的贡献即可。

具体代码实现

例题3

动态逆序对

现在给出 \(1\sim n\)​ 的一个排列,按照某种顺序依次删除 \(m\)​ 个元素,你的任务是在每次删除一个元素之前统计整个序列的逆序对数。

题目链接

本题的在线算法:树套树。可以用树状数组套权值线段树,或者用分块套树状数组都可以实现。

现在我们来看看本题的离线算法。虽然没有强制离线这种玩意儿,但总还是要学的。

正序删除难以操作,怎么办?变为倒序插入呀!将删除时间 \(t\) 作为一维,统计满足 \(t_i\ge t_j\) 的逆序对数。CDQ 分治是以一个元素为根本,然后寻找满足条件的另一个元素。因此逆序对数需要由两部分构成:\(i<j\)\(v_i>v_j\) 或是 \(i>j\)\(v_i<v_j\)。我们需要做两次 CDQ 分治。在这里为了更加顺眼,我将每个元素的删除时间改为 \(m-t+1\),将不动的元素时间设置为 \(0\),这样就变为统计满足 \(t_i\le t_j\) 的逆序对数了。

这里就有两种写法了。一种是两次合为一次(因为大体框架相同),另一种是做两次。而做两次的时候有一个坑点需要注意:如果用 \(ans[t]\) 记录 \(t\) 时刻插入元素后新增的逆序对,那么初始的逆序对数 \(ans[0]\) 会被统计两次,因此需要除以 \(2\)。我们统计的是每一时刻的总逆序对数,因此需要累加。

具体代码实现

例题4

Radio stations

给定 \(n\)\(k\),以及 \(n\) 个元素,第 \(i\) 个元素的属性为 \((x_i,r_i,f_i)\)。求满足 \(i\ne j\)\(\min(r_i,r_j)\ge |x_i-x_j|\)\(|f_i-f_j|\le k\) 的点对 \((i,j)\) 数。注:\((i,j)\)\((j,i)\)​ 相同,不重复计数。数据保证不存在 \(i\ne j\)\(x_i=x_j\) 的点对。

题目链接

拆绝对值成为四维偏序?不不不,时间复杂度多个 \(\log\) 承受不起。换个方法,我们拆 \(\min\)

将元素按照 \(r\) 从大到小排序,这样就可以保证区间 \([mid+1,r]\) 内的元素 \(i\) 在统计时,对于每个 \([l,mid]\) 间的元素 \(j\) 都满足 \(\min(r_i,r_j)=r_i\)​,从而将原题的条件变为:

  1. \(r_i\le r_j\)
  2. \(r_i-x_i\le x_j\le r_i+x_i\)
  3. \(f_i-k\le f_j\le f_i+k\)

第一维,排序解决。

第二维,树状数组维护。

第三维,单边双指针实现。

看到这里或许有些读者会有问题:二、三两维是否可以交换实现方式?答案是否定的。

如果第二维用单边双指针维护,我们会发现难以维护:对于每个 \(i\)\(r_i-x_i\)\(r_i+x_i\)​​​ 的范围太大,移动起来复杂度太高。反观 \(1\le f_i\le 10^4,0\le k\le 10\),移动起来复杂度较小。因此我们用树状数组维护第二维,用单边双指针维护第三维。

具体代码实现

推荐习题

Part 2 优化 1D/1D 动态规划的转移

1D/1D 动态规划是指:状态一维,转移 \(O(n)\) 的一类 dp 问题。有的问题可以用单调队列、斜率进行优化,而还有的问题可以使用 cdq 分治来降低复杂度。

我们来尝试优化这个式子:\(dp_i= 1 + \max\limits_{j=1}^{i-1} dp_j[a_j<a_i][b_j<b_i]\)

直接转移,时间复杂度为 \(O(n^2)\)。我们尝试用 cdq 去优化转移过程。发现 \(dp_j\) 转移到 \(dp_i\) 也是一种点对间的关系,因此会有如下算法流程:

算法流程

  1. \(l=r\) 时,说明 \(dp_r\) 已经计算完毕,只需 \(+1\) 即可。
  2. 找到区间 \([l,mid]\) 的中点 \(mid\)
  3. 先递归计算 \(solve(l,mid)\)
  4. 处理 \(l\le j\le mid\)\(mid+1\le i\le r\) 一类点对的转移关系
  5. 再递归计算 \(solve(mid+1,r)\)

注意点:与偏序问题不同的是,点对间的关系处理需要放在两次递归操作之间。为什么?因为 \(dp\)​ 的转移时有序的、从前往后的。因此在 \([mid+1,r]\)\([l,mid]\) 进行转移时,首先要保证 \([l,mid]\) 间的 \(dp\) 都已经转移完毕。而在进行 \([mid+1,r]\)​ 内部的转移时,首先要保证之前的状态都已经转移完毕。因此点对间的关系处理需要放在两次递归操作之间。如果我们将 cdq 分治的递归树的递归过程拉出来看,会发现刚好是进行了中序遍历,因此是按照顺序处理了所有的 dp 值。

例题

拦截导弹

\(n\)​​ 个导弹,每个导弹有三个参数 \(t\)​​,\(a\)​​ 和 \(b\)​​。你需要求出一个最长的序列 \({s}\)​​,满足对于序列中所有的 \(i\)​​,均有 \(t_ i\le t_{i+1}\)​​ 且 \(a_i\ge a_{i+1}\)​​ 且 \(b_i\ge b_{i+1}\)​​。输出最长的序列的长度。由于可能有多种最长的序列的方案,每次随机选一种,你需要求出对于每个导弹,其成为最长序列中的一项的概率。

数据范围:\(n\le 5\times 10^4\)​,\(1\le a_i,b_i\le 10^9\)​,\(1\le t_i\le n\)​。

第一问:二维最长不升子序列长度。第二问:每个位置在方案中出现的概率。(即出现的方案数除以总方案数)

第一问正序枚举 \(i,j\) 进行转移求得。设 \(f1_i\) 表示以 \(i\) 为结尾的二维最长不升子序列的长度,\(g1_i\) 表示以 \(i\) 为结尾的长度等于 \(f1_i\) 的子序列数。有如下转移:

\(f1_i= 1 + \max\limits_{j=1}^{i-1} f1_j[a_j\ge a_i][b_j\ge b_i]\)

\(g1_i=\sum\limits_{j=1}^{i-1} g1_j[a_j\ge a_i][b_j\ge b_i][f1_i=f1_j+1]\)

则长度为 \(ans=\max\limits_{i=1}^n f1_i\),总方案数为 \(sum=\sum\limits_{i=1}^n g1_i[f1_i=ans]\)​​。

第二问倒序枚举 \(i,j\) 进行转移求得。设 \(f2_i\) 表示以 \(i\) 为开头的二维最长不升子序列的长度,\(g2_i\) 表示以 \(i\) 为开头的长度等于 \(f2_i\) 的子序列数。有如下转移:

\(f2_i= 1 + \max\limits_{j=i+1}^{n} f2_j[a_j\ge a_i][b_j\ge b_i]\)

\(g2_i=\sum\limits_{j=i+1}^{n} g2_j[a_j\ge a_i][b_j\ge b_i][f2_i=f2_j+1]\)

当一个元素位于某个二维最长不升子序列中时,则必然满足 \(f1_i+f2_i-1=ans\)​。因此第二问的答案 \(P(i)=\dfrac{g1_i\times g2_i}{sum}[f1_i+f2_i-1=ans]\)

从转移方程来看,都可以用 cdq 分治统计其中的偏序关系来进行转移。如第一问中,先将每个元素按照位置排序,解决掉第一维。在点对间的统计中,我们对 \([l,mid]\)\([mid+1,r]\) 部分的元素分别按照第二维 \(a\) 从大到小排序,通过某种数据结构得到 \(\max f1_j\) 和对应方案 \(g1_j\)​​​。能单点修改,区间查询最大值及对应方案和的数据结构是什么?可以用线段树,但也可以用树状数组。重载加号运算符可以使代码的可读性更高。

第二问由于是从后往前统计,为了方便使用同一个 cdq 进行统计,我们将参数 \(t\) 变为 \(n-t+1\)\(a\)\(b\) 也取相反数,在离散化之后就可以轻松地使用同一个 cdq 了。

没那么好写,恶心死我了。

具体代码实现

推荐习题

Part 3 将一些动态问题转化为静态问题

尚未完工qwq。

参考资料

posted @ 2021-08-11 20:34  cyl06  阅读(179)  评论(2编辑  收藏  举报