整体二分
前言
整体二分
整体二分是二分一种类型,其运用了分治思想来将二分拓展到多个询问。
整体二分更多是一种思想,而非特定算法,所以我们用题目的方式引入。
题目
给定一个 \(n\) 个数的数组 \(a\)。
\(q\) 次询问,每次求 \(a\) 中第 \(k_i\) 小的值。
显然,此题一个排序就结束了。
但是我们来使用整体二分解决,以演示整体二分的思路。
解法
对于单个询问,我们可以使用二分的方法在较长的时间内求得答案。
即二分答案 \(ans\),计算小于 \(ans\) 的数的个数,并将此与 \(k_i\) 对比。
但显然,对于多个询问,本就时间复杂度爆炸的二分时间复杂度已经消失了。
但是若模拟每个询问的二分过程,容易发现,对于一些二分过程中的 \(ans\),我们实际上重复计算了许多次小于 \(ans\) 的数的个数。
我们优化这个算法可以从此下手。
整体二分,就是整体一起二分。对于一个 \(ans\),\(a\) 中小于 \(ans\) 的数的个数记为 \(k_0\),我们可以拿所有需要判断的询问一起来判断。
对于每个需要的判断询问 \(j\),我们将 \(k_j\) 与 \(k_0\) 对比。最终将这些询问按照与 \(k_0\) 的大小分为两部分。
然后,我们继续对这两个询问分别二分。
大致过程如下:
solve(l, r, Q):
	if l == r:
		for i in Q:
			ans[i] = l
		end
	mid = (l + r) / 2
	k0 = a 中小于 mid 的数的个数。
	for i in Q:
		if k[j] <= k0:
			push j in Q1
		else:
			push j in Q2
	solve(l, mid, Q1)
	solve(mid + 1, r, Q2)
end
容易计算,这个算法的时间复杂度约为 \(O(q\log n\log v)\)(其中,\(v\) 为 \(a_i\) 的值域大小)。
题目
给定一个 \(n\) 个数的数组 \(a\)。
\(q\) 次询问,每次求 \(a\) 中区间 \([l_i,r_i]\) 内第 \(k_i\) 小的值。
解法
其实本题与上题差别不大,我们还是利用整体二分的思想,二分值域划分询问。
由于新增了区间 \([l_i, r_i]\) 的限制,我们无法对每个询问 \(i(1\leq i \leq q)\) 都直接使用相同的 \(k_0\),而是需要分别计算在区间 \([l_i, r_i]\) 中小于 \(ans\) 的数的个数。
我们需要用到数据结构,一般使用树状数组,此处使用单修区查。
我们可以枚举每个位置 \(i(1\leq i \leq n)\),若 \(a_i \leq ans\),则在树状数组位置 \(i\) 上加入值 \(1\),否则为值 \(0\)。
于是对于区间 \([l_i, r_i]\) 中小于 \(ans\) 的数个数的计算则变成区间查询。
但是注意,此处时间复杂度仍然较大,因为我们需要枚举每个 \(i(1\leq i\leq n)\)。
即使我们储存每个询问可能需要用到的 \(a_i\),在分治递归时下传,我们依旧需要所有 \(a_i(1\leq i \leq n, a_i\leq ans)\),会 \(TLE\)
所以说我们稍微更改算法。
对于我们当前同一处二分的所有询问集合,记为 \(Q\),当前二分的值域区间 \([l,r]\),设 \(L\) 为 \(l\) 在 \(a_i\) 中的排名,\(R\) 为 \(r\) 在 \(a_i\) 中的排名。我们有
此时在用 \(mid\) 划分询问时,我们要用到所有 \(a_i(1\leq i \leq n, a_i\leq mid)\)。为了减少时间复杂度,我们则需要减少使用到的 \(a_i\) 的个数。
注意到,上式可以转化为:
此时,\(k_i\) 相当于在值域区间 \([l, r]\) 内的 \(a_i\) 中排名为 \(k_i - L + 1\) 的数。
若再用 \(mid\) 划分询问,我们则只需要用到 \(a_i(1\leq i \leq n, l\leq a_i\leq mid)\),时间复杂度从 \(O(n)\) 降到了 \(O(\log n)\)
于是我们可以考虑对每个询问 \(i\),在二分途中更改 \(k_i\),使得答案在值域区间 \([l, r]\) 内的 \(a_i\) 中排名为 \(k_i\)。
实现很简单,我们在划分询问时,若询问 \(j\) 被分到右区间 \([mid + 1, r]\),我们则将 \(k_j\) 减取 \(k_0\)。因为在询问 \(j\) 的询问区间 \([l_j, r_j]\) 中,值域区间 $[l, r] 中,小于 \(mid + 1\) 这个值的 \(a_i\) 有 \(k_0\) 个。
过程如下:
solve(l, r, A, Q)://A 为 a[i] 属于 [l, r] 的下标 i 的集合
	if l == r:
		for i in Q:
			ans[i] = l
		end
	mid = (l + r) / 2
	for i in A:
		if a[i] <= mid:
			add(i, 1)//在第 i 个位置插入 i,使用数据结构维护
			push i in A1
		else:
			push i in A2
	for i in Q:
		k0 = query(l[j], r[j])//查询 l[j], r[j] 之间大于等 l 小于等于 mid 的数的个数。
		if k[j] <= k0:
			push j in Q1
		else:
			k[j] = k[j] - k0//修改排名
			push j in Q2
	solve(l, mid, A1, Q1)
	solve(mid + 1, r, A2, Q2)
