【HT NOI周赛 T1,CF1801G】 信息密度 题解(AC自动机,字符串后缀结构)
T1 放 CF *3400。。。。
虽然有一点点加强但是做法相同,按照这道题的题意来讲。
题意
给定 m m m 个模式串 t i t_i ti,每个模式串有一个价值 v i v_i vi。
同时给定一个长度为 n n n 的模板串 S S S。 q q q 次询问,每次询问给定 l , r l, r l,r,你需要求出 S l , r S_{l, r} Sl,r 中每个模式串的出现次数乘价值之和: ∑ i ( c n t i × v i ) \sum\limits_{i}(cnt_i \times v_i) i∑(cnti×vi)。
强制在线。
1 ≤ n , m , q ≤ 10 6 1 \leq n, m, q \leq 10^6 1≤n,m,q≤106, ∑ ∣ t i ∣ ≤ 10 6 \sum|t_i| \leq 10^6 ∑∣ti∣≤106。
分析
首先看到多模式串匹配所以我们来考虑 A C A M ACAM ACAM。( A C AC AC 自动机)
不要因为学过 S A M SAM SAM 就只要看到字符串题就往 S A M SAM SAM 上想, S A M SAM SAM 在处理一个或多个字符串的子串信息时比较有优势,但是相对的它的结构更复杂或许拓展起来就比较难。像这道题所求信息只是简单的多模式串匹配,因此我们考虑 A C AC AC 自动机。
我们建出 t i t_i ti 的 A C AC AC 自动机。将 S S S 在 A C AC AC 自动机上匹配一遍,能求出以 S S S 的每个位置为结尾所有匹配的字符串的价值之和,记为 h i h_i hi。那么将 h i h_i hi 求前缀和得到 h i ′ h'_i hi′,每次询问回答 h r ′ − h l − 1 ′ h'_r - h'_{l - 1} hr′−hl−1′。
发现这个做法虽然很自然但是却并不正确,因为有可能一个 h i h_i hi 中包含了一个左端点不在 [ l , r ] [l, r] [l,r] 以内的字符串的贡献。我们尝试在这个做法上进行一些修改使它正确。
假设以位置 i i i 为结尾能匹配的最长模式串的编号为 g i g_i gi,我们找到 [ l , r ] [l, r] [l,r] 中最靠后的位置 p p p,满足 p − ∣ t g p ∣ + 1 ≤ l p - |t_{g_p}| + 1 \leq l p−∣tgp∣+1≤l,那么对于 [ p + 1 , r ] [p + 1, r] [p+1,r],它们的贡献就等于 h r − h p h_r - h_p hr−hp。
对于 [ l , p ] [l, p] [l,p] 的贡献怎么算呢?
暴力的想法是我们对 S l , p S_{l, p} Sl,p 在 A C AC AC 自动机上跑一遍匹配,过程中求出贡献,这显然复杂度不可接受。
但是注意到 S l , p S_{l, p} Sl,p 完全等于 t g p t_{g_p} tgp 长为 p − l + 1 p - l + 1 p−l+1 的后缀,我们完全可以预处理所有 t i t_i ti 的后缀信息,这样就可以 O ( 1 ) O(1) O(1) 查询。
预处理所有 t i t_i ti 的后缀信息:将所有 t i t_i ti 的反串建 A C A M ACAM ACAM,然后对每个 t i t_i ti 从后往前跑一遍匹配即可。复杂度 O ( ∑ ∣ t i ∣ ) O(\sum |t_i|) O(∑∣ti∣)。
每次询问用线段树二分找 p p p,复杂度 O ( m log n ) O(m \log n) O(mlogn)。
总复杂度 O ( ∑ ∣ t i ∣ + m log n ) O(\sum |t_i| + m\log n) O(∑∣ti∣+mlogn)。代码很好写。
CODE:
// 把要求的东西拆成两部分,一部分可以直接算,另一部分可以转化到一个可以预处理的问题上
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
char s[N], t[N];
int n, m, q, l[N], r[N], g[N]; // g[i] 表示 i 位置为结尾能匹配的最长 t 串的编号
LL v[N], S[N], sum[N];
struct ACAM { int tr[N][26], fail[N], et[N], tot; // 初始节点从 0 开始 LL val[N];inline void ins(char *s, int idx, LL v) {int p = 0, len = strlen(s + 1);for(int i = 1; i <= len; i ++ ) {if(!tr[p][s[i] - 'a']) tr[p][s[i] - 'a'] = ++ tot;p = tr[p][s[i] - 'a'];}val[p] += v; et[p] = idx;}inline int Max(int x, int y) {if(!x || !y) return x ^ y;else return r[x] - l[x] <= r[y] - l[y] ? y : x;}inline void build() {queue< int > q;for(int i = 0; i < 26; i ++ ) if(tr[0][i]) q.push(tr[0][i]);while(!q.empty()) {int u = q.front(); q.pop();for(int i = 0; i < 26; i ++ ) {if(tr[u][i]) {fail[tr[u][i]] = tr[fail[u]][i];val[tr[u][i]] += val[fail[tr[u][i]]];et[tr[u][i]] = Max(et[tr[u][i]], et[fail[tr[u][i]]]);q.push(tr[u][i]);}else tr[u][i] = tr[fail[u]][i];}}}
} AC[2]; // 正反
struct SegmentTree {int l, r, mn;#define l(x) tree[x].l#define r(x) tree[x].r#define mn(x) tree[x].mn
} tree[N * 4];
inline void update(int p) {mn(p) = min(mn(p << 1), mn(p << 1 | 1));}
void build(int p, int lp, int rp) {l(p) = lp, r(p) = rp;if(lp == rp) {mn(p) = lp - (r[g[lp]] - l[g[lp]] + 1) + 1;return ;}int mid = (lp + rp >> 1);build(p << 1, lp, mid);build(p << 1 | 1, mid + 1, rp);update(p);
}
int ask(int p, int l, int r, int lim) {if(l <= l(p) && r >= r(p)) {if(mn(p) > lim) return lim - 1;else {if(l(p) == r(p)) return l(p);if(mn(p << 1 | 1) <= lim) return ask(p << 1 | 1, l, r, lim);else return ask(p << 1, l, r, lim);}}int mid = (l(p) + r(p) >> 1);if(r <= mid) return ask(p << 1, l, r, lim);else if(l > mid) return ask(p << 1 | 1, l, r, lim);else {int ret = ask(p << 1 | 1, l, r, lim);if(ret != lim - 1) return ret;else return ask(p << 1, l, r, lim);}
}
int main() { scanf("%s", s + 1); n = strlen(s + 1);scanf("%d", &m); int st = 0;for(int i = 1; i <= m; i ++ ) {char tmp[N]; scanf("%s", tmp + 1); int len = strlen(tmp + 1);for(int j = 1; j <= len; j ++ ) t[st + j] = tmp[j];l[i] = st + 1, r[i] = st + len;st += len; scanf("%lld", &v[i]);AC[0].ins(tmp, i, v[i]); reverse(tmp + 1, tmp + len + 1);AC[1].ins(tmp, i, v[i]);}AC[0].build(); AC[1].build();for(int i = 1; i <= m; i ++ ) {int p = 0;for(int j = r[i]; j >= l[i]; j -- ) {p = AC[1].tr[p][t[j] - 'a'];S[j] = AC[1].val[p];}}for(int i = 1; i <= st; i ++ ) S[i] += S[i - 1];int p = 0;for(int i = 1; i <= n; i ++ ) {p = AC[0].tr[p][s[i] - 'a'];sum[i] = AC[0].val[p]; g[i] = AC[0].et[p];}for(int i = 1; i <= n; i ++ ) sum[i] += sum[i - 1];LL lstans = 0;build(1, 1, n);scanf("%d", &q);while(q -- ) {LL L, R; scanf("%lld%lld", &L, &R);L = (L ^ lstans) % n + 1, R = (R ^ lstans) % n + 1;if(L > R) swap(L, R); lstans = 0;int p = ask(1, L, R, L); // 找到 mn <= L 的最靠后的位置, 没有返回 L - 1lstans += sum[R] - sum[p]; if(p != L - 1) {int x = g[p]; // 那么就是 x 的一段长为 p - L + 1 的后缀 lstans += S[r[x]] - S[r[x] - (p - L + 1)];}printf("%lld\n", lstans);}return 0;
}
总结
本题中做法的核心相当于我们减少了所要维护的信息。
第二部分的暴力思路其实等价于求出所有子串匹配的信息,维护所有子串的信息显然是不可接受的,但是我们通过转化将它变成了模式串的后缀信息,这样就是可接受的。
或许与字符串某些放缩的思路类似???
总之如果遇到所有维护的子串信息过多,我们就尝试能不能转化成前后缀信息,因为前后缀的数量是比较少的。