线段树(一):点修改

    动态范围最小值问题。给出一个有$n$个元素的数组$A_1, A_2, ..., A_n$,你的任务是设计一个数据结构,支持以下两种操作:

  • $Update(x, v)$:把$A_x$修改为$v$
  • $Query(L, R)$:计算$min \{ A_L, A_{L+1},...,A_R \} $

    如果还是使用$Sparse-Table$算法,每次$Update$操作都需要重新计算$d$数组,时间无法承受。为了解决这个问题,这里介绍一种灵活的数据结构:线段树(segment tree).

    在查询时,我们从根节点开始自顶向下找到待查询线段的左边界和右边界,则“夹在中间”的所有叶子结点不重复不遗漏地覆盖了整个待查询线段。从图中不发现,树的左右各有一条“主线”,虽然各自分叉,但每层最多只有两个结点向下延伸,因此“查询边界”结点个数不超过$2h$个,其中$h$是线段树的最大层编号。这实际上是把待查询线段分成了不超过$2h$个不相交线段的并。在后文中,凡是遇到这样的区间分解,就把分解得到的各个区间叫做边界区间,因为它们对应于分解过程的递归边界。

    如何更新线段树呢?显然需要更新线段$[i, \ i]$对应的结点,然后还需要更新它的所有祖先结点。不发现,其他的值并没有改变。

    以下是代码。这里的$o$是当前结点编号,$L$和$R$是当前结点的左右端点。查询时,全局变量$ql$和$qr$分别代表查询区间的左右端点;修改时,全局变量$p$和$v$分别代表修改点位置和修改后的值。

 1 const int INF = 0x3f3f3f3f;
 2 const int maxn = 100000 + 10;
 3 int minv[maxn << 2];
 4 int n, a[maxn];
 5 
 6 void build(int o, int L, int R)
 7 {
 8     int M = L + (R-L) / 2;
 9     if(L == R)  minv[o] = a[L];
10     else
11     {
12         build(2*o, L, M);
13         build(2*o+1, M+1, R);
14         minv[o] = min(minv[2*o], minv[2*o+1]);
15     }
16 }
17 
18 int ql, qr;    //查询[ql, qr]中的最小值
19 int query(int o,int L,int R)
20 {
21     int M = L + (R - L) / 2;
22     int ans = INF;
23     if(ql <= L && R <= qr)  return minv[o];
24     if(ql <= M)  ans = min(ans, query(2*o, L, M));
25     if(qr > M)  ans = min(ans, query(2*o+1, M+1, R));
26     return ans;
27 }
28 
29 int P, v;  //修改A[p]=v
30 void update(int o, int L, int R)
31 {
32     int M = L + (R-L)/2;
33     if(L == R)  minv[o] = v;  //更新叶子结点
34     else
35     {
36         if(P <= M)  update(2*o, L ,M);
37         else  update(2*o+1, M+1, R);
38         minv[o] = min(minv[2*o], minv[2*o+1]);  //更新非叶子结点
39     }
40 }

    最后叙述一下建树过程。一种方法是每读入一个元素$x$后执行修改操作$A[i]=x$,则时间复杂度为$O(nlogn)$。其实只需要事先设置好每个结点的值,自底向上递推即可(也可以写成递归)。每个结点仅计算了一次,因此为时间复杂度$O(n)$。

 

posted @ 2019-06-27 00:27  Rogn  阅读(459)  评论(2编辑  收藏  举报