大家都知道在OI算法当中复杂度大都是O(N)、O(N^2)、O(NlogN)......比较常见,而O(sqrt(n))的复杂度并不常见,只有分块、朝鲜树(似乎并不常用但确实复杂度带根号)但是在某些意想不到的地方,可以使用根号算法,不仅效果不错,还比较好写。
引出我们今天的题目CODEVS 3550,已知序列a_1,a_2,...a_i...,a_n,可以进行单点修改,(输入格式为(0,x,y)三元组,意即a_x=a_x+y),可以进行极值查询(输入格式为(1,x,d)三元组,意即查询a_x,a_x+d,a_x+2*d...a_x+k*d中最大值,其中满足x+k*d<=n并且x+(k+1)*d>n(通俗一点来说就是,查询一个首项为x,公差为d的等差数列的数组下标集合所对应的元素极值)
我们所学过的数据结构大多支持连续的区间查询,这样零散的数据怎么办呢。我们可以发现,如果公差d比较小,我们可以对于每种公差d建立d棵线段树,分别保存首项为1,2,......,n-1,n公差为d的下标集合所对应的数;但这种方法在d较大时会导致空间和时间的浪费。如果公差d比较大,我们可以直接暴力枚举。于是很容易想到把两种方法结合起来。那么问题来了,d的分界线在哪呢。
设d的分界限为K,序列长度为N,操作数为K,可以算出时间复杂度为O(N*K*logN(建树)+M*logN(线段树查询)+M*N/K(暴力查询)+M*K*logN(线段树修改)),我们发现这是一个对勾函数,当K=sqrt(M*N/(M+N)*logN),考虑到N与M大致相同,简化为sqrt(M/logN)。这样算法的复杂度变为O(M*sqrt(M*logN))。根号便奇怪的出现在了时间复杂度当中。
在实际实现过程当中,由于线段树常数过大,K应取小一点。
code:(考虑到上面的K太难算了,我取了个常数15= =)
#include<iostream>
#include<cstdio>
#include<cstring>
#define mid (l+r)/2
#define lch t,i<<1