1.16闲话

哎我草怎么要考试了

推歌:四重罪孽/洛天依,言和,乐正绫 by DELA

今天在写线段树合并的时候突然说需要在周日去选直升!!!惊!!!

那么我们不学线段树合并了,去复习板子

突然发现有一份没发出来的学习笔记demo版放鲜花里撑字数

线段树是一种我非常喜欢的数据结构,线段树主要用于维护对于区间的操作

例如对于普通的数组进行区间查询的时间复杂度为\(O(n)\) (直接对其进行相加) ,而单点修改和区间修改的时间复杂度分别是是 \(O(1)\)\(O(n)\)

对于前缀和数组进行的区间查询复杂度为\(O(1)\),但单点修改的复杂度是\(O(n)\),区间修改为\(O(n^2)\)

那么这些复杂度是比较劣的

对于线段树而言,其查询的时间复杂度仅为\(O(\log n)\),而修改的复杂度也为\(O(\log n)\)

事实上,线段树相较于树状数组常数因子较大以至于在毒瘤卡常题中似乎表现不佳,但可维护的操作较多

  • 线段树的结构

    线段树结构类似二叉树,如以下图片

    image

    线段树把每个长度不为 \(1\) 的区间\(\{l\sim r\}\)划分为两个部分

    1. 左区间 \(\{l\sim mid\}\)

    2. 右区间 \(\{mid+1 \sim r\}\)

    其中我们定义 \(mid=\dfrac {l+r} 2\),我们可以对每个区间进行编号

    编号方法如下

    \(root\) 的节点编号为 \(1\)

    对于编号为\(q\)的节点,其左儿子编号为 \(q\times 2\),右儿子编号为\(q\times 2+1\)

    因此,线段树需要开 \(4\) 倍空间,特殊的对于一些需要额外推平一次标记的线段树需要开 \(8\) 倍空间

  • 线段树的建树

    我们通常考虑采取递归建树的方式

    对于现在递归到的根节点,若其长度不为1则直接按照上面的划分方法去划分并递归建树

    若长度为\(1\)直接初始化,接下来返回即可

    • 代码

      inline void build(int q,int l,int r){
          t[q].l=l;
          t[q].r=r;
          if(l==r){
              t[q].dat=a[l];
              return;
          }
          int mid=(l+r)/2;
          build(q*2,l,mid);
          build(q*2+1,mid+1,r);
          t[q].dat=t[q*2].dat+t[q*2+1].dat;
      }
      
  • 线段树的区间查询

    如果需要查询一个已经被划分好的区间,直接返回即可

    但对于区间\(\{l \sim r\}\)并不是任何一个完整的已划分区间,我们可以将其拆成多个区间来查询

    • 代码

      inline int ask(int q,int l,int r){
          if(t[q].l>r || t[q].r<l) 
              return 0;
          if(t[q].l>=l && t[q].r<=r) 
              return t[q].dat;
          return ask(q*2,l,r)+ask(q*2+1,l,r); 
      }
      
  • 线段树的区间修改

    首先我们需要了解如何对线段树进行单点修改

    显而易见的是我们可以访问到最下层的节点对其进行修改,并在返回时对包括其的区间进行修改,时间复杂度为\(O(\log n)\)

    如何进行区间修改呢?若对于长度为\(n\)的区间进行\(n\)次单点修改,其复杂度为\(O(n \log n)\),明显无法承受

    那么我们可以引入一个名为 \(lazy\) 标记的东西

    在修改时并不直接对区间内所有包括的节点修改,而是通过打标记的方法表明该节点对应的区间在某一次操作中被更改,访问时直接对其的左右儿子修改并下传标记至左右儿子即可

    • 代码

      inline void lazy(int q){
          t[q*2].lazy+=t[q].lazy;
          t[q*2].dat+=(t[q*2].r-t[q*2].l+1)*t[q].lazy;
          t[q*2+1].lazy+=t[q].lazy;
          t[q*2+1].dat+=(t[q*2+1].r-t[q*2+1].l+1)*t[q].lazy;
          t[q].lazy=0;
      }
      inline void change(int q,int l,int r,int v){
          if(t[q].l>r || t[q].r<l) return;
          if(t[q].l>=l && t[q].r<=r)
          {
              t[q].lazy+=v;
              t[q].dat+=(t[q].r-t[q].l+1)*v;
              return;
          }
          if(t[q].lazy!=0) 
              lazy(q);
          change(q*2,l,r,v);
          change(q*2+1,l,r,v);
          t[q].dat=t[q*2].dat+t[q*2+1].dat;
      }
      

      由于lazy标记的存在,我们需要对 ask 操作进行略微的修改

      inline int ask(int q,int l,int r){
          if(t[q].l>r || t[q].r<l) 
              return 0;
          if(t[q].l>=l && t[q].r<=r) 
              return t[q].dat;
          if(t[q].lazy!=0)
              lazy(q);
          return ask(q*2,l,r)+ask(q*2+1,l,r); 
      }
      

