如果你被「左闭右开二分」、「左闭右闭二分」等刁钻问题纠结的很烦恼,不妨看一下这篇博客。希望这篇博客能让你再也不用纠结于这些刁钻问题。
引入
先来个瞎编的例题
- 交互。有一个长为 \(N\) 的
01字符串 \(S\),下标从 \(1\) 到 \(N\)(\(1 \leq N \leq 10^6\))。 - 你只知道 \(N\) 的值,而不知道 \(S\) 具体是什么样。
- 保证存在一个位置 \(p\)(\(1 \leq p < N\))使得,\(S_1\) 到 \(S_{p}\) 全为
0,\(S_{p+1}\) 到 \(S_N\) 全为1。 - 每次可以询问一个 \(S_i\) 的值。需要在 \(30\) 次操作内找到 \(p\) 是多少。
也就是说 \(S\) 一定形如 \(\mathtt{00111111}\) 这样,并且 \(S_1\) 必定为 0,\(S_N\) 必定为 1。
我们考虑如下步骤:
- 使用两个变量 \(l\)、\(r\)。\(l\) 初始为 \(1\),\(r\) 初始为 \(N\)。
- 当 \(l = r - 1\) 时,结束。否则进行下一步。
- 令变量 \(mid\) 为 \(\left \lfloor \dfrac{l + r}{2} \right \rfloor\)。若 \(S_{mid}\) 为
0,将 \(l\) 更新为 \(mid\)。否则(\(S_{mid}\) 为1),将 \(r\) 更新为 \(mid\)。 - 重新回到第二步。
举个具体例子。假设 \(N\) 为 \(12\),字符串 \(S\) 为 \(\mathtt{000011111111}\)。
我们用红色代表 \(l\) 指向的位置,蓝色代表 \(r\) 指向的位置:
此时\(l = 1\)、\(r = 12\)、\(mid = \left \lfloor \dfrac{1 + 12}{2} \right \rfloor = 6\)。
由于 \(S_{mid}\) 为 1,将 \(r\) 更新为 \(mid\):
此时\(l = 1\)、\(r = 6\)、\(mid = \left \lfloor \dfrac{1 + 6}{2} \right \rfloor = 3\)。
由于 \(S_{mid}\) 为 0,将 \(l\) 更新为 \(mid\):
此时\(l = 3\)、\(r = 6\)、\(mid = \left \lfloor \dfrac{3 + 6}{2} \right \rfloor = 4\)。
由于 \(S_{mid}\) 为 0,将 \(l\) 更新为 \(mid\):
此时\(l = 4\)、\(r = 6\)、\(mid = \left \lfloor \dfrac{4 + 6}{2} \right \rfloor = 5\)。
由于 \(S_{mid}\) 为 1,将 \(r\) 更新为 \(mid\):
此时 \(l = r - 1\),步骤结束。
之后我们来分析一下 \(l\) 和 \(r\) 的位置有什么意义。
在一开始的时候,\(l\) 指向 0,\(r\) 指向 1。
每次根据 \(mid\) 移动完 \(l\) 和 \(r\) 后,\(l\) 依旧指向 0,\(r\) 还是指向 1。
而当步骤结束的时候,\(l\) 和 \(r\) 刚好贴到一起了。这样一来,此时 \(l\) 所指向的,恰好是最后一个为 0 的位置,而 \(r\) 指向第一个 \(1\) 的位置。
而题目要我们求的,就是最后一个 0 的位置。这样一来,我们把最后得到的 \(l\) 拿出来即可。
进一步
如果上述题目当中,\(S\) 有可能全是 0 或者全是 1,应该怎么办?
我们可以修改一下上述算法。让 \(l\) 初始指向 \(0\),\(r\) 初始指向 \(N + 1\)。然后正常做即可。
如果全 0,结束之后 \(l = N\)、\(r = N + 1\)。
如果全 1,结束之后 \(l = 0\)、\(r = 1\)。
总之也是很符合直觉的。
你无需担心在过程中访问到 \(1 \sim N\) 以外的 \(S\) 的位置。
感性上理解,因为我们一开始就假定了 \(0\) 的位置一定是 0,\(N+1\) 的位置一定是 1,我们不需要再去 check 一遍它到底是不是。
或者从式子上看,由于每次取 \(mid\) 之前,有 \(l < r - 1\) 成立。因此有 \(l < mid < r\)。整个过程中 \(l\) 不会比 \(0\) 小,\(r\) 不会比 \(N+1\) 大,因此 \(mid\) 无法取到 \(0\) 或者是 \(N + 1\)。
总结一下思路
\(l\) 和 \(r\),两个指针,一个只指向 \(0\),一个只指向 \(1\)(这里 \(0\) 和 \(1\) 指的是,用来分界的条件是否成立),这样结束(也就是 \(l\) 和 \(r\) 贴贴)之后,不管是想要最后一个 \(0\) 还是第一个 \(1\) 都可以直接拿。十分无脑。
细节
- 注意两边到底是什么性质。别更新反了。
- 注意初始情况下 \(l\) 和 \(r\) 一定要指对地方。如果实际有效范围(比如上述题目里的 \([1, N]\))你无法保证端点(\(1\) 和 \(N\))的性质的话,就像上面的例题一样,额外往外推一下,相当于让 \(l\) 和 \(r\) 先初始指向人为设置的哨兵节点。
还是例题
之后简单再举个例子。
CF2132E Arithmetics Competition
题目大意:
- 给定两堆牌 A 和 B,每张牌都有权值。两堆牌的权值分别记作 \([a_1, a_2, \ldots, a_n]\) 与 \([b_1, b_2, \ldots, b_m]\)。\(1 \leq n,m \leq 2 \times 10^5\)
- \(q\) 次询问。\(q \leq 10^5\)。每次询问三个非负整数 \(x\)、\(y\)、\(z\),问,从牌堆 A 选出不超过 \(x\) 张牌,牌堆 B 选出不超过 \(y\) 张牌,总共不超过 \(z\) 张牌的情况下,权值之和最大可以是多少。\(0 \leq x \leq n\)、\(0 \leq y \leq m\)、\(0 \leq z \leq x + y\)。
朴素的贪心是,把两堆牌混合在一起,从大到小排序然后取。考虑如何优化这一过程。
观察一下发现,在上述过程中,在 \(x\)、\(y\)、\(z\) 有一个减为 \(0\) 之前,一定是遇到的牌都能选。在此之后,\(x\)、\(y\)、\(z\) 当中不管是哪个减为 \(0\),你都只能在至多一堆牌当中去选。
假定两堆牌合并后得到长度为 \(n + m\) 的数组。
我们先去二分一个 \(pos\),使得 \([1, pos]\) 的数可以全选,而 \([1, pos+1]\) 的无法全选。前缀和维护一下前缀有多少个 A 的牌,多少个 B 的牌即可。
- \(l\) 初始为 \(0\),\(r\) 初始为 \(z + 1\);
- 左边具有性质:\([1, mid]\) 当中的 A 牌数量 \(\leq x\) 且 B 牌数量 \(\leq y\);
- 右边性质则是它的反。
- 结束后取出 \(l\) 作为 \(pos\)。
之后,对于 \(pos+1\) 及以后的位置,之后如果还能选牌,那么要么只能选 A,要么只能选 B。以 A 为例。我们要找一个位置 \(end\),使得:\([pos+1, end]\) 当中的 A 可以全选,\([pos+1, end+1]\) 中的 A 没法全选。
- \(l\) 初始为 \(pos\),\(r\) 初始为 \(n + m + 1\);
- 左边具有性质:\([1, mid]\) 当中的 A 的数量 \(\leq min \{ x, z \}\);
- 右边性质则是它的反。
- 结束后取出 \(l\) 作为 \(end\)。
即可轻松简单的解决这个题。
代码:https://codeforces.com/contest/2132/submission/334851310