区间修改问题常用的三种手段,我觉得还是有必要复习一下。

树状数组 Link to heading

binary index tree 来自OI-wiki的图,我记得刘汝佳书里也有,不过那本书不在我手边 fenwick1

树状数组的核心是lowbit:x&(-x),其中-x为x的负数的补码表示形式,计算出来可以得到x的二进制从右往左的第一个1所对应的十进制数字。 这样就可以算出非叶子节点对应的管辖范围

单点修改,一路上山

int tree[MAXN];
inline void update(int i, int x)
{
    for (int pos = i; pos < MAXN; pos += lowbit(pos))
        tree[pos] += x;
}

求前n项和,一路下山

inline int query(int n)
{
    int ans = 0;
    for (int pos = n; pos; pos -= lowbit(pos))
        ans += tree[pos];
    return ans;
}

区间查询

inline int query(int a, int b)
{
    return query(b) - query(a - 1);
}

举个例子,查询前6个的和,lowbit(6) =2 ,所以S[6] = c[6]+c[4],这是比较自然的。 更新的时候,如果要更新6的话,需要更新c[6]和c[8]的值。 唯一的问题就是需要多加理解,二进制表示毕竟不够直观。但是原题目是很简单的。

RMQ Link to heading

Range Maximum query,区间查找算法。同样出现在刘汝佳的书里面。该算法的核心是二分法,就是将对一个区间的查找转变为对不断二分的子区间的查找,其中子区间长度均为2的倍数。

因为找不到书了,参考网上的代码

建立 Link to heading

假设有一个数组:1,2,6,8,4,3,7 设二维数组dp[i][j]表示从第i位开始连续2^j个数中的最小值。例如dp[2][1]表示第二位数开始连续两个数的最小值,也就是第二位数和第三位数的最小值,即2. 在求的时候可以将其分为两个部分,第一部分是$i$到$i+2^{j-1}-1$,第二部分是$i+2^{j-1}$到$i+2^{j}-1$。

显然初始值dp[i][0]为数本身,因此转移方程还是比较好求的 dp[i][j] = min(dp [i][j - 1], dp [i + (1 << j - 1)][j - 1]),也即第一部分和第二部分的最小值在求解。

代码

void rmq_init()
{
    for(int i=1;i<=N;i++)
        dp[i][0]=arr[i];//初始化
    for(int j=1;(1<<j)<=N;j++)
        for(int i=1;i+(1<<j)-1<=N;i++)
            dp[i][j]=min(dp[i][j-1],dp[i+(1<<j-1)][j-1]);
}

注意循环变量的顺序不可改变。这相当于就是不断增加区间的长度来更新所有的状态。另外第二部分是可以超过数组长度限制的,这不影响最终的结果。

查询 Link to heading

假设需要查询区间[l,r]的最小值,令$k=log2(r-l+1)$,区间最小值RMQ[l,r] = min(dp[l][k], dp[r - (1 « k) + 1][k]);,即从左边和右边同时开始搜索的最小值。 证明过程就略了,毕竟也不是搞算法的。这里刘汝佳算法竞赛训练指南那幅图比较直观,查询时其实就是从两端伸出了两个块,只要保证2^k大于等于区间的一半,则最小值一定是能算出来的。

线段树 Link to heading

参考资料 特点是能够在O(logN)的时间复杂度内实现单点修改、区间修改、区间查询(区间求和、求区间最大值、求区间最小值等) 求区间最值用线段树,我的理解是在修改操作较多的时候这样会更好一些。毕竟RMQ每次修改后都要更新一遍dp数组,速度其实也不快。

核心思想也是二分法,把大区间分成两个小区间管理,直到区间长度为1为止。区间索引按照完全二叉树,所以位置为x的节点其孩子分别为2x与2x+1

树建立参考 Link to heading

递归方法建立线段树,其中d数组保存着树节点,a数组保存着原始数据

void build(int s, int t, int p) {
  // 对 [s,t] 区间建立线段树,当前根的编号为 p
  if (s == t) {
    d[p] = a[s];
    return;
  }
  int m = (s + t) / 2;
  build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
  // 递归对左右区间建树
  d[p] = d[p * 2] + d[(p * 2) + 1];
}

区间查询 Link to heading

以求和为例子,查询区间[3,5]的和,可以用【3,3】+【4,5】来得到最终结果。 示例代码

int getsum(int l, int r, int s, int t, int p) {
  // [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p 为当前节点的编号
  if (l <= s && t <= r)
    return d[p];  // 当前区间为询问区间的子集时直接返回当前区间的和
  int m = (s + t) / 2, sum = 0;
  if (l <= m) sum += getsum(l, r, s, m, p * 2);
  // 如果左儿子代表的区间 [l,m] 与询问区间有交集,则递归查询左儿子
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  // 如果右儿子代表的区间 [m+1,r] 与询问区间有交集,则递归查询右儿子
  return sum;
}

区间维护 Link to heading

如果在修改的时候将所有区间内节点进行修改,代价将难以承受。线段树使用的是”懒惰标记“,即在父节点缓存下更改,这样在查询的时候可以直接累加上对应的修改值。

实例代码1 Link to heading

区间求和

void update(int l, int r, int c, int s, int t, int p) {
  // [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p
  // 为当前节点的编号
  if (l <= s && t <= r) {
    d[p] += (t - s + 1) * c, b[p] += c;
    return;
  }  // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
  int m = (s + t) / 2;
  if (b[p] && s != t) {
    // 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
    d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
    b[p * 2] += b[p], b[p * 2 + 1] += b[p];  // 将标记下传给子节点
    b[p] = 0;                                // 清空当前节点的标记
  }
  if (l <= m) update(l, r, c, s, m, p * 2);
  if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
  d[p] = d[p * 2] + d[p * 2 + 1];
}
int getsum(int l, int r, int s, int t, int p) {
  // [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p为当前节点的编号
  if (l <= s && t <= r) return d[p];
  // 当前区间为询问区间的子集时直接返回当前区间的和
  int m = (s + t) / 2;
  if (b[p]) {
    // 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
    d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m),
        b[p * 2] += b[p], b[p * 2 + 1] += b[p];  // 将标记下传给子节点
    b[p] = 0;                                    // 清空当前节点的标记
  }
  int sum = 0;
  if (l <= m) sum = getsum(l, r, s, m, p * 2);
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  return sum;
}

区间直接修改为某个值而不是加上某个值

void update(int l, int r, int c, int s, int t, int p) {
  if (l <= s && t <= r) {
    d[p] = (t - s + 1) * c, b[p] = c;
    return;
  }
  int m = (s + t) / 2;
  if (b[p]) {
    d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
          b[p * 2] = b[p * 2 + 1] = b[p];
    b[p] = 0;
  }
  if (l <= m) update(l, r, c, s, m, p * 2);
  if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
  d[p] = d[p * 2] + d[p * 2 + 1];
}
int getsum(int l, int r, int s, int t, int p) {
  if (l <= s && t <= r) return d[p];
  int m = (s + t) / 2;
  if (b[p]) {
    d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
          b[p * 2] = b[p * 2 + 1] = b[p];
    b[p] = 0;
  }
  int sum = 0;
  if (l <= m) sum = getsum(l, r, s, m, p * 2);
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  return sum;
}