块状数组是一个非常有趣的数据结构,利用分块的思想将再简单不过的数组化腐朽为神奇,sqrt(n)虽不及log(n),
但是性价比还是很好的。
但是需要注意的是块状数组一定要从0开始,这样可以求出i在哪个块中 pos = i / s , 其中s 为一个块的规模。
块状数组就是将数组以sqrt(size)为单位分割成块,对于每一块单独维护其相应的信息。
用这样的方法可以做很多线段树可以做的题目,例如A Simple Problem with Integers
http://poj.org/problem?id=3468
题意: 给一个数列,M个操作 add l r c 把l到r的每一个数加上c ; query l r 查询l到r的区间和
这个题可以用块状数组做,借鉴线段树的lazy-tag思想。
对于每一块维护其前缀和,并对每一块加一个lazy标记,add表示该块所有数都统一加了多少,初始化为0 。
对于每一个查询操作
<1>如果l和r位于同一个块中 ,那么 ans = b[r] -b[l-1] +add * (r-l+1) ( 如果l位于块的开始位置 那么这个公式就有问题了)
<2>如果l和r不位于同一块中, 对于中间的每一个块 ans+= b[y]+add*(y-x+1) , 对于两侧的块用1的方法解决
时间复杂度最好O(1),最坏O(sqrt(n)) .
对于每一个修改操作
<1>如果l和r位于同一个块中,直接修改.
<2>如果不位于同一块,对于中间完整的块修改add即可,不完整的块一次修改每一个数。
时间复杂度最坏也是sqrt(n)。
块状数组也可以解决区间第K值这样的问题 Dynamic Ranking
http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=2112
题意 : 给一个数列,m个操作 , c i t 将a[i]改为t , Q l r k 查询l..r区间的第k小数
同样是将数列分块,每一块均维护递增顺序,代码如下:
scanf("%d%d",&n,&m); for (int i=0; i<n; ++i) scanf("%d",a+i); memcpy(b,a,sizeof(a)); s = sqrt(n) , t= n /s ; if (n%s) ++t ; for (int i=0; i<t ; ++i) { int lb= i*s , rb = lb +s-1 ; if (rb>=n) rb=n-1; sort(b+lb, b+rb+1) ; }
对于每一个询问,采用二分答案的方式,对于每一个二分得到的值进行验证。
但是这里有一个问题,二分到的数值可能满足要求,但是原数列中未必有这个数。
例如数列 : 1 3 5 7 10
第5小的数值有 8 9 10 这三个数,但真正合法的是10
我们可以去寻找比答案大1的数,即最小的数p满足数列中有k个比p小的数
答案就是p-1。 二分的代码如下:
int query(int x, int y, int k) { int l= 0 ,r = 1000000020, mid ; while (l < r) { mid = l + r >> 1 ; int k1= countnum(x,y,mid); if (k1>=k) r = mid ; else l = mid+1 ; } return l-1; }
countnum函数 的原理就很简单了 ,代码
int countnum(int x ,int y, int z) { int x_pos = x /s , y_pos = y /s ,ret= 0 ; if (x_pos==y_pos) { for (int i=x ; i<=y; ++i) ret += a[i]<z ; } else { for (int i= x ; i<=x_pos*s +s -1 && i<n; ++i) ret+= a[i]<z ; for (int i=x_pos+1; i<=y_pos-1; ++i) ret+= getnum(i*s,i*s+s-1,z); for (int i=y_pos*s ; i<=y ;++i) ret+= a[i]<z ; } return ret ; }
getnum函数是统计第i个块中小于z的个数,因为块中的数是递增的,可以用log(sqrt(n))的时间
完成,整个countnum的时间复杂度是sqrt(n)