P6292 区间本质不同子串个数
知识点:SAM,LCT,线段树
原题面:Luogu。
之前好像说过无限期停更来着……不管了!
简述
给定一长度为 的字符串 ,给定 次询问。每次询问给定参数 ,求由 的第 到第 个字符组成的字符串包含多少个本质不同的子串。
定义两个字符串 相同当且仅当 且对于 都有 。
,。
1S,500MB。
分析
一些约定:
记 的第 到第 个字符组成的子串为 。
SAM 的状态 的后缀链接为 。其维护的字符串的终止集合为 ,其中最长串的长度为 ,。
算法一
先考虑最简单的暴力。
套路地考虑此类区间无重问题(P1972 [SDOI2009]HH的项链),离线询问并按右端点排序。之后枚举右端点,考虑新加入新字符的影响,并回答以枚举位置为右端点的询问。设当前枚举到的右端点为 ,某次询问的区间为 。对于前缀 中的一个子串 ,当且仅当其最后一次出现位置的左端点 满足 时,它会对这次询问做出 1 的贡献。
由上,考虑维护一个权值数列。对于前缀 中的每种本质不同子串 ,记其最后一次出现位置的左端点为 ,令权值数列位置 加 1。询问区间 的答案即为权值数列对应区间的和。
考虑右端点 右移一位的影响。发现仅会影响作为前缀 的后缀的子串的最后一次出现位置。又发现这些子串对应的状态恰好就是前缀 的 SAM 上从 对应状态到根的链上的所有状态,于是考虑对每个 SAM 的状态 维护其 集合中的最大值,即其中所有串最后一次出现位置的右端点,记为 。SAM 的每个状态对应子串的 集合相同,则同一状态所有串最后一次出现位置的左端点也构成了一段区间,即为 。并且可以发现这些区间的并即为 。
考虑动态维护 SAM,在加入新字符后暴力跳 parent 树枚举所有被影响的串对应状态,更新它们最后一次出现位置的左端点对权值区间的贡献即可。权值数列可以使用线段树维护,单次右端点移动复杂度 级别,总复杂度 级别。
算法二
考虑上述算法中在 parent 树上进行了什么操作:
- 从链底暴力上跳,并对每个状态上对应区间进行区间减。
- 将链上每个节点的 都修改为 。
瓶颈在于操作 1 中每个状态对应的区间不同,必须暴力上跳。但可以发现操作 1 类似 LCT 的 access 操作,操作 2 是以根为端点的链覆盖,考虑使用 LCT 维护 parent 树。
发现 parent 树是一棵有根树,且链覆盖操作一端点为根,仅需对被修改节点 access 成实链后即可直接进行覆盖。且根据此过程可知,一条实链所有状态的 均相同,它们影响的位置构成了一段连续区间,又 LCT 上一个点到根最多有 级别个 splay,区间减的次数变为 次,顺便削除了操作 1 造成的瓶颈。
但上述算法中存在一个漏洞。在动态维护 SAM 的过程中需要进行加边删边操作。在此过程中需要进行 access,破坏了上述链覆盖得到的“一条实链所有状态的 均相同”的优美性质。
但是可以发现根本没有必要动态维护 SAM。可以预先建立 SAM,并维护每个前缀对应的状态。令初始 LCT 中的边全为虚边,LCT 操作时对维护的前缀状态进行操作即可。由于每次链覆盖的对象,都是一段前缀的后缀,显然这样不会使得没有出现过的串做出贡献,可以保证正确性。
单次右端点移动复杂度变为 级别,总复杂度 级别。
代码
只需要 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\n", 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\n", ans[i]); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】