CDQ分治 从树状数组问题到Mokia

\(CDQ\) 分治再续

这一次是给出两个常见的问题,由于某种原因,我们可以使用 \(CDQ\) 有效的解决这一系列的问题。

\(I.\) 一维 \ 树状数组 \(1\) 问题

题目大意 : 给出一个序列 (\(N\)个数字) ,操作包括单点修改,区间查询。

再次提一下 \(CDQ\) 分治的主要思想:

  • 求左边区间的贡献
  • 求右边区间的贡献
  • 求左半边对右半边的贡献

在求偏序的时候,以上三点的顺序是可以打乱的。不过在这道题目中,必须遵循一定的顺序,需要注意。

在我们使用树状数组或者线段树解决这道问题的时候,基于的是前缀和和分治的思想,这里稍微有点不同。我们想,一个修改必定会影响它后面的查询,当我们把原数组也视作修改的时候,
整个就变成了哪个修改会影响哪个查询的问题。

再想一下 \(CDQ\) 分治的思想,我们可不可以求左半边对右半边的贡献呢? 先假设一个区间 \(l,r\),假如 \(l\)\(mid\) 的操作都是在 \(mid+1\)\(r\) 前面做的(也就是左半边所有的时间都小于右半边的时间),那么 \(l\)\(mid\) 的修改必定会影响到 \(mid+1\)\(r\) 的查询。

而我们把所有的查询 \(L,R\) 的值变为 \(1,R\) 的值减去 \(1,L-1\) 的值,那么所有的查询的起点都是 \(1\)。这个时候我们判断是否有贡献的时候,我们就拿右端点 (修改的只有一个点,也视其为右端点) 来判断,修改的右端点小于查询的右端点,那么肯定是对查询有贡献的。(黑色的是加的,红色的是减去的,剩下的黄色就是目标区间)

那是左半边所有的修改都会影响到右半边所有的贡献吗? 并不是。

(注意,\(time\) 为操作的时间,\(point\) 为右端点的位置,\(mode\) 为查询 (\(Q\)) 或者修改 (\(C\))。以下的 \(n\) 为操作的总数,包括把查询拆分和原序列)

我们对左半边和右半边的区间的右端点进行排序以后,左半边的时间还是要小于右半边最低的时间。我们可以很容易想到, \(1\) 号 的 右端点是 \(1\),它对右半边的所有查询都会产生贡献。而 \(2\) 号的右端点是 \(3\)右半边,它只能对
右半边的 \(2,3,4\) 号有贡献。也就是说,我们只需要每一次比较 \(left\) 是否小于 \(right\) (其中 \(left\)\(1\)\(mid\),\(right\)\(mid+1\)\(r\)),如果小于,那么当前的 \(left\) 号会对 \(right\)\(r\) 都有贡献,
所以 \(inc(sum,val[left])\)。如果不是,我们就直接计算 \(1\)\(left-1\)\(right\) 的贡献,就是 \(inc(ans[id[right]],sum)\)

上述的方法是要求左半边和右半边的右端点有序,且左半边的最大时间小于等于右半边的最小时间的。我们可以采用先 \(CDQ(l,mid)\ CDQ(mid+1,right)\) 的方法,每一次算完当前的贡献,就排好序,这样子就可以保证啦。既然是 \(CDQ\),那么可以直接归并排序,常数小。

然后解释一下为什么 \(CDQ\) 要分治。对于一个点 \(i\),它经过 \(\log\ n\) 层的分治以后,必然会与每一个右区间算贡献。一开始右区间有 \(\frac{n}{2}\) 个数字,然后变为 \(\frac{n}{4}\ \frac{n}{8}.etc\)。时间复杂度涉及到递归,这里作出大致的推测 : \(\log\ n\) 层,每一层的总和是 \(n\),那就是 \(n \log\ n\)。实际上应该是 \(O(f(n) \log\ n)\),我表示并不会主定理。

最后注意一下时间相同,先修改,再查询(虽然不知道为什么时间会相同)。

function judge(i,j:longint):boolean;
begin
	if place[i]<>place[j] then // place 是右端点
	begin
		if place[i]<place[j] then exit(True);
		exit(False);
	end;
	if mode[i]<mode[j] then exit(True); // 查询的 mode 是 2,修改的 mode 是 1
	exit(False);
end;

procedure CDQ(l,r:longint);
var
	left,right,i,mid:longint; // left 是左半边开始第一个节点,right 是右半边第一个节点,两者都用于归并排序
	sum:int64; // '现在'左半边的贡献
