https://acm.sjtu.edu.cn/OnlineJudge/problem/1122
Hint已经告诉我们,这是一道线段树题。其实,此题确实展示了线段树的基本功能之一:维护01序列中区间最大连续0的长度。
最普通的线段树结点中有l
,r
两个成员,表示该结点对应区间
[l,r]
[
l
,
r
]
。添加不同的成员可以维护不同的属性。在本题中,我们要维护01序列中最大连续0的长度,那么需要添加:
lsum
,表示以l
为起点的连续0的最大长度;rsum
,表示以r
为终点的连续0的最大长度;sum
,表示区间 [l,r] [ l , r ] 中连续0的最大长度;lazy
,懒惰标记。lazy=0
表示无标记,lazy=1
表示区间被占用,lazy=2
表示区间被回收。
接下来考虑如何维护这几个成员。显然对于叶子节点,几个sum属性要么全为0,要么全为1。而对于非叶子节点,我们用pushUp()
函数来更新。
void pushUp(int x){ //x是要更新的结点编号
//置为初始值
a[x].lsum = a[x << 1].lsum; //左连续零等于左儿子的左连续零
a[x].rsum = a[x << 1 | 1].rsum; //右连续零等于右儿子的右连续零
int mid = (a[x].l + a[x].r) >> 1;
if (a[x << 1].lsum == mid - a[x].l + 1) //左儿子全是零,那么左连续零应该再加上右儿子的左连续零
a[x].lsum += a[x << 1 | 1].lsum;
if (a[x << 1 | 1].rsum == a[x].r - mid) //同理
a[x].rsum += a[x << 1].rsum;
a[x].sum = max(a[x << 1].rsum + a[x << 1 | 1].lsum, max(a[x << 1].sum, a[x << 1 | 1].sum));
//根据最大连续零出现的位置有三种可能
//以上三项依次为:跨左右儿子,仅在左儿子,仅在右儿子
//跨左右儿子的情况,必定是左儿子的右连续零加上右儿子的左连续零
}
然后是询问操作。根结点的sum
表示的是整个区间的最大连续零,先判断有没有可能分配。能分配,再从根开始往下二分,每次尽可能向左儿子移动。
int query(int x, int len){
if (a[x].l == a[x].r)
return a[x].l;
pushDown(x); //先下推lazy标记
int mid = (a[x].l + a[x].r) >> 1;
if (a[x << 1].sum >= len) //左儿子可以分配,必定向左走
return query(x << 1, len);
else if (a[x << 1].rsum + a[x << 1 | 1].lsum >= len) //中间可分配
return mid - a[x << 1].rsum + 1;
else //只能向右分配
return query(x << 1 | 1, len);
}
在分配和释放一段序列后,需要打上lazy
标记。lazy
标记的维护方法和其他种类的标记相同。lazy=1
对应的操作是把lsum
、rsum
和sum
全部置0;而lazy=2
对应的操作是吧lsum
、rsum
和sum
全部置为区间长度。具体细节不再赘述。