字串雜題
J. Suffix Automaton
來源:The 2021 CCPC Guilin Onsite (Grand Prix of EDG)
2021CCPC 桂林站的題,用字尾陣列+平衡樹做的.
統計字典序問題可以用字尾陣列方便解決,即提取出 $\mathrm{sa, Height}$ 陣列.
對於本質不同的字串,排名為 $\mathrm{i}$ 的字尾所貢獻的字串長度區間為 $\mathrm{[L,R]}$.
將問題轉化為求長度為 $\mathrm{len}$ 的字串的第 $\mathrm{k}$ 個,可以離線用平衡樹維護當前處在長度為 $\mathrm{len}$ 的情況.
然後在平衡樹中二分找到對應位置即可.
#include <cstdio> #include <set> #include <map> #include <cstring> #include <algorithm> #define ll long long #define pb push_back #define N 2000009 #define setIO(s) freopen(s".in","r",stdin) using namespace std; char str[N]; namespace SA { int n, ty, rk[N], sec[N], sa[N], t[N], h[N], mi[20][1000005], Lg[1000004], m2[20][1000005]; void getsa() { for(int i = 0; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) t[rk[i] = str[i]] ++ ; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = 1; i <= n ; ++ i) sa[t[rk[i]] -- ] = i; for(int k = 1; k <= n ; k <<= 1) { int cnt = 0; for(int i = n - k + 1; i <= n ; ++ i) sec[++ cnt] = i; for(int i = 1; i <= n ; ++ i) if(sa[i] > k) sec[++ cnt] = sa[i] - k; for(int i = 0; i <= ty; ++ i) t[i] = 0; for(int i = 1; i <= n ; ++ i) t[rk[i]] ++ ; for(int i = 1; i <= ty; ++ i) t[i] += t[i - 1]; for(int i = n; i >= 1 ; -- i) { sa[t[rk[sec[i]]] -- ] = sec[i], sec[i] = 0; } swap(rk, sec), rk[sa[1]] = cnt = 1; for(int i = 2; i <= n ; ++ i) { rk[sa[i]] = (sec[sa[i]] == sec[sa[i - 1]] && sec[sa[i] + k] == sec[sa[i - 1] + k]) ? cnt : ++ cnt; } ty = cnt; if(cnt == n) break; } int det = 0; for(int i = 1; i <= n ; ++ i) { // i and rk[i] - 1 的 lcp if(det) -- det; while(str[i + det] == str[sa[rk[i] - 1] + det]) ++ det; h[rk[i]] = det; } } void init() { n = strlen(str + 1), ty = 134; } void RMQ() { Lg[1] = 0; for(int i = 2; i < 1000003; ++ i) Lg[i] = Lg[i >> 1] + 1; for(int i = 1; i <= n ; ++ i) mi[0][i] = h[i], m2[0][i] = sa[i]; for(int i = 1; (1 << i) <= n ; ++ i) { for(int j = 1; j + (1 << i) - 1 <= n ; ++ j) { mi[i][j] = min(mi[i - 1][j], mi[i - 1][j + (1 << (i - 1))]); m2[i][j] = min(m2[i - 1][j], m2[i - 1][j + (1 << (i - 1))]); } } } int query(int l, int r) { int o = Lg[r - l + 1]; return min(mi[o][l], mi[o][r - (1 << o) + 1]); } int getans(int l, int r) { int o = Lg[r - l + 1]; return min(m2[o][l], m2[o][r - (1 << o) + 1]); } }; namespace SGT { // 普通線段樹即可. #define ls now << 1 #define rs now << 1 | 1 int sum[N << 2]; void update(int l, int r, int now, int p, int v) { if(l == r) { sum[now] += v; return ; } int mid = (l + r) >> 1; if(p <= mid) update(l, mid, ls, p, v); else update(mid + 1, r, rs, p, v); sum[now] = sum[ls] + sum[rs]; } int query(int l, int r, int now, int k) { if(l == r) return l; int mid = (l + r) >> 1; if(k <= sum[ls]) return query(l, mid, ls, k); else return query(mid + 1, r, rs, k - sum[ls]); } #undef ls #undef rs }; ll arr[N], pre[N]; int cnt, pl[N], pr[N], n; struct data { int id, len; ll k; data(int id = 0, int len = 0, ll k = 0):id(id), len(len), k(k){} }a[N]; struct suff { int id, pos, d; suff(int id = 0, int pos = 0, int d = 0):id(id), pos(pos), d(d){} }e[N]; bool cmp1(data i, data j) { return i.len < j.len; } bool cmp2(suff i, suff j) { return i.pos < j.pos; } int main() { // setIO("input"); scanf("%s", str + 1); n = strlen(str + 1); SA::init(); SA::getsa(); SA::RMQ(); for(int i = 1; i <= n; ++ i) { int l = n - SA::sa[i] + 1, st = SA::h[i] + 1; arr[st] ++ , arr[l + 1] -- ; // 貢獻是 [st, l] e[i * 2 - 1] = suff(i, st, 1); e[i * 2] = suff(i, l + 1, -1); } for(int i = 1; i <= n ; ++ i) { arr[i] += arr[i - 1]; pre[i] = pre[i - 1] + arr[i]; } int Q; scanf("%d", &Q); for(int i = 1; i <= Q; ++ i) { ll k; scanf("%lld", &k); // 直接不合法. if(k > pre[n]) { pl[i] = pr[i] = -1; } else { int p = lower_bound(pre + 1, pre + 1 + n, k) - pre - 1; k -= pre[p]; // 得到 (p + 1, k) a[++ cnt] = data(i, p + 1, k); } } sort(a + 1, a + 1 + cnt, cmp1); sort(e + 1, e + 1 + 2 * n, cmp2); for(int i = 1, j = 1; i <= cnt; ++ i) { while(j <= 2 * n && e[j].pos <= a[i].len) { SGT::update(1, n, 1, e[j].id, e[j].d); ++ j; } int p = SGT::query(1, n, 1, (int)a[i].k); int po = SA::sa[p]; int l = p + 1, r = n, ans = 0; while(l <= r) { int mid = (l + r) >> 1; if(SA::query(p + 1, mid) >= a[i].len) ans = mid, l = mid + 1; else r = mid - 1; } if(ans) { po = min(po, SA::getans(p + 1, ans)); } pl[a[i].id] = po; pr[a[i].id] = po + a[i].len - 1; } for(int i = 1; i <= Q; ++ i) { printf("%d %d\n", pl[i], pr[i]); } return 0; }
M. String Problem
來源:The 2021 ICPC Asia Shenyang Regional Contest
標籤:字串,字尾自動機
正式賽的時候用一個 $\mathrm{lcp}$ 亂搞切掉的,這裡給出正經做法.
遇到字典序問題,考慮利用字尾自動機來解.
靜態問題可以沿著字尾自動機的字元邊貪心走大的.
那麼不妨先維護出字首 $\mathrm{i}$ 的答案,然後考慮下一位的影響.
顯然,如果影響的話一定是下一位的字母被加入答案,那麼在後綴自動機中就是答案中深度最小的點改變.
改變成的新點一定是加入 $\mathrm{i+1}$ 字元時所影響的新點,而所有新點總和是 $\mathrm{O(n)}$ 的.
由於字尾自動機的構建方式,線上做是不現實的,不妨離線構建完字尾自動機然後記錄每個點最早出現位置.
最後開一個數組統計並離線下來即可.
#include <bits/stdc++.h> #define ll long long #define pb push_back #define N 2000009 #define setIO(s) freopen(s".in","r",stdin) using namespace std; char str[N]; int n, last, tot, fir[N], len[N], st[N], ch[N][26], pre[N], dep[N], vis[N], tail, a[N]; struct data { int p, c; data(int p = 0, int c = 0):p(p), c(c){} }; vector<data>G[N]; void extend(int c, int pos) { int np = ++ tot, p = last; len[np] = len[p] + 1, last = np; fir[np] = pos; for(; p && !ch[p][c]; p = pre[p]) { ch[p][c] = np; } if(!p) pre[np] = 1; else { int q = ch[p][c]; if(len[q] == len[p] + 1) pre[np] = q; else { int nq = ++ tot; len[nq] = len[p] + 1; fir[nq] = fir[q]; memcpy(ch[nq], ch[q], sizeof(ch[q])); pre[nq] = pre[q], pre[np] = pre[q] = nq; for(; p && ch[p][c] == q; p = pre[p]) ch[p][c] = nq; } } } int main() { // setIO("input"); scanf("%s", str + 1); n = strlen(str + 1); last = tot = 1; for(int i = 1; i <= n ; ++ i) { extend(str[i] - 'a', i); } for(int i = 1; i <= tot; ++ i) { for(int j = 0; j < 26; ++ j) { if(ch[i][j]) { int q = ch[i][j]; G[fir[q]].pb(data(i, j)); } } } vis[1] = 1, a[++ tail] = 1, st[1] = -1; for(int i = 1; i <= n ; ++ i) { int mk = 0, de = 0; for(int j = 0; j < G[i].size() ; ++ j) { data e = G[i][j]; if(vis[e.p] && e.c > st[e.p]) { if(mk == 0) { mk = e.p, de = dep[e.p]; } else if(dep[e.p] < de) { mk = e.p, de = dep[e.p]; } } } if(mk) { while(a[tail] != mk) vis[a[tail]] = 0, -- tail; st[mk] = str[i] - 'a'; a[++ tail] = ch[mk][str[i] - 'a'], dep[a[tail]] = tail, vis[a[tail]] = 1, st[a[tail]] = -1; } printf("%d %d\n", i - tail + 2, i); } return 0; }
Little Elephant and Strings
來源:CF204E
做法一:$\mathrm{SA}+$ 主席樹
可以把所有串拼在一起,然後中間用特殊分隔符分開.
考慮處理第 $\mathrm{i}$ 個串的 $\mathrm{l}$ 位置開始的子串.
顯然這個 $\mathrm{r}$ 具有單調性,所以可以二分最大的 $\mathrm{r}$.
這個東西用雙指標掃,然後每個區間用主席樹查一下編號種類即可.
時間複雜度為 $\mathrm{O(n \log n)}$.
做法二: $\mathrm{SAM}$
構建廣義字尾自動機,然後插入串的時候暴力更新 $\mathrm{pre}$ 的資訊.
最後查詢的時候把串放到自動機上跑一遍倍增求一下即可.
時間複雜度也是 $O(n \log n)$.
p.s. 暴力跳的方法可能不太優美.
這個問題等價於給定樹上若干個點,求這些點所構成的樹鏈的並.
可以將所有點離線,按照 $\mathrm{dfs}$ 序排序,然後在相鄰 $\mathrm{lca}$ 處減掉
做法三:$\mathrm{SA}+$單調佇列.
若固定左端點 $\mathrm{l}$, 則顯然要找到最大的 $\mathrm{r}$ 使得種類不小於 $K$.
所以可以用雙指標維護數量恰好為 $\mathrm{K}$ 的區間,並維護這個區間的 $\mathrm{lcp}$.
對於一個位置 $\mathrm{p}$ 來說, $\mathrm{p}$ 的貢獻就是包含 $\mathrm{p}$ 的 $\mathrm{lcp}$ 最大的.
這個東西用線段樹或單調佇列維護一下即可.
做法二:
#include <cstdio> #include <cstring> #include <vector> #include <algorithm> #define N 200009 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; char str[N], tp[N]; int n, K, st[N], ed[N], pre[N << 1], len[N << 1], ch[N << 1][26]; int last, tot, vis[N], cn[N], fa[N][19]; void extend(int c) { if(ch[last][c]) { int p = last, q = ch[last][c]; if(len[q] == len[p] + 1) last = q; else { int nq = ++ tot; len[nq] = len[p] + 1; vis[nq] = vis[q], cn[nq] = cn[q]; memcpy(ch[nq], ch[q], sizeof(ch[q])); pre[nq] = pre[q], pre[q] = nq; for(; p && ch[p][c] == q; p = pre[p]) ch[p][c] = nq; last = nq; } } else { int p = last, np = ++ tot; len[np] = len[p] + 1, last = np; for(; p && !ch[p][c]; p = pre[p]) ch[p][c] = np; if(!p) pre[np] = 1; else { int q = ch[p][c]; if(len[q] == len[p] + 1) pre[np] = q; else { int nq = ++ tot; len[nq] = len[p] + 1; vis[nq] = vis[q], cn[nq] = cn[q]; memcpy(ch[nq], ch[q], sizeof(ch[q])); pre[nq] = pre[q], pre[np] = pre[q] = nq; for(; p && ch[p][c] == q; p = pre[p]) ch[p][c] = nq; } } } } int jump(int x) { for(int i = 18; i >= 0; -- i) { if(cn[fa[x][i]] < K) x = fa[x][i]; } return x; } int main() { // setIO("input"); scanf("%d%d", &n, &K); last = tot = 1; for(int i = 1; i <= n ; ++ i) { scanf("%s", tp + 1); st[i] = ed[i - 1] + 1; ed[i] = st[i] + strlen(tp + 1) - 1; last = 1; for(int j = st[i]; j <= ed[i]; ++ j) { str[j] = tp[j - st[i] + 1]; extend(str[j] - 'a'); int p = last; while(p && vis[p] != i) { vis[p] = i, ++ cn[p], p = pre[p]; } } } for(int i = 1; i <= tot; ++ i) fa[i][0] = pre[i]; for(int i = 1; i < 19 ; ++ i) for(int j = 1; j <= tot; ++ j) fa[j][i] = fa[fa[j][i - 1]][i - 1]; cn[0] = N; for(int i = 1; i <= n ; ++ i) { int p = 1; ll ans = 0ll; for(int j = st[i]; j <= ed[i] ; ++ j) { int c = str[j] - 'a'; p = ch[p][c]; int nex = jump(p); if(cn[nex] >= K) ans += len[nex]; else { nex = pre[nex]; if(nex && cn[nex] >= K) ans += len[nex]; } } printf("%I64d", ans); if(i != n) printf(" "); } return 0; }
Paper task
來源:CF653F
這道題的重點是模擬,字尾自動機只是幫助統計本質不同的工具.
p.s. 字尾陣列的做法類似,是利用 $\mathrm{height[x]}$ 陣列來保持本質不同的條件.
$\mathrm{parent}$ 樹和 $\mathrm{height}$ 陣列都是有助於思考的同類型工具.
#include <cstdio> #include <vector> #include <map> #include <set> #include <cstring> #include <algorithm> #define M 500002 #define N 1000009 #define ll long long #define setIO(s) freopen(s".in","r",stdin) using namespace std; int n, tot, last; int prefix[N]; int ch[N][2], pre[N], len[N], pos[N], size[N]; set<int>se[N]; set<int>::iterator it, it2; char str[N]; void extend(int c) { int p = last, np = ++ tot; len[np] = len[p] + 1, last = np; for(;p && !ch[p][c]; p = pre[p]) ch[p][c] = np; if(!p) pre[np] = 1; else { int q = ch[p][c]; if(len[q] == len[p] + 1) pre[np] = q; else { int nq = ++ tot; len[nq] = len[p] + 1; pos[nq] = pos[q]; memcpy(ch[nq], ch[q], sizeof(ch[q])); pre[nq] = pre[q], pre[np] = pre[q] = nq; for(;p&&ch[p][c]==q;p=pre[p]) ch[p][c]=nq; } } } int main() { // setIO("input"); last = tot = 1; scanf("%d", &n); scanf("%s", str + 1); for(int i = n; i >= 1; -- i) { if(str[i] == '(') extend(0); else extend(1); pos[last] = i; } se[0 + M].insert(0); for(int i = 1; i <= n ; ++ i) { prefix[i] = prefix[i - 1]; if(str[i] == '(') ++ prefix[i]; else -- prefix[i]; size[i] = se[prefix[i] + M].size(); se[prefix[i] + M].insert(i); } ll ans = 0; for(int i = 2; i <= tot; ++ i) { int l = pos[i], pr = n, le = len[pre[i]]; it = se[prefix[l - 1] - 1 + M].lower_bound(l); if(it != se[prefix[l - 1] - 1 + M].end()) { pr = (*it) - 1; } if(l + le > pr) { continue; } int cur = prefix[l - 1]; int L = l + le, R = min(pr, l + len[i] - 1); it = se[cur + M].lower_bound(L); it2 = se[cur + M].lower_bound(R + 1); if(it != se[cur + M].end()) { -- it2; ans += size[*it2] - size[*it] + 1; } } printf("%lld", ans); return 0; }