begin
	if l=r then exit;
	mid:=(l+r) >> 1;
	CDQ(l,mid); // 先往下
	CDQ(mid+1,r);
	sum:=0; left:=l; right:=mid+1;
	for i:=l to r do
		if (left<right)and(judge(left,right)) or (right>r) then // 在左边
		begin
			if mode[left]=1 then inc(sum[id[left]]); // 修改的 id 是修改的值,而查询的 id 是此查询是第几个查询
			copy[1,i]:=mode[left]; copy[2,i]:=place[left]; copy[3,i]:=id[left]; inc(left); // 归并排序
		end
		else
		begin
			if mode[right]=2 then dec(ans[id[right]],sum);
			if mode[right]=3 then inc(ans[id[right]],sum);
			copy[1,i]:=mode[right]; copy[2,i]:=place[right]; copy[3,i]:=id[right]; inc(right);
		end;
	for i:=l to r do
	begin mode[i]:=copy[1,i]; place[i]:=copy[2,i]; id[i]:=copy[3,i]; end;
end;

\(II.\) 二维 \ \(Mokia\)

题目大意 : 给出一个空的二维矩阵 (\(N\)\(N\) 列),操作包括单点修改,矩阵查询。

这是一个比较简单的二维树状数组的题目,至少在看到 \(N\) 的数据范围之前是怎么认为的。

数据范围 : \(N \leq 2 \times 10^6\)。而树套树 (我指的是树状数组或者线段树) 怎么也只能 \(N \leq 10^4\)

这就展现了 \(CDQ\) 比较好的以一面 哪个修改会影响哪个查询,就不用考虑 \(N\) 的大小,不用考虑模拟矩阵。

第一步,我们把查询看做是一个二维前缀和的查询。也就是说,我们对于 \(x,y,x1,y1\) 的查询,看做全部以 \(1\) 开始的矩阵,如下图 : (黑色的是加的,红色的是减去的,剩下的黄色就是目标矩阵)

现在问题从原来的一个右端点变成了两个右端点,但是贡献还是相同的,且有一个。假如两个点 \(x_i,y_i\),\(x_i\) 已经满足 \(\leq x_j\),那么我们就只需要看 \(y_i\)\(y_j\) 怎么样。这时候我们用一个树状数组(权域)来维护一下,
每一次左半边有修改的时候更新一下树状数组,每当右半边有点可以满足当前的左半边的点的时候 (\(left,right\)),就统计右边的点的第二个右端点 (\(right_y\)) 的前面有多少个左半边的点 (\(1_y~left_y\)) 的第二个右端点。

我们可以思考一下,如果我们先对所有的第一个右端点 (\(x\)) 进行了排序,那么每一次往下,我们只需要对时间进行归并排序即可,这样子跟上文的 从下往上,对右端点进行排序 是一样的。

procedure CDQ(l,r:longint);
var
	left,right,i,mid:longint; // left 是左半边开始第一个节点,right 是右半边第一个节点,两者都用于归并排序
begin
	if l=r then exit;
	mid:=(l+r) >> 1;
	left:=l; right:=mid+1;
	for i:=l to r do 
	begin
		if (time[i]<=mid)and(mode[i]=1) then // 计算左半边的产生的贡献
			Insert(point[2,i],value[i]);  // value[i] 是修改的值,Insert 是树状数组的 Add
		if (time[i]>mid)and(mode[i]=2) then // 计算右半边拥有的贡献
			inc(ans[id[i]],Query(point[2,i])*value[i]); // 查询的 value[i] 只有 1 和 -1,用来表示加还是减
		if (time[i]<=mid) then // 以下都是归并排序
		begin
			copy[1,left]:=time[i]; copy[2,left]:=point[1,i]; copy[3,left]:=point[2,i]; 
			copy[4,left]:=value[i]; copy[5,left]:=id[i]; copy[6,left]:=mode[i]; inc(left);
		end
		else
		begin
			copy[1,right]:=time[i]; copy[2,right]:=point[1,i]; copy[3,right]:=point[2,i]; 
			copy[4,right]:=value[i]; copy[5,right]:=id[i]; copy[6,right]:=mode[i]; inc(right);
		end;
	end;
	for i:=l to r do // 算完贡献以后按照 time 排序
	begin
		time[i]:=copy[1,i]; point[1,i]:=copy[2,i]; point[2,i]:=copy[3,i];
		value[i]:=copy[4,i]; id[i]:=copy[5,i]; mode[i]:=copy[6,i]; 
	end;
	CDQ(l,mid);
	CDQ(mid+1,r);
end;

最后总结 \(CDQ\) 分治需要注意的地方:

\(I.\ CDQ\)分治会按照各个参数的顺序而改变递归的顺序。

\(II.\ CDQ\)分治的时间复杂度并不是非常的好,常数巨大,但是可以做那些二维以上的题目,不用考虑 \(N\) 的大小。 (但是询问太多还是会炸)

$III.\ $有些人是直接 \(Sort\) 的,而不是归并排序,所以 \(CDQ\) 分治的三个步骤可以打乱。

posted @ 2018-09-22 21:50  _ARFA  阅读(248)  评论(0编辑  收藏  举报