end
时间复杂度约为 \(O(q\log n\log v)\)(其中,\(v\) 为 \(a_i\) 的值域大小)。
题目
给定一个 \(n\) 个数的数组 \(a\)。
\(q\) 次操作,第 \(i\) 次:
- 若 \(op_i = 0\),求 \(a\) 中区间 \([l_i,r_i]\) 内第 \(k_i\) 小的值。
 - 若 \(op_i = 1\),将 \(a\) 中第 \(x_i\) 个数,即 \(a_{x_i}\) 修改为 \(k_i\)。
 
解法
我们需要支持修改操作。
实际上,我们划分询问的操作并没有将操作时间先后顺序打乱。我们把修改操作也当作一次询问,丢到询问数组中,我们直接从上往下遍历操作(修改的操作直接修改,询问的操作直接回答),得到的就是正确答案。
现在的问题是,如何划分操作。我们要把修改操作划分到 \([l, mid]\)、\([mid + 1, r]\) 中哪个区间?
修改操作有关值域区间的只有原来的值与修改后的值,我们可以把修改操作分成两个操作。
插入和删除,这样,一个修改的操作只会影响一个值的个数,我们便可以将其划分到其修改的值所对应的区间。
想明白这点之后,就会发现这题与前一题写法差不多。
solve(l, r, OP)://OP 操作集合
	if l == r:
		for i in OP:
			ans[i] = l
		end
	mid = (l + r) / 2
	for i in OP:
		if op[i] == 1://修改操作
			if a[i] <= mid:
				update//相对应的在数据结构中修改
				push i in OP1
			else:
				push i in OP2
		else://询问操作
			k0 = query(l[j], r[j])//查询
			if k[j] <= k0:
				push j in OP1
			else:
				k[j] = k[j] - k0//修改排名
				push j in OP2
	solve(l, mid, OP1)
	solve(mid + 1, r, OP2)
end
时间复杂度依旧。
总结
至此,我们将整体二分在带修区间第k小题目中讲解完了。
整体二分应用不止于此,但主要解决可二分的多次询问类题目。
主要思想就是将多个询问一起二分,每次划分成两组询问,再分别二分。
                    
                
                
            
        
浙公网安备 33010602011771号