解题报告-论对“卡常”的新理解

解题报告-论对“卡常”的新理解

当我们要用一个如能过的算法过一道题的时候,卡常就派上用场了。下面这道题,就是卡常的大部分精髓所在。

这道题的正解是线段树,但是很多人把它当作自己的莫队入门第一题。乍一看,\(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}\)
  • 尽量不要为了节省代码长度而做无意义的赋值。(代码长度限制很松,但是时间很死啊。)
  • 减少无意义的排序。我的初始版本排序了两遍,有一遍大可不必。
  • 数组访问的区间跨度尽量小。事实证明,这才是今天的重头戏。
posted @ 2024-12-03 19:54  KarmaticEnding  阅读(47)  评论(0)    收藏  举报