动态范围最小值问题。给出一个有$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)$。