【SJTUOJ笔记】P1122 二哥开房间

https://acm.sjtu.edu.cn/OnlineJudge/problem/1122
Hint已经告诉我们,这是一道线段树题。其实,此题确实展示了线段树的基本功能之一:维护01序列中区间最大连续0的长度。
最普通的线段树结点中有lr两个成员,表示该结点对应区间 [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对应的操作是把lsumrsumsum全部置0;而lazy=2对应的操作是吧lsumrsumsum全部置为区间长度。具体细节不再赘述。

你可能感兴趣的:(算法与数据结构)