zoukankan      html  css  js  c++  java
  • P6292 区间本质不同子串个数

    知识点:SAM,LCT,线段树

    原题面:Luogu

    之前好像说过无限期停更来着……不管了!

    简述

    给定一长度为 (n) 的字符串 (S),给定 (m) 次询问。每次询问给定参数 (l,r),求由 (s) 的第 (l) 到第 (r) 个字符组成的字符串包含多少个本质不同的子串。
    定义两个字符串 (a,b) 相同当且仅当 (|a|=|b|) 且对于 (iin[1,|a|]) 都有 (a_i=b_i)
    (1le nle 10^5)(1le mle 2 imes 10^5)
    1S,500MB。

    分析

    一些约定:

    (s) 的第 (l) 到第 (r) 个字符组成的子串为 (s[l:r])
    SAM 的状态 (u) 的后缀链接为 (operatorname{link}(u))。其维护的字符串的终止集合为 (operatorname{endpos}(u)),其中最长串的长度为 (operatorname{len}(u)),。

    算法一

    先考虑最简单的暴力。

    套路地考虑此类区间无重问题(P1972 [SDOI2009]HH的项链),离线询问并按右端点排序。之后枚举右端点,考虑新加入新字符的影响,并回答以枚举位置为右端点的询问。设当前枚举到的右端点为 (r),某次询问的区间为 ([l,r])。对于前缀 (s[1:r]) 中的一个子串 (t),当且仅当其最后一次出现位置的左端点 (p) 满足 (pge l) 时,它会对这次询问做出 1 的贡献。
    由上,考虑维护一个权值数列。对于前缀 (s[1:r]) 中的每种本质不同子串 (t),记其最后一次出现位置的左端点为 (p),令权值数列位置 (p) 加 1。询问区间 ([l,r]) 的答案即为权值数列对应区间的和。

    考虑右端点 (r) 右移一位的影响。发现仅会影响作为前缀 (s[1:r+1]) 的后缀的子串的最后一次出现位置。又发现这些子串对应的状态恰好就是前缀 (s[1:r + 1]) 的 SAM 上从 (s[1:r+1]) 对应状态到根的链上的所有状态,于是考虑对每个 SAM 的状态 (u) 维护其 (operatorname{endpos}) 集合中的最大值,即其中所有串最后一次出现位置的右端点,记为 (operatorname{end}_u)。SAM 的每个状态对应子串的 (operatorname{endpos}) 集合相同,则同一状态所有串最后一次出现位置的左端点也构成了一段区间,即为 ([operatorname{end}_u - operatorname{len}(u) + 1, operatorname{end}_u - operatorname{len}(operatorname{link} (u))])。并且可以发现这些区间的并即为 ([1,r + 1])

    考虑动态维护 SAM,在加入新字符后暴力跳 parent 树枚举所有被影响的串对应状态,更新它们最后一次出现位置的左端点对权值区间的贡献即可。权值数列可以使用线段树维护,单次右端点移动复杂度 (O(nlog n)) 级别,总复杂度 (O((n^2 + m)log n)) 级别。

    算法二

    考虑上述算法中在 parent 树上进行了什么操作:

    • 从链底暴力上跳,并对每个状态上对应区间进行区间减。
    • 将链上每个节点的 (operatorname{end}_u) 都修改为 (r+1)

    瓶颈在于操作 1 中每个状态对应的区间不同,必须暴力上跳。但可以发现操作 1 类似 LCT 的 access 操作,操作 2 是以根为端点的链覆盖,考虑使用 LCT 维护 parent 树。

    发现 parent 树是一棵有根树,且链覆盖操作一端点为根,仅需对被修改节点 access 成实链后即可直接进行覆盖。且根据此过程可知,一条实链所有状态的 (operatorname{end}) 均相同,它们影响的位置构成了一段连续区间,又 LCT 上一个点到根最多有 (log n) 级别个 splay,区间减的次数变为 (log n) 次,顺便削除了操作 1 造成的瓶颈。

    但上述算法中存在一个漏洞。在动态维护 SAM 的过程中需要进行加边删边操作。在此过程中需要进行 access,破坏了上述链覆盖得到的“一条实链所有状态的 (operatorname{end}) 均相同”的优美性质。
    但是可以发现根本没有必要动态维护 SAM。可以预先建立 SAM,并维护每个前缀对应的状态。令初始 LCT 中的边全为虚边,LCT 操作时对维护的前缀状态进行操作即可。由于每次链覆盖的对象,都是一段前缀的后缀,显然这样不会使得没有出现过的串做出贡献,可以保证正确性。

    单次右端点移动复杂度变为 (O(log^2 n)) 级别,总复杂度 (O(nlog^2 n + mlog n)) 级别。

    代码

    只需要 access 的 LCT 真是太好写辣!

    算法二

    //知识点:SAM,LCT,线段树
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #define LL long long
    const int kN = 1e5 + 10;
    //=============================================================
    struct Que {
      int l, r, id;
    } q[kN << 1];
    int n, m, pos[kN];
    LL ans[kN << 1];
    char S[kN];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    void Chkmax(int &fir, int sec) {
      if (sec > fir) fir = sec;
    }
    void Chkmin(int &fir, int sec) {
      if (sec < fir) fir = sec;
    }
    bool CMP(Que fir_, Que sec_) {
      return fir_.r < sec_.r;
    }
    namespace Seg {
      #define ls (now_<<1)
      #define rs (now_<<1|1)
      #define mid ((L_+R_)>>1)
      const int kNode = kN << 2;
      LL sum[kNode], tag[kNode];
      void Pushup(int now_) {
        sum[now_] = sum[ls] + sum[rs];
      }
      void Pushdown(int now_, int L_, int R_) {
        sum[ls] += 1ll * tag[now_] * (mid - L_ + 1);
        sum[rs] += 1ll * tag[now_] * (R_ - mid);
        tag[ls] += tag[now_];
        tag[rs] += tag[now_];
        tag[now_] = 0ll;
      }
      void Modify(int now_, int L_, int R_, int l_, int r_, LL val_) {
        if (l_ <= L_ and R_ <= r_) {
          sum[now_] += 1ll * (R_ - L_ + 1) * val_;
          tag[now_] += val_;
          return ;
        }
        Pushdown(now_, L_, R_);
        if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
        if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
        Pushup(now_);
      }
      LL Query(int now_, int L_, int R_, int l_, int r_) {
        if (l_ <= L_ and R_ <= r_) return sum[now_];
        Pushdown(now_, L_, R_);
        LL ret = 0;
        if (l_ <= mid) ret += Query(ls, L_, mid, l_, r_);
        if (r_ > mid) ret += Query(rs, mid + 1, R_, l_, r_);
        return ret;
      }
      #undef ls
      #undef rs
      #undef mid
    }
    namespace SAM {
      const int kNode = kN << 2;
      int node_num = 1, last = 1, tr[kNode][26], len[kNode], link[kNode];
      int end[kNode];
      void Insert(int ch_, int pos_) {
        int p = last, now = last = ++ node_num;
        pos[pos_] = now;
        len[now] = len[p] + 1;
        for (; p && !tr[p][ch_]; p = link[p]) tr[p][ch_] = now;
        if (!p) {
          link[now] = 1;
          return ;
        }
        
        int q = tr[p][ch_];
        if (len[q] == len[p] + 1) {
          link[now] = q;
          return ;
        }
    
        int newq = ++ node_num;
        memcpy(tr[newq], tr[q], sizeof (tr[q]));
        len[newq] = len[p] + 1;
        end[newq] = end[q];
        link[newq] = link[q], link[q] = link[now] = newq;
        for (; p && tr[p][ch_] == q; p = link[p]) tr[p][ch_] = newq;
      }
    }
    namespace LCT {
      #define f fa[now_]
      #define ls son[now_][0]
      #define rs son[now_][1]
      const int kNode = kN << 2;
      int fa[kNode], son[kNode][2], end[kNode], tag[kNode];
      void Modify(int now_, int val_) {
        if (!now_) return;
        end[now_] = tag[now_] = val_;
      }
      void Pushdown(int now_) {
        if (tag[now_]) Modify(ls, tag[now_]), Modify(rs, tag[now_]);
        tag[now_] = 0;
      }
      bool IsRoot(int now_) {
        return son[f][0] != now_ && son[f][1] != now_;
      }
      bool WhichSon(int now_) {
        return son[f][1] == now_;
      }
      void Rotate(int now_) {
        int fa_ = f, w = WhichSon(now_);
        if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
        f = fa[f];
    
        son[fa_][w] = son[now_][w ^ 1];
        fa[son[fa_][w]] = fa_;
    
        son[now_][w ^ 1] = fa_;
        fa[fa_] = now_;
      }
      void Update(int now_) {
        if (!IsRoot(now_)) Update(f);
        Pushdown(now_);
      }
      void Splay(int now_) {
        Update(now_);
        for (; !IsRoot(now_); Rotate(now_)) {
          if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
        }
      }
      void Access(int pos_) {
        int last_ = 0, now_ = pos[pos_];
        for (; now_; last_ = now_, now_ = f) {
          Splay(now_), rs = last_;
          if (end[now_]) { //减去之前 end 的贡献
            Seg::Modify(1, 1, n, end[now_] - SAM::len[now_] + 1,
                                 end[now_] - SAM::len[f], -1); //注意被修改区间
          }
        }
        Seg::Modify(1, 1, n, 1, pos_, 1); //
        Modify(last_, pos_); //链覆盖,更新 end
      }
    }
    
    void Init() {
      scanf("%s", S + 1);
      n = strlen(S + 1);
      m = read();
      for (int i = 1; i <= m; ++ i) q[i] = (Que) {read(), read(), i};
      std::sort(q + 1, q + m + 1, CMP);
      for (int i = 1; i <= n; ++ i) SAM::Insert(S[i] - 'a', i);
      for (int i = 1; i <= SAM::node_num; ++ i) LCT::fa[i] = SAM::link[i]; //初始时全为虚边
    }
    //=============================================================
    int main() { 
      Init();
      for (int r = 1, i = 1; r <= n; ++ r) {
        LCT::Access(r);
        for (; q[i].r <= r && i <= m; ++ i) {
          ans[q[i].id] = Seg::Query(1, 1, n, q[i].l, q[i].r);
        }
      }
      for (int i = 1; i <= m; ++ i) printf("%lld
    ", ans[i]);
      return 0; 
    }
    

    算法一

    //知识点:SAM
    /*
    By:Luckyblock
    */
    #include <algorithm>
    #include <cctype>
    #include <cstdio>
    #include <cstring>
    #define LL long long
    const int kN = 1e5 + 10;
    //=============================================================
    struct Que {
      int l, r, id;
    } q[kN << 1];
    int n, m;
    LL ans[kN << 1];
    char S[kN];
    //=============================================================
    inline int read() {
      int f = 1, w = 0;
      char ch = getchar();
      for (; !isdigit(ch); ch = getchar())
        if (ch == '-') f = -1;
      for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
      return f * w;
    }
    void Chkmax(int &fir, int sec) {
      if (sec > fir) fir = sec;
    }
    void Chkmin(int &fir, int sec) {
      if (sec < fir) fir = sec;
    }
    bool CMP(Que fir_, Que sec_) {
      return fir_.r < sec_.r;
    }
    namespace Seg {
      #define ls (now_<<1)
      #define rs (now_<<1|1)
      #define mid ((L_+R_)>>1)
      const int kNode = kN << 2;
      LL sum[kNode], tag[kNode];
      void Pushup(int now_) {
        sum[now_] = sum[ls] + sum[rs];
      }
      void Pushdown(int now_, int L_, int R_) {
        sum[ls] += 1ll * tag[now_] * (mid - L_ + 1);
        sum[rs] += 1ll * tag[now_] * (R_ - mid);
        tag[ls] += tag[now_];
        tag[rs] += tag[now_];
        tag[now_] = 0ll;
      }
      void Modify(int now_, int L_, int R_, int l_, int r_, LL val_) {
        if (l_ <= L_ and R_ <= r_) {
          sum[now_] += 1ll * (R_ - L_ + 1) * val_;
          tag[now_] += val_;
          return ;
        }
        Pushdown(now_, L_, R_);
        if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
        if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
        Pushup(now_);
      }
      LL Query(int now_, int L_, int R_, int l_, int r_) {
        if (l_ <= L_ and R_ <= r_) return sum[now_];
        Pushdown(now_, L_, R_);
        LL ret = 0;
        if (l_ <= mid) ret += Query(ls, L_, mid, l_, r_);
        if (r_ > mid) ret += Query(rs, mid + 1, R_, l_, r_);
        return ret;
      }
      void Debug(int now_, int L_, int R_) {
        if (L_ == R_) {
          printf("%lld ", sum[now_]);
          return ;
        }
        Pushdown(now_, L_, R_);
        Debug(ls, L_, mid), Debug(rs, mid + 1, R_);
      }
    }
    namespace SAM {
      const int kNode = kN << 2;
      int node_num = 1, last = 1, tr[kNode][26], len[kNode], link[kNode];
      int end[kNode];
      void Insert(int ch_) {
        int p = last, now = last = ++ node_num;
        len[now] = len[p] + 1;
        for (; p && !tr[p][ch_]; p = link[p]) tr[p][ch_] = now;
        if (!p) {
          link[now] = 1;
          return ;
        }
        
        int q = tr[p][ch_];
        if (len[q] == len[p] + 1) {
          link[now] = q;
          return ;
        }
    
        int newq = ++ node_num;
        memcpy(tr[newq], tr[q], sizeof (tr[q]));
        len[newq] = len[p] + 1;
        end[newq] = end[q];
        link[newq] = link[q], link[q] = link[now] = newq;
        for (; p && tr[p][ch_] == q; p = link[p]) tr[p][ch_] = newq;
      }
      void Modify(int pos_) {
        int u = last;
        for (; u != 1; u = link[u]) {
          if (end[u]) Seg::Modify(1, 1, n, end[u] - len[u] + 1, end[u] - len[link[u]], -1);
          end[u] = pos_;
        }
        Seg::Modify(1, 1, n, 1, pos_, 1);
      }
    }
    void Init() {
      scanf("%s", S + 1);
      n = strlen(S + 1);
      m = read();
      for (int i = 1; i <= m; ++ i) q[i] = (Que) {read(), read(), i};
      std::sort(q + 1, q + m + 1, CMP);
    }
    //=============================================================
    int main() { 
      Init();
      for (int r = 1, i = 1; r <= n; ++ r) {
        SAM::Insert(S[r] - 'a');
        SAM::Modify(r);
        for (; q[i].r <= r && i <= m; ++ i) {
          ans[q[i].id] = Seg::Query(1, 1, n, q[i].l, q[i].r);
        }
      }
      for (int i = 1; i <= m; ++ i) printf("%lld
    ", ans[i]);
      return 0; 
    }
    
    作者@Luckyblock,转载请声明出处。
  • 相关阅读:
    学习Bitmap,处理“海量”数据
    学习Trie树,处理“海量”数据
    学习KMP算法
    学习堆与栈内存分配方式
    学习继承和虚析构函数
    学习处理数组子集和的算法
    学习类中的const和static类型
    学习利用动态规划解决若干问题
    【MySQL】MySQL忘记root密码解决方案
    【API】短信通106端口验证短信的实现
  • 原文地址:https://www.cnblogs.com/luckyblock/p/14634965.html
Copyright © 2011-2022 走看看