原本准备讲题用的,结果讲急了没用上。
乱写的,就扔这儿吧。
先看一个题。
CF797E Array Queries
- 给定长度为 \(n\) 的序列 \(a\)。\(m\) 次询问。
- 每次询问给出 \(p,k\)。您要不断地执行操作 \(p\gets p+a_p+k\),直到 \(p>n\) 为止。询问的答案为操作次数。
- \(1\le n,q\le 10^5\),\(1\le a_i\le n\),\(1\le p,k\le n\)。
首先考虑每次询问直接做会发生什么。
如果 \(k\) 都很小的时候显然直接做是会撑到接近 \(O(n)\) 去世的。但是如果比较大的话,操作次数较少,倒还能撑。
那预处理一些信息呢?
一次 \(O(n)\) DP 可以求出 \(k\) 为一个定值的时候所有 \(p\) 的答案。但是 \(k\) 上限很大的时候显然不能做 \(k\) 次把所有情况都找出来。
于是 \(k\) 小的时候预处理是可行的,\(k\) 大的时候直接暴力跑是可行的。到底用哪个能多拿一点分呢。
小孩子才做选择,我全都要!
不如直接选定一个分割点 \(B\),\(k\le B\) 的时候使用预处理出来的结果直接输出,预处理复杂度 \(O(nB)\),\(k>B\) 的时候直接暴力跑一遍,最坏复杂度 \(O(q\frac{n}{B})\)。总复杂度 \(O(nB+q\frac{n}{B})\)。
最后只剩 \(B\) 应该选多少的问题了。显然如果要让两种做法的效率较为均衡的话,选 \(\sqrt{n}\) 是最优的。
于是喜提一个约为 \(O(n\sqrt{n})\) 的能过做法!
根号分治
顾名思义,对于一个数据范围为 \(n\) 的问题,如果在规模为 \(x\) 的情况下,你可以找出 \(O(x)\) 和 \(O(\frac{n}{x})\) (或带一点 \(\log\) 之类的抽象东西)的两种做法,但单独使用其中一种无法直接通过此题,这时你就可以考虑把它们组合起来,对于 \(x\) 的大小选择跑得更快的那一个方法。通常分割点选在 \(\sqrt{n}\),这样大概就可以稳定到一个 \(O(n\sqrt{n})\) 的级别。
对于其中某些问题可以片面地理解为把离线算法和在线算法混合起来。
大概当注意到题目里有“两个与时间复杂度相关的元素的值的乘积不大于某个值”之类的性质,就可以往根号分治考虑了。
思想很简单,但是每个题的具体实现都不一样,比较灵活,还是有点考验思维的。值得注意的是因为复杂度带个根号,通常这种题目时限都是卡满的,建议写的时候有意识地选择常数更小的写法。
看点没那么裸的题。
Topcoder SRM589 Level-3 FlippingBits
- 给定 \(n,m\) 和一个长度为 \(n\) 的 01 字符串。
- 你可以进行若干次操作。
- 每次操作可以将字符串里的一个位置取反,或者是将一段长度为 \(m\) 的倍数的前缀全部取反。
- 问最少需要多少次操作,才能使字符串的循环节为 \(m\)。
- 循环节长度为 \(m\) 定义为,对于任意 \(i\),如果 \(i+m\) 这个位置存在,那么两个位置上的字符相等。
- \(n \le 100\)。
感觉挺 \(O(n^3)\),但不像直接 DP 能搞的东西。
发现循环节个数和循环节长度的乘积并不超过 \(n\)。根号分治,启动!
若 \(m\le \sqrt{n}\),即循环节个数不超过 \(17\),那么也只有 \(17\) 种前缀取反方式,枚举哪几种被用了(显然用两次和不用没有区别),然后跑一遍整个字符串看还要几次单独取反,次数加起来取最小值即可。
若 \(m>\sqrt{n}\),则循环节长度不超过 \(17\),则你可以直接枚举这个循环节长什么样,然后用最优方式构造出一种操作方法使得次数最小,也就是使后面的循环节等于目前枚举的循环节或者其反串。这个理论上可以 DP 每段循环节是枚举的原串还是反串解决,每次从原串变成反串或者反串变原串都将次数加一(多一次前缀取反)。
所以实际复杂度 \(O(n2^{\sqrt{n}})\)。
不知道什么题
- 给定一个 \(n\) 个点 \(m\) 条边的无向图,初始每个点都是黑色或白色。
- 有两种操作:
- 将一个点反色。
- 查询与某个点直接连边的点中有多少个黑点。
- \(n,m,q \le 10^5\)。
首先如果出边少的话暴力维护其它点的答案显然是可以直接过的。
但是出边多咋办呢?
这时需要想到,边数固定,那么既然出边数量都到了会让你爆炸的程度,这样的点还能有多少个呢?
所以这时不如以 \(B=\sqrt{m}\) 为界,出边小于 \(B\) 的点直接修改连向的点的答案,出边大于 \(B\) 就记有多少其他的点连向了这个点,然后仅记录这些点被翻的次数,查询其它小点时看有没有连到这样被翻过的大点。\(O(m\sqrt{m})\)。
所以根号分治有时候分的不只是数据范围,还可以是其它抽象的东西。
CF1039D You Are Given a Tree
- 有一棵 \(n\) 个节点的树。
- 其中一个简单路径的集合被称为 \(k\) 合法当且仅当:树的每个节点至多属于其中一条路径,且每条路径恰好包含 \(k\) 个点。
- 对于 \(k\in [1,n]\),求出 \(k\) 合法路径集合的最多路径。即:设 \(k\) 合法路径集合为 \(S\),求最大的 \(|S|\)。
- \(n \leq 10^5\)。7s。
首先考虑如果 \(k\) 是定值,能不能快速求出答案。一眼感觉挺像树形 DP,实际上不完全是。
假设现在位于 \(p\) 点,则路径的选取有两种可能:选一个儿子接到 \(p\) 上形成一条长度加 \(1\) 的链,继续往上接;选两个儿子和 \(p\) 接起来形成一条长度为两个儿子下面接的链的长度加上 \(1\) 的链,上面重新开一条新链。
如果是第一种情况的话显然取子树里那条最长的链往上连是最优的,更容易出现长度足够的路径。
考虑什么时候用第二种情况。设它的儿子所在的最长、次长链分别为 \(a,b\)。首先直接取 \(a,b\) 来配是最容易出现长度达到 \(k\) 的新路径的。然后猜有这么个结论:如果 \(a,b\) 配起来长度达到 \(k\),那么先把 \(a,b\) 放到一起配成新路径是肯定不会比单独取 \(a\) 往上连的结果要劣的。当然如果配不上就不得不选第一种情况了。
发现大概是可以证明的。设最长、次长链分别为 \(a,b\)。如果把 \(a\) 继续往上连也找不到长度达到 \(k\) 的方式,那么你显然亏了。如果 \(a\) 往上连,它自身长度达到了 \(k\) 或是与另一条链组合达到了 \(k\),即存在一种让这条链达到 \(k\) 的方式,那么如果你在这里把 \(a,b\) 连起来,重开一条链往上走,还是有可能存在另一种使这条链长度达到 \(k\) 的方式,这样你依然亏了。当然也可能确实重开之后就没有办法达到 \(k\) 了,不幸损失一条路径,但是同时下面 \(a,b\) 又连成了一条新路径,这样也仅仅是不亏不赚而已,没有什么影响。
不咋严谨,感性理解。
于是我们获得了一个 \(O(n^2)\) 的过不了做法。
发现 \(k\) 与答案的乘积不会超过 \(n\),启动根号分治。
当 \(k\le \sqrt{n}\) 的时候,直接按上面方法贪心预处理求出每一个点在每一个 \(k\) 下的答案即可。
当 \(k>\sqrt{n}\) 的时候,答案不会超过 \(\sqrt{n}\)。考虑反过来枚举答案,求有哪些 \(k\) 是这个答案。意识到答案随 \(k\) 的增大而减小,于是答案相等的 \(k\) 一定在一段区间内,二分跑 \(\log n\) 次树形 DP 求出每个答案第一次被取到的 \(k\) 即可。
于是我们获得了一个 \(O(n\sqrt{n}\log n)\) 的理论能过做法。
然而实践发现被卡常了,所以有几个小优化:
- 可以把 dfs 序和每个点的祖先预处理出来再按顺序跑,除掉递归的常数。到这里 6s。
- 每次二分的时候并不需要对整个 \([1,n]\) 都找一遍。如果从小到大枚举答案的话,你已经确定了前面答案所在的区间,位于整个 \([1,N]\) 的尾部,那把二分右端点放在上一个答案的前一位跑就够了。到这里 3s。
- 注意到按 \(\sqrt{n}\) 分割的话两部分做法时间复杂度并不统一,理论上取 \(\sqrt{n\log n}\) 才是最优的。到这里 2s。