字符串杂题
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;
}

浙公网安备 33010602011771号