解题报告-论对“卡常”的新理解
解题报告-论对“卡常”的新理解
当我们要用一个如能过的算法过一道题的时候,卡常就派上用场了。下面这道题,就是卡常的大部分精髓所在。
这道题的正解是线段树,但是很多人把它当作自己的莫队入门第一题。乍一看,\(n\le 10^6\),\(O(n\sqrt n)\) 是无论如何都不可能过去的,但为什么还是有许多人认为这道题是经典的莫队模板题呢?
原因很简单,它不只是莫队,它是莫队+大量卡常。
现在我们已经按照莫队的正常方式写完了这道题,先把序列分块,再按左端点所在块排序。经过一阵激动人心的转移,然后成功地 \(\texttt{Time Limit Excceed}\) 了。由于莫队算法本身不带递归,也一般不会有死循环的情况,其最坏复杂度又是 \(O(n\sqrt{n})\),题目时间限制 \(2\) 秒,得出结论不是算法本身的错。
那么只能是我们常数太大了。我们说复杂度是 \(O(n\sqrt{n})\),是因为其更精确的复杂度是 \(O(n\sqrt{n})\times O(1)\),而后面这个 \(O(1)\),可能是 \(1\),可能是 \(100\),是 \(10^5\) 也说不定。这个时候就要启动我们的卡常了。在这道题中,这是我分数的变化历程:
-
快读,\(40\) 分。
-
\(\texttt{fread}\),\(52\) 分。这说明快读真的啥也不是,\(\texttt{fread}\) 才是真理。
-
快写,\(52\) 分。根据以前几次的实践,得出结论:快写真的啥也不是。
-
有一点我是没想到的:我用两个变量 \(l\) 和 \(r\) 分别存下了第 \(k\) 次询问的左端点和右端点,但是当我直接用 \(\texttt{q[k].l}\) 和 \(\texttt{q[k].r}\) 之后,反而变成 \(60\) 了。
-
给 \(a\) 数组里面每一个数按照其出现的顺序重新赋值。我真没想到加了这个以后直接 \(60\rightarrow 100\) 了。然而——这是有科学依据的。由于莫队转移是一步一步的转移的,其每步必然访问的是一个连续的区间,而一个连续区间内的数第一次出现的次序是差不多的,所以这使我们访问 \(cnt\) 数组常数大大减小。
什么意思呢?举个例子:我们要莫队处理 \(\{1,10000,1,10000\}\) 这个序列,如果我们直接依次直接访问 \(cnt\{1,10000,1,10000\}\) 是很慢的,反之,如果我们访问 \(cnt\{1,2,1,2\}\) 就要快一点。
综上所述,不要再无意义卡常了,这道题真的是一个莫队好题+卡常好题。卡常技巧得到如下提升:
- \(\texttt{fread}\)。
- 快写啥也不是,赶紧丢掉,老实 \(\texttt{printf}\)。
- 尽量不要为了节省代码长度而做无意义的赋值。(代码长度限制很松,但是时间很死啊。)
- 减少无意义的排序。我的初始版本排序了两遍,有一遍大可不必。
- 数组访问的区间跨度尽量小。事实证明,这才是今天的重头戏。