隐藏时间复杂度

前言

注意到我们做过的一些题,其解法类似暴力,但时间复杂度均摊下来是正确的。因为可以证明时间复杂度跑不满。

我把这一类题目的时间复杂度暂且称为【隐藏时间复杂度】。

这类题目的时间复杂度大多有带根号,这也是这类题目的一种特征。

种类和

给定一个 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\) 次操作:

  1. \(\forall i \in [l, r], a_i \leftarrow \lfloor\sqrt{a_i}\rfloor\)
  2. \(a_x \leftarrow k\)
  3. 输出 \(\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\) 次操作:

  1. \(\forall i \in [l, r], a_i \leftarrow a_i \bmod x\)
  2. \(a_x \leftarrow k\)
  3. 输出 \(\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\) 次操作:

  1. \(\forall i \in [l, r], a_i \leftarrow \min\{a_i,x\}\)
  2. 输出 \(\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\)

\[O(\sum\limits_u I_uO_u)\leq O(\sum\limits_u\min\{m,d_u^2\})\leq O(\sum\limits_u\sqrt{md_u^2})\leq O(\sqrt{m}(\sum\limits_u{du})) = O(m\sqrt m) \]

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)\)

这种套路见过不知多少遍,模拟赛上还是没能做出来(悲

posted on 2023-10-25 20:59  Evan_song  阅读(12)  评论(0)    收藏  举报