这样线段树基础操作就完成了

  • 代码

    点击查看代码
    #include<bits/stdc++.h>
    using namespace std;
    int n,m,a[0x66ccff];
    inline long long read(){
        long long f=1,x=0;char ch;
        while(ch<'0'||ch>'9'){ch=getchar();if(ch=='-')f=-1;};
        while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();};
        return f*x;
    }
    struct node{
        long long l,r;
        long long lazy,dat;
    }t[0x66ccff*4];
    void build(int q,int l,int r){
        t[q].l=l;
        t[q].r=r;
        if(l==r){
            t[q].dat=a[l];
            return;
        }
        int mid=(l+r)/2;
        build(q*2,l,mid);
        build(q*2+1,mid+1,r);
        t[q].dat=t[q*2].dat+t[q*2+1].dat;
    }
    void lazy(int q){
        t[q*2].lazy+=t[q].lazy;
        t[q*2].dat+=(t[q*2].r-t[q*2].l+1)*t[q].lazy;
        t[q*2+1].lazy+=t[q].lazy;
        t[q*2+1].dat+=(t[q*2+1].r-t[q*2+1].l+1)*t[q].lazy;
        t[q].lazy=0;
    }
    void change(int q,int l,int r,int v)
    {
        if(t[q].l>r || t[q].r<l) return;
        if(t[q].l>=l && t[q].r<=r)
        {
            t[q].lazy+=v;
            t[q].dat+=(t[q].r-t[q].l+1)*v;
            return;
        }
        if(t[q].lazy>0) 
            lazy(q);
        change(q*2,l,r,v);
        change(q*2+1,l,r,v);
        t[q].dat=t[q*2].dat+t[q*2+1].dat;
    }
    long long ask(int q,int l,int r){
        if(t[q].l>r || t[q].r<l) 
            return 0;
        if(t[q].l>=l && t[q].r<=r) 
            return t[q].dat;
        if(t[q].lazy>0) 
            lazy(q);
        return ask(q*2,l,r)+ask(q*2+1,l,r); 
    }
    signed main(){
        n=read();
        for(int i=1;i<=n;i++)
            a[i]=read();
        m=read();
        build(1,1,n);
        for(int i=1;i<=m;i++){
            int x,y;
            string qwq;
            cin>>qwq;
            if(qwq=="ADD"){
                int k;
                x=read(),y=read(),k=read();
                change(1,x,y,k);
            }
            else {
                x=read(),y=read();
                cout<<ask(1,x,y)<<endl;
            }
        }
    }
    

  • 动态开点线段树

    众所周知,线段树需要维护的区间非常大,但是可能用到的节点会很少,那么就浪费了很多的空间

    我们可以只开一部分的节点,建立一个残疾线段树,节点只有在需要时才被创建

    此时的线段树因为建的很丑所以不再是完全二叉树,所以不能用\(q\times 2\)\(q\times 2+1\)来记录左右儿子

    我们可以定义两个数组\(ls_q\)\(rs_q\)表示\(q\)的左儿子和右儿子

    那么就可以借此实现动态开点线段树了

    • 代码

      点击查看代码
      #include<bits/stdc++.h>
      #define lc t[q].ls
      #define rc t[q].rs
      #define mid ((l+r)/2)
      #define int long long
      using namespace std;
      int n,m,a[0x66ccff],tot,root;
      inline int read(){
          int f=1,x=0;char ch;
          while(ch<'0'||ch>'9'){ch=getchar();if(ch=='-')f=-1;};
          while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();};
          return f*x;
      }
      struct node{
          int l,r,ls,rs;
          int lazy,dat;
      }t[0x66ccff];
      inline void insert(int &q,int l,int r,int i,int val){
          if(!q) q=++tot;
          t[q].l=l;t[q].r=r;
          if(l==r){
              t[q].dat+=val;
              return;
          }
          if(i<=mid) insert(lc,l,mid,i,val);
          else insert(rc,mid+1,r,i,val);
          t[q].dat=t[lc].dat+t[rc].dat;
      }
      inline void lazyy(int q){
          t[lc].lazy+=t[q].lazy;
          t[lc].dat+=(t[lc].r-t[lc].l+1)*t[q].lazy;
          t[rc].lazy+=t[q].lazy;
          t[rc].dat+=(t[rc].r-t[rc].l+1)*t[q].lazy;
          t[q].lazy=0;
      }
      inline void change(int &q,int l,int r,int v){
          if(t[q].l>r || t[q].r<l) return;
          if(t[q].l>=l && t[q].r<=r){
              t[q].lazy+=v;
              t[q].dat+=(t[q].r-t[q].l+1)*v;
              return;
          }
          if(t[q].lazy>0) 
              lazyy(q);
          change(lc,l,r,v);
          change(rc,l,r,v);
          t[q].dat=t[lc].dat+t[rc].dat;
      }
      inline int ask(int q,int l,int r){
          if(!q) return 0;
          if(t[q].l>r||t[q].r<l) return 0;
          if(t[q].l>=l&&t[q].r<=r) return t[q].dat;
          if(t[q].lazy) lazyy(q);
          return ask(lc,l,r)+ask(rc,l,r); 
      }
      signed main(){
          n=read();
          for(int i=1;i<=n;i++){
              insert(root,1,n,i,read());
          }
          m=read();
          for(int i=1;i<=m;i++){
              int x,y;
              string qwq;
              cin>>qwq;
              if(qwq=="ADD"){
                  int k;
                  x=read(),y=read(),k=read();
                  change(root,x,y,k);
              }
              else {
                  x=read(),y=read();
                  cout<<ask(1,x,y)<<endl;
              }
          }
      }
      

  • 线段树合并

    考虑一种暴力的思想,把两颗线段树直接大力合并起来,相同位置的信息揉成一团

    合并的时候直接从 root 节点进行递归合并

    递归到某个节点时判断,若其对应的节点为空则直接在新的线段树里用另一个非空的来代替

    若两个都存在则对其进行合并

    最后,根据子节点去更新节点即可

