某些想法
该文主要记录一些OI题目的技巧或是思想。
1.转换遍历对象与改变求和顺序:
对于某些需要枚举序列元素来获取信息的题目,当n很大以至于O(n)也超时的时候,我们可以考虑枚举序列中数的值,把枚举对象转换为值域,枚举值域的贡献。
如Effects of Anti Pimples这题。
很显然有一个做法是枚举每个数的所有所在集合然后找出最大值来算,时间复杂度会爆炸。但实际上,对于该题可以通过直接枚举以a[i]为黑染色后面最远加入a[j]时集合的最大值来减少无效遍历,只需考虑一个数若在序列中是集合中的最小最大值,显然只有1个集合里有它,而后可以推出对于第i个最大值,显然有 2 ^ (i-1)个集合里其为最大值。只算它作为最大值的贡献,便可最多在O(nlogn)的时间复杂度内解决问题。
还如该题:

该题n,k<=3e5,ai <= 1e6
该题如果是按满足i - j >= k的序列下标进行遍历,然后判断是否是最大值需要O(n ^ 2)的时间,但或许可以换一个角度,我们提前预处理出来某个数或某个数的倍数在序列中最靠左的位置和最靠右的位置,然后枚举值域中的每个数,看两个预处理出来的位置之差是否大于等于k,我们只是转换了条件,因为当某个数对gcd最大的时候,显然是两者相等的情况,当然这种条件比较苛刻,它的倍数与它的gcd也是它本身,从大到小枚举这个数,第一个合法的一定是最大值。相当于我们先满足gcd(a[i],a[j])最大这个条件,再判断下标之差是否大于等于k。
我们再思考一下为什么第一种方法会O(n ^ 2),这是因为对于一个已经在序列中出现过的数,我们后面再枚举到的时候,其实就没必要向后枚举了,因为若前面那个数不满足条件,此时枚举的数在后面,更没有必要枚举。这是正着来的,我们再反着来一遍,便能得出所有合法情况。但此时依然不是很优,因为我们这样枚举依然会枚举到很多无效的数,它们对答案并无影响,我们只考虑对于一个数比它大且为它的倍数的数对它的影响即可。
好了,由第一种方法的局限性,我们考虑要提前预处理出来一个数前面与后面比它大且为它的倍数的数的位置,并且对于一个数我们只用处理一次,后面再出现重复的也没必要了。既然如此,为何不构造一个每个数只出现一次的序列呢,更简单一些,为什么不直接枚举这里面每个数的值呢。由此第二种方法自然推出。
有道同类型的题:
该题数据范围达到了1e6,算约数要用O(nlogn)的方法。
还有一个经典问题。
求树上所有点对的距离之和,当然,我们可以直接两个for,for循环,然后用lca算距离,O(n ^ 2 log n)当然可以解决,但我们可以考虑点对距离之和,实际上是由两点之间的边组成的,每条边的权值一定,该边做的贡献显然是将他切断后,所形成的两个区域的点数的乘积乘上边的权值,我们可以先dfs出每个节点的子树内的节点个数num[u],然后num[u] * (n - num[u]) * w即可,最后求和,即为所求。也不只是所有点对之间的距离,凡是边所能维护的某些权值均可。也不是非要所有点对,也可以存在一些特殊点,这时候,我们考虑dfs出一个子树内所有的特殊点个数,然后利用num[u] * (sum - num[u]) * w即可。
[CSP-S2019 江西] 和积和
该题的形式比较复杂,纯暴力显然是O(n ^ 4),用前缀和预处理的话能达到O(n ^ 2)。
这时候我们不妨考虑,该题所给式子的意义是什么。在[1,n]中的所有子区间中区间内b的和与a的和的乘积,我们的暴力相当于是为了先满足区间这个条件,然后算b和a对于该区间的贡献,不妨换一个角度。我们直接考虑b和a对于所有区间的贡献,即
将原式换成 ∑b[i] * ∑a[j] * q[i],即考虑包括b[i]与a[j]的区间个数有多少个。当i < j 时,显然此时区间的左端点有i个取值点(1 ~ i),区间的右端点有n - j + 1个取值点(j ~ n),那么此时对答案的贡献就是 ∑b[i] * i * ∑a[j] * (n - j + 1),i > j 时也易类比推出,得到这两个式子后,我们发现,∑a[j] * (n - j + 1) 与 ∑a[j] * j 都是可以预处理出来的,于是O(n)即可解决。这题我们一开始思考式子的实际含义,然后转换遍历对象,实际上是一种改变求和顺序的想法。
改变求和顺序也有一个经典的应用。
莫比乌斯反演
这个经典式子:
∑∑[gcd(i,j) == 1]
用莫比乌斯反演后可以转变为∑∑∑μ(d),其中第三个∑,d的条件是为gcd(i,j)的约数,此时可以抽象成一个表格,有n行m列,对于i行j列的这个格子,它的贡献是∑μ(d),d|gcd(i,j),那我们可以转换一下遍历对象,考虑对于一个d,它对多少个表格做了贡献,即它是多少个表格行数列数gcd的约数,那我们可以把原式改成这样 ∑μ(d)∑∑[ d | i 且 d | j ],这实际上是改变了求和顺序,而后面那两个∑,很显然是down[n/d]*down[m/d] (down为向下取整)。
由此我们多少可以看出,求和顺序的改变本质显现的是改变遍历对象,而改变遍历对象在数学式子上的体现便是求和顺序的改变。
2.分类讨论
分类讨论是一种常见的思维方法,分类体现了独立,讨论则是对于易解决的具体问题进行分析,是一种解决子问题后再解决最终问题的体现。
通常分类讨论的题目或许是某个参数的数据范围很小,最多不超过5,或是某个参数在某些数据范围区间内有特殊性质。这些都是我们可以考虑分类讨论的引子,部分分也往往是考场上能迅速引导到分类讨论思想的一个捷径。
例题:

