二进制报警器
ftiasch
·
·
算法·理论
- 到底是报警器还是警报器啊。
- 鬼街喜提最优解 () 所以写一下。
题目描述
有 n 个变量 x_1, x_2, \dots, x_n,一开始都是 0。现在要处理两种操作:
对于每个 \mathtt{watch} 操作,我们要找出是哪个 \mathtt{add} 操作让它响的。
假设每个集合 S 的大小都不超过 K,而且 K 比较小。
如果可以离线做,用"整体二分"可以 O(K \times Q \times \log Q) 解决。
解法
方法一:减半报警器
先介绍一个简单点的办法:减半报警器。
对于每个 \mathtt{watch}(\{s_1, s_2, \dots, s_k\}, \mathtt{th}),我们把它拆成 k 个小报警器:
\mathtt{watch}\left(\{s_i\}, \left\lfloor\frac{\mathtt{th}}{k}\right\rfloor\right)
如果这些小报警器都没响,那原来的报警器肯定也不会响。所以原来的报警器最多响 O(\log_{\frac{k}{k - 1}} \mathtt{th}) 次。
怎么实现: 每个变量都开个优先队列,存着跟它有关的所有小报警器。每次有报警器响的时候:
- 算一下真正的阈值是多少(因为可能有些变量加上去了但没让报警器响,阈值就不准了)。
- 再把新的小报警器加回去。
每次报警器响要 O(K \log Q) 时间。
假设 V 是所有阈值里面最大的那个,总时间是:
K\, \log Q \, \frac{\log V}{\log K - \log (K - 1)} < K^2 \, \log Q \log V
(因为 \log(k) - \log(k - 1) > \frac{1}{k})
方法二:二进制报警器
二进制报警器用两个技巧让速度更快。
基本想法
对于每个 \mathtt{watch}(\{s_1, s_2, \dots, s_k\}, \mathtt{th}),我们找最大的 t 使得 2^t \leq \left\lfloor\frac{\mathtt{th}}{k} \right\rfloor。换句话说,t 是最大的数使得 \left\lfloor \frac{\mathtt{th}}{2^t} \right\rfloor \geq k。
然后我们加 k 个小报警器:
\mathtt{watch'}\left(\{s_i\}, 2^t\right)
区别在于: \mathtt{watch'} 在变量和跨过 2^t 的倍数时就响,而不是增加 2^t 才响。比如,变量和从 7 变成 9,那么 \mathtt{watch'}(4) 会响,因为跨过了 8(8 是 4 的倍数)。
- **不会漏掉:** 如果 $\mathtt{watch}\left(\left\lfloor\frac{\mathtt{th}}{k}\right\rfloor\right)$ 响了,那么 $\mathtt{watch'}(2^t)$ 肯定也响了。这样就不会漏掉真正的报警。
- **不会响太多:** 如果 $\mathtt{watch'}(2^t)$ 响了 3 次,那么 $\mathtt{watch}\left(\left\lfloor\frac{\mathtt{th}}{k}\right\rfloor\right)$ 至少响了 1 次。因为前面说过 $\mathtt{watch}$ 最多响 $O(K \log V)$ 次,所以 $\mathtt{watch'}$ 也不会响太多次。
#### 优化 1:不用优先队列
用 $\mathtt{watch'}$ 的话,就不用优先队列了(省掉 $O(\log Q)$),因为只有 $O(\log V)$ 种不同的小报警器。
**有点慢但没关系:** $\mathtt{add}$ 操作从 $O(\log Q)$(用优先队列)变成 $O(\log V)$(遍历所有种类的小报警器),但总时间不变。
#### 优化 2:不用重算阈值
在减半报警器里,每次报警器响都要重新算阈值。但用二进制报警器时,如果当前的阈值 $\mathtt{th'}$ 满足 $\left\lfloor \frac{\mathtt{th'}}{2^t} \right\rfloor \geq k$,就不用算了。
因为如果有些变量加上去但没让报警器响,这些增量加起来最多是 $(k - 1) \times 2^t$,这种情况下原来的报警器肯定不会响。只有当 $t$ 变小的时候才需要重新算,这样又省了 $O(K)$ 时间。
#### 最终时间
把这些优化都用上,总时间是 $O(K \log Q \log V)$。