闲话,但是数论复习特供版

  • gcd

    求解 a 和 b 的最大公约数

    inline int gcd(int a, int b) {
        return b==0?a:gcd(b,a%b);
    }
    
  • exgcd

    求解 \(ax+by=\gcd(a,b)\) 的一组可行解

    inline int exgcd(int a,int b,int &x,int &y){
        if(b==0){
            x=1,y=0;
            return a;
        }
        int d=exgcd(b,a%b,x,y);
        int z=x;x=y,y=z-y*(a/b);
        return d;
    }
    
  • lcm

    求解 a 和 b 的最小公倍数

    \(\text {lcm}(a,b)=\dfrac{a\times b}{\gcd(a,b)}\)

    inline int lcm(int a, int b) {
        return (a*b)/gcd(a,b);
    }
    
  • 逆元

    求解 a 在 mod b 意义下的逆元

    就是用 exgcd 求解 \(ax\equiv 1\pmod{b}\)

    inline int exmod(int a,int b){
        int d,x,y;
        d=exgcd(a,b,x,y);
        if(d==1) return(x%b+b)%b;
        else return -1;
    }
    
  • 欧拉函数
    • 欧拉函数\(\varphi(n)\)

      小于等于\(n\)中与\(n\)互质的数的个数

    • 求$\varphi(x)$
      inline int phi(int n){
          int ret=n;
          for(int i=2;i*i<=(n);i++)
          if(!(n%i)){
              ret=ret/i*(i-1);
              while(!(n%i)) n/=i;
          }
          if(n>1) ret=ret/n*(n-1);
          return ret;
      }
      
    • 欧拉函数的线性筛

      估计用不到(你

  • 费马小定理

    \(p\in \mathcal P\)\(\gcd(a,p)=1\) ,则\(a^{p-1}\equiv 1 \pmod{p}\)

  • 欧拉定理

    \(\gcd(a, m) = 1\) ,则 \(a^{\varphi(m)} \equiv 1 \pmod{m}\)

  • 组合数
    • 预处理法

      (适用于\(a,b≤10^4\))

      void init(){
      for(int i=0;i<N;i++)
          for(int j=0;j<=i;j++)
              if(!j) c[i][j]=1;
              else c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
      }
      
    • 乘法逆元

      根据\(\dbinom{b}{a}=\dfrac{n!}{m!\times(n-m)!}\)直接怒求

      int fact[N],Inverse[N];
      inline int qpow(int a,int k,int p){
          int res=1;
          while(k){
              if(k&1) res=res*a%p;
              k>>=1;
              a=a*a%p;
          }
          return res;
      }
      inline int solve(int n,int m){
          return fact[a]*Inverse[a-b]%mod*Inverse[b]%mod;
      }
      signed main(){
          fact[0]=Inverse[0]=1;
          for(int i=1;i<N;i++){
              fact[i]=fact[i-1]*i%mod;
          }
          Inverse[N-1]=qpow(fact[N-1],mod-2,mod);
          for(int i=N-2;i>=1;i--)
              Inverse[i]=Inverse[i+1]*(i+1)%mod;
          int n=read(),m=read();
          cout<<solve(n,m);
      }
      
    • Lucas定理

      \[\dbinom{n}{m}\bmod p = \dbinom{\left\lfloor n/p \right\rfloor}{\left\lfloor m/p\right\rfloor}\cdot\dbinom{n\bmod p}{m\bmod p}\bmod p \]

      inline int qpow(int a,int k, int p){
          int res=1;
          while(k){
              if(k&1) res=res*a%p;
              k>>=1;
              a=a*a%p;
          }
          return res;
      }
      inline int Comb(int a,int b,int p){
          if(a<b) return 0;
          int res=1;
          for(int i=1,j=a;i<=b;i++,j--)
              res=res*j%p,
              res=res*qpow(i,p-2,p)%p;
          return res;
      }
      inline int lucas(int a, int b, int p){
          if(a<p&&b<p) return Comb(a,b,p);
          return Comb(a%p,b%p,p)*lucas(a/p,b/p,p)%p;
      }
      signed main(){
          int a=read(),b=read(),p=read();
          cout<<lucas(a,b,p)<<endl;
      }
      
  • $\text{CRT}$

    \(\begin{cases} x&\equiv a_1 \pmod {m_1} \\ x &\equiv a_2 \pmod {m_2} \\ &\vdots \\ x &\equiv a_k \pmod {m_k} \\ \end{cases}\)

    方程组的解为

    \(x=(\sum_{c=i}^{k}a_i\times \frac{ m}{m_i} \times (\frac{ m}{ m_i})^{-1})\bmod \ m\)

    #include<bits/stdc++.h>
    #define int long long
    #define maxm 0X6CF
    inline int read(){
        int s = 0,w = 1;char ch = getchar();
        while(ch<'0'||ch>'9'){ if(ch == '-') w = -1;ch = getchar();}
        while(ch>='0'&&ch<='9'){ s = s*10+ch-'0';ch = getchar();}
        return s*w;
    }
    inline int exgcd(int a,int b,int &x,int &y){
        if(b==0){
            x=1,y=0;
            return a;
        }
        int d=exgcd(b,a%b,x,y);
        int z=x;x=y,y=z-y*(a/b);
        return d;
    }
    inline int exmod(int a,int b){
        int d,x,y;
        d=exgcd(a,b,x,y);
        if(d==1) return(x%b+b)%b;
        else return -1;
    }
    std::vector<int> a,m;
    int M=1,x=0;
    signed main(){
        int n=read();
        a.resize(n),
        m.resize(n);
        for(int i=0;i<n;i++){
            M*=(m[i]=read());
            a[i]=read();
        }
        for(int i=0;i<n;i++){
            (x+=(a[i]*(M/m[i])*exmod(M/m[i],m[i])))%=M;
        }
        std::cout<<x;
    }    
    

posted @ 2024-01-16 20:56  Vsinger_洛天依  阅读(29)  评论(5编辑  收藏  举报