序列分块。设块长为 \(B\)。每块预处理出最大值。对于询问 \([l, r]\),答案就是整块最大值和散块最大值拼起来。答案显然是 \(O(n) \sim O(\dfrac{n}{B} + B)\)。这是普通分块。
我们预处理出每个散块的前缀最大值和后缀最大值。预处理线性。对于跨越两个块的询问就是 \(O(\dfrac{n}{B})\)。但是缺点是对于左右端点在一个块内的询问不好处理,复杂度为 \(O(B)\)。根号平衡后还是 \(O(n) \sim O(\sqrt n)\)。
我们开一个数组 \(m_{l, r}\),表示第 \(l\) 个块到第 \(r\) 个块中最大值的最大值。这个数组显然可以很容易的 \(O\left(\left(\dfrac{n}{B}\right)^2 \right)\) 求出。接下来我们预处理所有长度为 \(O(B)\) 的段的最大值。用单调队列也很容易做到 \(O(nB)\)。这样对于左右端点在同一个块内的询问,其长度一定小于 \(B\)。我们可以直接查表。对于跨越多个块的情况,可以查一下 \(m\) 数组。这样复杂度是 \(O\left(\left(\dfrac{n}{B}\right)^2 + nB + n\right) \sim O(1)\) 的。根号平衡一下发现当 \(B = n ^ {1 / 3}\) 的时候有最小值 \(O(n ^ {4 / 3}) \sim O(1)\)。
这似乎是根号算法的极限?
我们考虑 \(\log\) 算法。首先是众所周知的 ST 表。复杂度 \(O(n \log n) \sim O(1)\) ,全方位吊打分块TNT。
我们考虑优化这个算法。考虑分块。不妨还设块长为 \(B\)。散块还是求出前后缀的 \(\max\),整块之间做一下 ST 表。跨越多块的做法可以做到 \(O(\dfrac{n}{B} \log \dfrac{n}{B}) \sim O(1)\)。对于单块之间的询问我们沿袭上面分块的思路,直接预处理所有长度为 \(B\) 的区间的 \(\max\),这样就可以直接查表。所以复杂度就是 \(O(\dfrac{n}{B} \log \dfrac{n}{B} + nB) \sim O(1)\)。根号平衡一下发现当 \(B = \sqrt{\log n}\) 的时候就可以达到 \(O(n \sqrt{\log n}) \sim O(1)\)。这比传统意义上的 ST 表要快了。\(\color{red}{(1)}\)
考虑继续优化这个算法。我们发现瓶颈在处理左右端点在同一块内的答案。在每块内再做一遍 ST 表。这样就需要 \(O(\dfrac{n}{B} \times B \log B) = O(n \log B)\) 的时间。剩下的算法和原来一样。预处理就变成了 \(O(\dfrac{n}{B} \log \dfrac{n}{B} + n \log B)\) 了。我们让 \(B = \log n\) 的时候就已经做到了 \(O(n \log \log n) \sim O(1)\) 了。当然块长显然可以比这个优秀,但是我不会求了。\(\color{orange}{(2)}\)
这个做法不是很好。我认为做法 \(\color{red}{(1)}\) 虽然渐进复杂度更劣但是完全有实力吊打做法 \(\color{orange}{(2)}\)。