折半警报器/二进制警报器
首先给出一类问题:
我们需要支持两个操作。
- 单点加
- 给出一个大小不超过 \(k\) 的集合 \(S(x)\),当集合中的元素和第一次超过 \(v\) 时,报告这个 \(x\)。
折半警报器
注意到一个惊天地的事实,设 \(y = v-\sum_{i \in S(x)} a_i\)。若集合 \(S(x)\) 需要满足要求,一定存在一个位置 \(i\) 满足 \(i\in S(x)\), 且 \(\Delta a_i \geq \lceil \frac{y}{sz(S(x))}\rceil\),由抽屉原理可得。
那我们就有一个及其暴力的做法就是,你对于每个位置开一个堆存它对应的二操作有哪些,按照上述的 \(a_i + \lceil \frac{y}{sz(S(x))}\rceil\) 从小往大一个一个看,当 \(a'_i \geq\) 这个报警值时就更新所有这个集合对应的所有位置的报警值,直到最后满足要求。
分析一下复杂度,首先二操作复杂度 \(O(n k \log n)\)。
一操作复杂度对于一个二操作的集合,最多被报警 \(O(\log _{\frac{k+1}{k}} V)\) 次,对于一次报警的复杂度为 \(O(k \log n)\),每次要更新集合的报警值和堆,那么复杂度为 \(O(q k \log n \log_{\frac{k+1}{k}} V)\)。
其中 \(log_{\frac{k+1}{k}} V\) 几乎可以视作 \(k \log V\),那么总复杂度大约 \(O(q k^2 \log n \log V)\)。
二进制警报器
上面的做法过于慢了,这里有一种更加聪明的做法。
对于报警次数没有改变,但是对于每次报警的复杂度做了很大优化。
考虑换一种报警方式,我们对于每个操作二的集合 \(S(x)\),维护一个 \(h_x\),若集合中的值在操作时经过了 \(2^{h_x}\) 的倍数,我们就进行报警,对于这里的 \(h_x\),若可以在所有位置都不报警情况满足要求,则将 \(h_x\) 降低 \(1\)。
这里若 \(x\) 在操作 \(w\) 后经过了经过了 \(v\) 指 $v\in (a_x,a_x+w] $。
那么此时的 \(v_x-\sum_{i \in S(x)} a_i\) 为 \(O(k 2^{h_x})\) 量级,显然对于一个 \(h\) 只会操作 \(O(k)\) 次,因为一个位置被操作一次之后的每一次都至少增加 \(2^{h_x}\)。
那么一个操作只会报警 \(O(k \log V)\) 次,我们可以通过一些手法将每次更新复杂度达到均摊 \(O(k\log V)\)。
具体而言,我们对每个 \((位置,h)\) 开一个 vector,记 \(next(x,v)\) 表示 \(x\) 经过的下一个 \(2^v\) 的倍数是哪一个。对于每个二操作集合维护一个 \(rem_x\),表示 \(v-\sum_{i\in S(x)} a_i\) 减去集合中所有值到达未报警的最大限度后还剩下多少值。
那么对于一次单点操作,我们可以找到所有会报警的二进值位,然后我们把所有这些二进制位对应的操作二都找出来,并且更新这些操作二的 \(rem_x\)。
-
\(rem_x\leq 0\),则说明需要更新这个操作的 \(h_x\),直接重构更新 \(rem_x\)。
-
否则不需要,把这个 操作 \(x\) 放回
vector\((位置,h_x)\)。
分析一下这样做的复杂度若此时 \(rem_x \gt 0\) 那么则不要更新,直接取出来再放进去,这是 \(O(1)\) 的。
若要更新,则一个操作只会更新 \(log V\) 次,一次复杂度为 \(O(k)\) 暴力重构。
那么总复杂度为 \(O(n k \log V)\)。
下面做法比上面更新快的原因是。
- 我们不需要开个堆,因为每次我们可以遍历二进制位,直接找到所有会报警的位置。
- 我们每次不需要实时更新集合其他位置的报警值,因为这里报警值的定义只和 \(h_x\) 与 \(a_i\) 相关,而上面的与集合整体的值相关。
- 我们可以根据定义维护 \(rem_x\) 从而可以每次都在 \(h\) 变化时再重构。

浙公网安备 33010602011771号