线段树 整理笔记

p.s. 代码块缩进有问题,但不影响正常阅读,请忽略。

区间内最大连续权值和

  • P4513 小白逛公园

    区间询问 + 单点修改。

    对于询问区间内最大连续权值和,按照套路,维护 ti.lmaxti.rmax,注意在查询的时合并答案要分情况。具体地,看代码吧(实在描述不来):

    node query (int i, int l, int r){
       if (tree[i].l >= l and tree[i].r <= r) return tree[i];
       int f1 = 0, f2 = 0; node L, R;
       int mid = (tree[i].l + tree[i].r) >> 1;
       if (l <= mid) L = query (i * 2, l, r),f1 = 1;
       if (r > mid) R = query (i * 2 + 1, l, r), f2 = 1;
       if (f2 and f1){
           node m;
           m.maxl = max (L.maxl, L.sum + R.maxl);
           m.maxr = max (R.maxr, L.maxr + R.sum);
           m.ans = max (L.ans, max (R.ans, L.maxr + R.maxl));
           return m;
       }
       if (f1 == 1) return L; if (f2 == 1) return R;
    }
    

区间乘 + 区间加 + 区间查询

  • P2023 [AHOI2009] 维护序列

    模板。线段树乘法不过是多加了一个乘法懒标记,维护的时候复杂了些。

    inline void push_down(ll i){
       tree[i*2].sum=(ll)(tree[i].mul*tree[i*2].sum+((tree[i*2].r-tree[i*2].l+1)*tree[i].add)%mod)%mod;
       tree[i*2+1].sum=(ll)(tree[i].mul*tree[i*2+1].sum+((tree[i*2+1].r-tree[i*2+1].l+1)*tree[i].add)%mod)%mod;
       tree[i*2].mul=(ll)(tree[i*2].mul*tree[i].mul)%mod;
       tree[i*2+1].mul=(ll)(tree[i*2+1].mul*tree[i].mul)%mod;
       tree[i*2].add=(ll)(tree[i*2].add*tree[i].mul+tree[i].add)%mod;
       tree[i*2+1].add=(ll)(tree[i*2+1].add*tree[i].mul+tree[i].add)%mod;
       tree[i].mul=1;tree[i].add=0;
    }
    inline void add(ll i,ll l,ll r,ll k){
       if(tree[i].l>=l&&tree[i].r<=r){
           tree[i].add=(ll)(tree[i].add+k)%mod;
           tree[i].sum=(ll)(tree[i].sum+k*(tree[i].r-tree[i].l+1))%mod;
           return ;
       }push_down(i);
       ......(后续皆为基础操作)
    }
    inline void mult(ll i,ll l,ll r,ll k){
       if(tree[i].l>=l&&tree[i].r<=r){
           tree[i].mul=(tree[i].mul*k)%mod;
           tree[i].add=(tree[i].add*k)%mod;
           tree[i].sum=(tree[i].sum*k)%mod;
           return ;
       }push_down(i);
        ......(后续皆为基础操作)
    }
    

区间除法

  • [雅礼集训 2017 Day1]市场

    两个数在同时除以相同的大于 1 的数大约 log 次后,它们的差会小于等于 1。这就为我们将区间除转化为区间减做了保障。代码实现:

    inline void push_down (int i){
       if (t[i].tag){
           int lenl, lenr;
           lenl = t[i << 1].r - t[i << 1].l + 1;
           lenr = t[i << 1 | 1].r - t[i << 1 | 1].l + 1;//len
           t[i << 1].tag += t[i].tag;
           t[i << 1 | 1].tag += t[i].tag;
           t[i << 1].mx += t[i].tag;
           t[i << 1 | 1].mx += t[i].tag;
           t[i << 1].mn += t[i].tag;
           t[i << 1 | 1].mn += t[i].tag;
           t[i << 1].sum += 1ll * t[i].tag * lenl;
           t[i << 1 | 1].sum += 1ll * t[i].tag * lenr;
       }t[i].tag = 0;
       return;
    }
    inline void update_chu (int i, int l, int r, int k){
       if (t[i].l >= l and t[i].r <= r and t[i].mx - floor ((double)t[i].mx / k) == t[i].mn - floor ((double)t[i].mn / k)){
           int d = floor ((double)t[i].mx / k) - t[i].mx;
           t[i].tag += d;
           t[i].mn += d;
           t[i].mx += d;
           t[i].sum += d * (t[i].r - t[i].l + 1);
           return;
       }push_down (i);
       if (t[i << 1].r >= l) update_chu (i << 1, l, r, k);
       if (t[i << 1 | 1].l <= r) update_chu (i << 1 | 1, l, r, k);
       t[i].sum = t[i << 1].sum + t[i << 1 | 1].sum;
       t[i].mn = min (t[i << 1].mn, t[i << 1 | 1].mn);
       t[i].mx = max (t[i << 1].mx, t[i << 1 | 1].mx);
    }
    

区间排序

  • CF558E A Simple Task

    套路题。26 个字母较少,所以对于每个字母,开一个线段树,标记每一个字母出现的位置,若出现了该字母则该位置记为 1,否则记为 0。以升序为例。从 a 开始遍历 26 个字母,对于每个字母(此处以字母 a 为例),先统计它在该区间 [l,r] 内的出现次数 x。然后再进行区间修改操作——在这个字母的线段树内的 [l,r] 全部修改为 0,再将此线段树内的 [l,l+x1] 全部修改为 1。降序同理。最后再遍历所有线段树然后输出即可。附 code.

  • [ABC237G] Range Sort Query

    发现我们只需要注意值为 x 的数的位置即可。所以不妨简化问题,开两个线段树,其中:

    • x<i:在两棵树中,i 所在的位置为 1
    • x=i:第一棵树中,i 所在位置为 1;第二棵树中,为 0
    • x>i:在两棵树中,i 所在的位置为 0

    维护排序的话,仿照上一道例题使用区间修改的方式维护即可。最后寻找在两棵树中权值不一样的位置即为答案。


