线段树上二分
有些二分答案的 check()
函数需要快速查询区间信息,例如区间最值、区间和,此时可以使用线段树维护。直接在线段树上进行二分答案的时间复杂度为 \(O(\log^2 n)\)。通过线段树上二分可以将单次查询的时间复杂度降低到 \(O(\log n)\)。
可 \(O(\log n)\) 解决的问题:
- 动态查询任意位置 \(p_0\) 左(右)侧第一个满足
check( SegT::query(p0, p) )
的位置 \(p\);
基本原理
注意到线段树正是一种形如二分的结构。
如果查询区间是 \([1,n]\),则可以直接利用线段树上每个节点保存的区间信息进行二分。例如,我们要查询 \([1,n]\) 范围内最靠左的满足 \(a_p\ge v_0\) 的位置 \(p\):
*样例中 \(n=8\),\(v_0=9\),答案 \(p=3\)。
但是如果查询区间不是 \([1,n]\),而是任意区间 \([l,r]\),我们就可以把 \([l,r]\) 拆成 \(O(\log n)\) 段线段树上的区间,按从左向右的顺序依次遍历所有区间,找到第一个 \(mx[p]\ge v_0\) 的区间:
这个区间可以看作是一棵完整的线段树,直接使用上文提到的方法二分即可。
更改处:
传入查询区间 int ql, int qr
:
添加剪枝,剪掉和查询区间无交的区间:
这样程序就只会递归 \([l_q,r_q]\) 所包含的区间(和它们的父区间)。同时因为递归的顺序是从左向右的,因此返回的答案一定是第一个满足条件的。
完整代码
例题
北京冬令营07B-C expand
题面
给定一个长为 \(n\) 的序列 \(a\)。你当前有一个区间 \([l,r]\) 和权值 \(v\),在此基础上你可以做以下操作:
- 若 \(l\ne 1\) 且 \(a_{l-1}\le v\),将 \(l\) 减去 \(1\);
- 若 \(r\ne n\) 且 \(a_{r+1}\le v\),将 \(r\) 加上 \(1\);
- 将 \(v\) 变为 \(\min\{a_{l-1},a_{r+1}\}\),这里规定 \(a_0=a_{n+1}=+\infty\);
定义 \(f(i)\) 为,假设初始区间为 \([i,i]\) 且 \(v=a_i\),将区间变为 \([1,n]\),最少需要做多少次第三种操作。
你需要处理 \(m\) 次以下两种操作:
- 给定 \(x\),交换 \(a_x, a_{x+1}\);
- 给定 \(l,r\),求出 \(\sum_{i=l}^{r}f(i)\);
观察题面容易发现:记集合 \(A\) 为区间 \([1,i]\) 的所有后缀的最大值组成的集合,\(B\) 为区间 \([i,n]\) 的所有前缀的最大值组成的集合,则 \(f(i)=|A\cup B|-1\)。
先考虑没有修改的情况如何求出所有 \(f(i)\)。注意到如果直接对每个元素取当前单调栈的深度,则无法去掉两侧重复的数字。
考虑每个元素 \(j\) 可以贡献到什么样的 \(f(i)\)。记 \(l_j\) 为 \(j\) 左侧第一个 \(a_l\ge a_j\) 的位置;\(r_j\) 为 \(j\) 右侧第一个 \(a_r\ge a_j\) 的位置(“第一个”均指离 \(j\) 最近的一个)。容易发现,\(j\) 可以贡献到 \((l_j,j)\) 和 \((j,r_j)\) 两个开区间的所有数字;而若 \(a_l=a_j\),则不应贡献到 \((l_j,j)\),因为 \((l_j,j)\) 已经被 \(l_j\) 统计过了。
注意到这样做是容易的,因为我们容易判断 \(a_l=a_j\) 的情况。只需要跑正反两遍单调栈即可。
在此我们额外介绍一种静态求 \(f(i)\) 的方法。注意到 \(f(i)\) 可由其左侧或右侧第一个 \(a_j\ge a_i\) 的 \(j\) 转移而来,即 \(f(i)=f(j)+[a_j\ne a_i]\),因此直接记忆化搜索即可。这种方法也适用于小规模快速单点查询 \(f(i)\)。
接下来考虑带修的情况。我们分讨 \(a_x\) 和 \(a_{x+1}\) 的大小关系。如果 \(a_x=a_{x+1}\) 则直接跳过;而 \(a_x>a_{x+1}\) 的情况其实和 \(a_x<a_{x+1}\) 的情况本质相同。考虑如何处理 \(a_x<a_{x+1}\)。我们依照上文单调栈的思路,先去除 \(a_x\) 在左侧的贡献,然后加入 \(a_x\) 在右侧产生的贡献。而这需要我们动态查询每个数的 \(l_i\) 和 \(r_i\)。这可以直接由上文的线段树上二分解决。
考虑如何处理 \(a_x\) 和 \(a_{x+1}\) 的 \(f\)。我们可以使用第二种求 \(f(i)\) 的思路,找到第一个比它大的 \(j\),然后从 \(j\) 转移到 \(i\)。分讨 \(j=l_x\) 或 \(j=x+1\) 的情况即可。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
|