快速操作序列$Ⅱ$:给出一个有$n$个元素的数组$A_1,A_2,..,,A_n$,设计一个数据结构支持以下两种操作:
- $Set(L,R,v)$:把$A_L,A_{L+1}, ..., A_R$的值全部修改成$v$($v geq 0$)
- $Query(L,R)$:计算子序列$A_L,A_{L+1},...,A_R$的最小值
不难想到把$set$操作进行分解,记录在结点中,但很快就会发现一个问题,add操作的时间顺序不会影响结果,但set会。比如先执行$add(1,4,1)$再执行$add(2,3,2)$等价于先执行$add(2,3,2)$再执行$add(1,4,1)$,但先执行$set(1,4,1)$再执行$set(2,3,2)$却不等价于先执行$set()2,3,2($再执行$set(1,4,1)$。怎么办呢?
简而言之,就是维护标记,包括标记下推和标记更新。当执行$update$操作时经过带标记结点时,需要将标记传推到左右子结点,同时消除自身的标记。$set$操作的标记更新很简单,直接用新的$setv$覆盖原来的$setv$。
值得注意的是,与之前的$add$操作相比,代码中多了两处$maintain$的调用。这是因为只要标记下传,该子树的附加信息就必须重新计算,但是我们只进入其中一个子树,该子树在递归访问结束后自然会调用$maintain$,因此还需要针对不进行递归访问的子树调用$maintain$。
1 void pushdown(int o) 2 { 3 if(setv[o] >= 0) //有标记才传递,假设set的值非负 4 { 5 setv[2*o] = setv[2*o+1] = setv[o]; 6 setv[o] = -1; //清除本结点标记 7 } 8 } 9 10 void maintain(int o, int L, int R) 11 { 12 if(setv[o] >= 0) minv[o] = setv[o]; 13 else 14 { 15 if(L == R) minv[o] = a[L]; 16 else minv[o] = min(minv[2*o], minv[2*o+1]); 17 } 18 } 19 20 int cl,cr,v; 21 void update(int o, int L, int R) 22 { 23 int M= L + (R - L) / 2; 24 if(cl <= L && R <=cr) setv[o] = v; 25 else 26 { 27 pushdown(o); //进入左右子树前先下推标记 28 if(cl <= M) update(2*o, L, M); else maintain(2*o, L, M); 29 if(cr > M) update(2*o+1, M + 1, R); else maintain(2*o+1, M+1, R); 30 } 31 maintain(o, L, R); 32 }
查询操作跟之前类似,不同的是,当两个add操作存在祖先-后代关系时是进行累加,而当两个set操作存在祖先-后代关系时,以祖先结点为准即可(为什么?祖先结点的set肯定是后出现的)。
1 int ql, qr; 2 int query(int o, int L, int R) 3 { 4 int M = L + (R-L)/2; 5 if(setv[o] >= 0) return setv[o]; 6 if(ql <= L && R <= qr) return minv[o]; 7 else 8 { 9 int ans = INF; 10 if(ql <= M) ans = min(ans, query(2*o, L, M)); 11 if(qr > M) ans = min(ans, query(2*o+1, M+1, R)); 12 return ans; 13 } 14 }