题解 P3373 一棵封装好的线段树

· · 题解

经典的线段树问题,大受\mathrm{OIer}的欢迎(话说到2018-11-6 17:02:24为止今天已经交了11页\mathfrak{QAQ}

基础的思想与前面的\mathcal{DALAO}们一样(可以选择跳过):

  为了避免暴力地去处理,我们利用lazy_tag来存储一段区间上的操作,当处理或访问该区间时下放标记,将区间的值处理为修改后的真实值。
  但是题目中有加、乘两种运算,我们在用两种标记分别存储两种操作的同时,还应处理好两种运算之间的关系——乘法的优先级高于加法,某次乘法操作的乘数k会影响到之前所有加上的值,因此要对区间的值和之前的加法操作标记都乘上k。
  在下放标记时,如果先处理加法标记再处理乘法,会导致加法标记被多乘一遍使答案偏大;我们应先下放乘法的标记使子区间原来的值和加法标记乘k,再从原区间的加法标记处理子区间的加法操作。
  对某几次操作的演示:
区间原值
--加法操作-> 区间原值+加法标记1*区间长度
--乘法操作-> (区间原值+加法标记1*区间长度)*乘法标记
--加法操作-> (区间原值+加法标记1*区间长度)*乘法标记+加法标记2*区间长度

在这些的基础上,我们可以进行一些特别的优化:

  1. 在区间的加法标记为0、乘法标记为1(即该区间没有进行过任何操作)时,可以不进行下放标记的操作;

  2. 当被操作数小于模数时,取模操作可以不进行,在此可以进行判断(但最后千万不要忘写取模操作!

  3. 我们把整棵线段树封装起来,将一些常用操作处理成专门的函数(可以使用\mathrm{inline}适当加快速度),更能提升代码的可读性,便于代码的修改。

这样,一篇别具风格的线段树代码就敲好啦~

\mathrm{Code:}
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#define PBCWZCC
using namespace std;
#define MAXN 100000
#define LL long long
#define DEBUG
#define MODIT
template<class T>inline void read(T&X)
{
    X=0;
    char symbol('\0'),ch(getchar());
    for(;ch<'0'||'9'<ch;(ch=='-')?(symbol='\1'):(1),ch=getchar());
    for(;'0'<=ch&&ch<='9';X=(X<<3)+(X<<1)+(ch^48),ch=getchar());
    (symbol)?(X=-X):(1); // 原谅我的鬼畜码风,
//在我的程序里没有while(),这里的展开只是为了清晰Q.Q 
} // 顺便有个快读板子?Q.Q 
int n,m;
LL Modit; // 模数 
namespace seg{ // 线段树所在的名空间 
    LL A[MAXN|3];
    class sgt{ // 整棵线段树 
      private:
        inline int fa(const int x) // 父亲(在这里没用) 
        {
            return x>>1;
        } 
        inline int lc(const int x) // 左孩子 
        {
            return x<<1;
        }
        inline int rc(const int x) // 右孩子 
        {
            return x<<1|1;
        }
      public:
        LL val[MAXN<<2|3], // 区间和 
            tag[MAXN<<2|3], // 加法标记 
            tag2[MAXN<<2|3]; // 乘法标记 
        inline void pushup(const int p)
        {
            val[p] = val[lc(p)] + val[rc(p)]; 
        // 首次/重新整合区间的信息
        // 在更新完左右区间后,整合到父区间 
        }
        void build(const int p,const int l,const int r)
        {
            tag2[p] = 1; 
        // 注意不能是 0 ,因为这个是乘法标记,表示
        // 这段区间没乘过任何数(或者乘过 1 ) 
            if(l==r)
            {
                tag[p] = 0; // 加法标记 
                val[p] = A[l];
            // 将单个元素的值赋到线段树的叶子结点 
                return ;
            }
            int mid((l+r)>>1);
            build(lc(p),l,mid);
            build(rc(p),mid+1,r);
            pushup(p); // 整合子区间信息 
        }
        inline void F1(const int p,const int l,const int r,const LL k)
        { // 指定区间的加法操作 
            tag[p] += k;
        // 在加法标记加上 k  
            (Modit<=tag[p])?(tag[p]%=Modit):(1);
        // 由于取模运算常数较大,这里特判是否需要取模 
            val[p] += k * (r-l+1);
        // 这段区间加上的 k 的个数等于区间中元素的个数 
            (Modit<=val[p])?(val[p]%=Modit):(1);
        }
        inline void F2(const int p,const int l,const int r,const LL k)
        { // 指定区间的乘法操作 
            tag2[p] *= k;
        // 乘法标记累乘 
            (Modit<=tag2[p])?(tag2[p]%=Modit):(1);
            tag[p] *= k; 
            (Modit<=tag[p])?(tag[p]%=Modit):(1);
            val[p] *= k;
            (Modit<=val[p])?(val[p]%=Modit):(1);
        // 区间的值和加法标记都要乘上 k 
// (区间原值+加法操作的值)*k -> 区间新值1*k -> 区间新值2 
        }
        inline void pushdown(const int p,const int l,const int r)
        {
            int mid((l+r)>>1);
            F2(lc(p),l,mid,tag2[p]);
            F2(rc(p),mid+1,r,tag2[p]);
// 应优先下放乘法标记,如果先下放加法标记会导致它被多乘一遍 
            F1(lc(p),l,mid,tag[p]);
            F1(rc(p),mid+1,r,tag[p]);
            tag2[p] = 1;
            tag[p] = 0;
        // 标记完成下放后,要记得还原 
        }
        void update1(const int x,const int y,const int p,const int l,const int r,const LL k)
        { // 加法操作全过程 
            if(x<=l&&r<=y)
            {
                F1(p,l,r,k);
        // 当前区间被包含在待修改区间里,直接处理这段区间对应的值 
                return ;
            }
            if(tag[p]||(tag2[p]^1)) pushdown(p,l,r);
        // 在标记非空的时候下放标记 
            int mid((l+r)>>1);
            if(x<=mid) update1(x,y,lc(p),l,mid,k);
            if(mid+1<=y) update1(x,y,rc(p),mid+1,r,k);
            pushup(p);
        // 处理完子区间,整合 
        }
        void update2(const int x,const int y,const int p,const int l,const int r,const LL k)
        { // 乘法操作全过程,基本与上面相同 
            if(x<=l&&r<=y)
            {
                F2(p,l,r,k);
                return ;
            }
            if(tag[p]||(tag2[p]^1)) pushdown(p,l,r);
            int mid((l+r)>>1);
            if(x<=mid) update2(x,y,lc(p),l,mid,k);
            if(mid+1<=y) update2(x,y,rc(p),mid+1,r,k);
            pushup(p);
        }
        LL query(const int x,const int y,const int p,const int l,const int r)
        { // 区间和的查询 
            if(x<=l&&r<=y)
            {
                return val[p];
        // 当前区间被包含在待查询区间里,直接返回这段区间对应的值
            }
            if(tag[p]||(tag2[p]^1)) pushdown(p,l,r);
            int mid((l+r)>>1);
            LL qq(0); // 待返回值 
            if(x<=mid) qq += query(x,y,lc(p),l,mid);
            if(mid+1<=y) qq += query(x,y,rc(p),mid+1,r);
            return qq%Modit; // 最后返回的时候记得取模(多数情况下这个 qq 会比模数大) 
        }
    };
    sgt T; // 一棵茂盛的(划)线段树 
}
using namespace seg;
int main()
{
    int i,t,x,y;
    LL z;
    read(n);
    read(m);
    read(Modit);
    for(i=1;i<=n;++i)
    {
        read(A[i]);
    }
    T.build(1,1,n);
    for(i=1;i<=m;++i)
    {
        read(t);
        read(x);
        read(y);
        if(!(t^1))
        {
            read(z);
            T.update2(x,y,1,1,n,z);
        }
        else if(!(t^2))
        {
            read(z);
            T.update1(x,y,1,1,n,z);
        }
        else
        {
            printf("%lld\n",T.query(x,y,1,1,n));
        }
    }
    return 0;
}
// 本人打这份代码平均要用 14 min 我真是太菜了TAT