隐藏时间复杂度
前言
注意到我们做过的一些题,其解法类似暴力,但时间复杂度均摊下来是正确的。因为可以证明时间复杂度跑不满。
我把这一类题目的时间复杂度暂且称为【隐藏时间复杂度】。
这类题目的时间复杂度大多有带根号,这也是这类题目的一种特征。
种类和
给定一个 01 串 \(s\)。有 \(q\) 次询问,你需要回答:
- \(s\) 中有多少个长度为 \(x\) 的区间和为 \(k\)
\(1\leq n,q\leq10^5\), \(1\leq \sum x\leq 10^5\)
首先考虑一个暴力:
预处理前缀和 \(a_i = \sum_{j=1}^{i} s_j\)。
对每个询问,枚举答案区间左端点 \(l\),可以直接计算右端点 \(r = l + x - 1\)。从而计算出区间 \([l, r]\) 的和 \(sum = a_r - a_{l-1}\),判断是否等于 \(k\)。
时间复杂度为 \(O(qn)\)
如果尝试用线段树等方式优化,会发现行不通。
但是注意到不同长度的区间种类个数为 \(O(\sqrt{\sum x})\)。
我们可以离线,然后把 \(x\) 相同的询问一起计算出答案,这样也是 \(O(n)\) 的。
由于不同 \(x\) 的区间小于 \(O(\sqrt{\sum x})\) 种,所以时间复杂度实际为 \(O(n\sqrt{\sum x})\)
势能线段树
给定一个长度为 \(n\) 的数组 \(a\),有 \(q\) 次操作:
- \(\forall i \in [l, r], a_i \leftarrow \lfloor\sqrt{a_i}\rfloor\)
- \(a_x \leftarrow k\)
- 输出 \(\sum_{i = l}^{r} a_i\)
\(1\leq n, q \leq 10^5, 1\leq a_i, k \leq 10^{12}\)
操作一不好用线段树维护。
但是稍微分析会发现,操作一只会让 \(a_i\) 减小,到什么时候停止?如果 \(a_i = 1\),那么操作不再有影响。
所以如果对一个值全为 \(1\) 的区间进行操作一,我们可以直接忽略这个操作。
那么我们再维护一个区间最大值。对于操作一时遍历到的一个线段树上的节点,如果其代表的区间最大值等于 1,我们不进行任何操作。否则,我们递归修改其两个节点,再更新此节点的信息。
时间复杂度为什么是对的?对于数组中一个数,他在不被操作二修改的情况下最多被操作一有效的修改 \(O(\log n)\) 次。
而操作二只能修改一个数,对于时间复杂度没有很大影响。
总体时间复杂度为 \(O(n\log n)\)
给定一个长度为 \(n\) 的数组 \(a\),有 \(q\) 次操作:
- \(\forall i \in [l, r], a_i \leftarrow a_i \bmod x\)
- \(a_x \leftarrow k\)
- 输出 \(\sum_{i = l}^{r} a_i\)
\(1\leq n, q \leq 10^6, 1\leq a_i, k \leq 10^{9}\)
与上题同理。
如果 \(x\) 取模了 \(a_i\),那么 \(a_i\) 的值只有两种可能:
不变,若 \(x > a_i\)。或者变成小于 \(\frac{a_i}{2}\),若 \(x \leq a_i\)。
那么我们继续维护区间最大值,每次取模操作对 \(x\) 进行判断。
给定一个长度为 \(n\) 的数组 \(a\),有 \(q\) 次操作:
- \(\forall i \in [l, r], a_i \leftarrow \min\{a_i,x\}\)。
- 输出 \(\sum_{i = l}^{r} a_i\)
\(1\leq n, q \leq 10^6, 1\leq a_i, x \leq 10^{9}\)
此题略微有些不同。
如果继续使用和之前相似的做法。即维护区间最大值,每次与最大值判断,然后暴力递归。
此时的复杂度不是 \(O(n\log n)\),而会被卡成 \(O(n^2)\)。
因为一次取 \(min\) 操作并不能保证 \(a_i\) 在 \(O(log n)\) 次内改变成 \(0\)。
如果每次操作一的 \(x\) 都为最大值减一,那么操作可以进行很多次,每次时间复杂度都为 \(O(n)\)。
我们需要换一种方法优化。
注意到区间取 min 这个操作会将不同的数变成相等的数。数的种类个数在减少。
当区间种类个数降到很低的时候,每一次修改修改两种及以上的可能就会降低。
所以我们维护最大值、严格次大值,以及最大值个数。
对于操作一,如果 \(x\) 大于最大值,不用修改。如果 \(x\) 介于最大值和次大值之间,我们可以根据最大值个数来 \(O(1)\) 修改此节点,并打上懒标记。只有 \(x\) 小于次大值时,我们才暴力递归更新。
这样,修改时区间种类个数只有没被操作区间完全覆盖的区间才可能加一。而暴力更新时种类个数一定有所减少。
种类增加的个数为 \(O(q \log n)\)。减少的个数为 \(O((n + q)\log n)\)
时间复杂度为 \(O((n+q)\log n)\)
三元环计数
给一张 \(n\) 个点,\(m\) 条边的图 \(G\),统计 \(G\) 中三元环的个数。
三元环:满足 \(u\) 到 \(v\) 之间有直接连边,\(v\) 到 \(w\) 之间有直接连边,\(u\) 到 \(w\) 之间有直接连边的无序三元组 \((u,v,w)\) 的个数。
$1\leq n,m \leq 10^5 $
正解十分巧妙,我就直接说了。
\(d_u\) 代表 \(u\) 的度数。
首先为每一条边定向:\(d_u\) 小的向 \(d_u\) 大的连边,相同时按照编号大小连边。
这张有向图一定无环。
考虑这样一个算法:枚举点 \(u\),对 \(u\) 所有 \((u, v)\) 的 \(v\) 打上标记。再枚举 \(u\) 的出边 \((u, v)\),枚举 \(v\) 的出边 \((v, w)\)。如果有边 \((u, w)\),即 \(w\) 有标记,则答案加一。
但实际上这个算法的时间复杂度为 \(O(m\sqrt m)\)。
可以证明。
先分析算法时间复杂度,记 \(u\) 出度为 \(O_u\),入度为 \(I_u\)。
考虑 \(v\) 的复杂度。\(v\) 会被每条入边访问一遍,访问一遍时需要访问每条出边。
所以时间复杂度:\(O(\sum\limits_u I_uO_u)\)
对于一个节点 \(u\),由于其只连向 \(d_v \geq d_u\) 的点 \(v\),其出度 \(O_u\) 一定满足 \(O_u \leq \frac{m}{d_u}\),即 \(O_ud_u \leq m\)。
所以有 \(O_uI_u\leq O_ud_u\leq m\)。
又有 \(O_uI_u\leq d_u^2\)
即
CF1119F
给定一颗 \(n\) 个点的树,树边有边权 \(w_i\)。
对于所有 \(k \in [1, n -1]\),求删去的边边权和最小的一组删边方案,满足剩余的图每个点度数小于 \(k\)。
\(1 \leq n \leq 10 ^ 5, 1\leq w_i \leq 10^9\)
若 \(k\) 固定,显然是树形 DP。
\(f_{u,0/1}\) 表示 \(u\) 子树内满足条件,\(u\) 到父亲的边断不断的最小值。
对于点 \(u\),取 \(f_{u,0}\) 和 \(f_{u,1}\) 差最大的前几个。
时间复杂度 \(O(n\log n)\)
但是现在需要对所有 \(k\) 求,如何优化?
发现对于一个点 \(u\),\(u\) 度数 \(d_u\) 如果小于 \(k\),则 \(u\) 不需要删边。我们可以直接忽略点 \(u\) 的 DP。
一个点 \(u\) 只有在 \(k \in [1, d_u - 1]\) 时才用 dp,时间复杂度则为 \(O(\sum\limits_u d_u\log n) = O(n\log n)\)
这种套路见过不知多少遍,模拟赛上还是没能做出来(悲
浙公网安备 33010602011771号