数据范围1 <= |T| <= 3
发现T的范围较小,于是我们可以考虑一下,T=1,T=2,T=3,分别有那些情况,并且怎么解决。
T = 1:
显然此时无论S怎么操作,若包含T,则一定无解,否则无需翻转
T = 2:
此时有01,10,11,00这四种情况,考虑1与0对称,本质只有01与00两种情况,那么我们考虑01时,只需将串变为前面全为1,后面全为零就行,本质是求逆序对个数。00的情况,先判断1的数量够不够,不够的话肯定无解,够的话只需要在妹两个00之间插一个即可。
T = 3:
此时情况复杂了,有000,011,010,三种本质不同的情况,对于011,考虑枚举所有连续的11即可,对于010,只需枚举所有为010的串即可,因为一定可以通过合并1来使其不合法,对于000,把大于等于三的连续0段的权值定义为长度-2,一个孤立0的权值定义1,00的权值定义为零,然后加入堆进行匹配即可。
Joyboard
题目乍一看形式很简单,这么多mod,该不会是数论吧。但推了推,发现啥有用的都推不出来。也没有什么很小的数据范围,咋搞?很简单,我成尊不就是了。直接打份暴力进行观察,然后发现,当k=1时,显然等于1,k=2时若m > n 则为m/n + n - 1
否则为m,当k==3时若m > n 则为m - m/n - n + 1否则为0,当k>3时,一定为0。
不是很难,但当时考试的时候可是推了好久的数论公式,最后才想到或许可以分类讨论的。
有许多算法都有分类讨论的思想内核,其中最明显的便是分治,分治把问题分而治之的过程便是分类讨论,如根号分治中将大于mid与小于mid的情况进行讨论,点分治中将经过当前根节点的路径与不经过当前根节点的路径进行讨论等。
3.平衡复杂度

浙公网安备 33010602011771号