维护区间最小值次数 + 区间修改

  • P4898 [IOI2018] seats 排座位

    1

    如何判断节点集合 P 中的所有节点所组成的图形是否是一矩形?此处采用了一个染色的办法。假设我们现在将 P 中的节点全部染成黑色,将其余的节点全部染成白色。然后统计符合一下两个条件之一的点的总数:

    • 一个黑点,且它的左方和上方相邻的节点都是白色(或者为空)【条件一】;
    • 一个白点,且它的上下左右相邻的节点中有 2 个黑色的节点【条件二】。

    回过头来,看一个矩形中按上述方式能统计多少个点出来(顺便口胡证明):

    • 若一个黑点的左方和上方相邻的节点都是白色(或为空),则代表有一个连通块。显然,一个矩形中只有一个大连通块,若出现两个这样的黑色节点,那它肯定不是一个矩形。
    • 若有一个白点符合第二个条件,通过简单构造可以发现,不论黑色节点以什么方式出现在白点周围都无法在此基础上构造一个矩形。反过来说,当出现一个“L”形拐角时,拐角处的白点就符合第二个条件。综述,一个矩形不存在满足条件二的节点。

    所以,一个矩形的两者之和必定为 1。

    2

    有了 1 中的结论与判定方法,我们再看看和这道题有什么关联。

    发现我们可以将题目所求转化为:变量 i 从 1 遍历到 hw,而 i 可以理解为一个时间点,即第 i 个时刻。在每个时刻 i,我们会在之前的基础上将第 i 为参赛者的座位染成黑色。即在时刻 i,第 1 到 i 位参赛者的座位都会被染上黑色。同时,记 vali 表示时刻 i 时满足 1 中所述两个条件的总点数。

    由此可以得到一个重要性质:白点染色时间必定大于黑点染色时间。

    此时结合 1,就可以统计每个 vali 的值:(r(i) 表示第 i 位参赛者的座位所在的行数;c(i) 表示所在列数;i 既代表时刻,也代表这个时刻要染色的节点;numx,y 表示座位 (x,y) 的参赛者的编号。)

        #define rep(i, a, b) for(register int i = a; i <= b; ++i)
        rep(i, 1, s){ val[i] = val[i - 1];
            if(black(r(i), c(i)) > i) val[i] += 1;
            if(white(r(i), c(i)) < i) val[i] -= 1;
            rep(j, 0, 3) if(legal((xx = r(i) + fx[j][0]), (yy = c(i) + fx[j][1]))){
                if(black(xx, yy) == i and num(xx, yy) < i) val[i] -= 1;
                if(white(xx, yy) == i and num(xx, yy) > i) val[i] += 1;
            }
        }
    

    一点点来说,首先 black() 函数是返回 i 节点左方和上方相邻节点它们染色时间的最小值。若这个最小值都大于 i,那么代表,i 这个节点是符合 1 中的条件一的节点,所以 vali 加一。

    反之, white() 函数是返回 i 节点四方相邻节点它们染色时间的次(第二)小值。若这个次小值小于 i,那么代表,i 这个节点此前是一个符合条件二的白点,但是现在它染成黑色,也就不满足条件二了,所以 vali 减一。

    后面循环它的四方节点目的就是处理它是白点时对周围节点的影响。比如说,它曾经是其下方节点的 black() 值,当时刻小于 i 且大于等于 num(下方节点坐标),下方节点就是满足条件一的黑点。但是当节点 i 染成黑色之后,它(i 节点的下方节点)就不再是满足条件一的节点了,所以 vali 要减一。周围其他方向的节点同理。

    3

    然后再来谈谈交换座位。

    这其中也涉及到 1 中为什么要以那样的条件去检测所围图形是否为矩形——因为这样的判定方式不仅快和方便,而且交换时也简便容易。

    很简单,只需要把将要交换的两个节点及它们周围四联通的节点(即它们会影响到的节点)都存起来,并取消它们对 val 的影响,然后再交换那两个节点,再统计存起来的所有节点对 val 的影响即可。

    4

    综上,考虑我们要进行的操作。显然,我们要实时统计 val 中有多少个 vali 的值为 1,并且在进行交换座位操作时,还要对 val 中某一区间的值进行修改。

    一言以蔽之,即全局最小值个数 + 区间修改。

    那不就是线段树嘛。

    再提一下为什么可以将“统计 val 中有多少个 vali 的值为 1”转换为“全局最小值个数”。因为当只有第一个节点被染成黑色时,它必定是一个矩形,即 val1=1。所以直接统计全局最小值个数即可。

    注意将下标全部加一之后再统计操作。

    5 code.


动态开点线段树

动态开点空间复杂度一般是 O(nlogn+qlogn)(q 为更改次数)。

  • CF911G Mass Change Queries

    这是一道动态开点线段树的模板题。发现 ai 的范围比较小,所以可以对每个数都建立一棵线段树。动态开点,说白了就是只在需要的时候建立这个点,如果这个点所代表的区间为空,就不建这个点。那么对于修改,其实就是合并两个不同的线段树,注意特判当两个数相等时不用合并。合并方式见代码及注释:code.

posted @   pldzy  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示