1.字符串哈希
P3370 【模板】字符串哈希
如题,给定
N
N
N 个字符串(第
i
i
i 个字符串长度为
M
i
M_i
Mi,字符串内包含数字、大小写字母,大小写敏感),请求出
N
N
N 个字符串中共有多少个不同的字符串。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e4 + 10;
ull h[N];
ull ha(string s) {
ull p = 131, res = 0;
int len = s.length();
for (int i = 0; i < len; ++i) {
res = res * p + s[i] - '0';
}
return res;
}
void solve() {
int n,ans=0;
cin >> n;
for (int i = 1; i <= n; ++i) {
string s;
cin >> s;
h[i] = ha(s);
}
sort(h + 1, h + n + 1);
for (int i = 1; i <= n; ++i) {
if (h[i] != h[i + 1]) ans++;
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
f ( s [ l . . r ] ) = f r ( s ) − f l − 1 ( s ) × b r − l + 1 f(s[l..r])=f_r(s)-f_{l-1}(s) \times b^{r-l+1} f(s[l..r])=fr(s)−fl−1(s)×br−l+1
2.Manacher算法
P3805 【模板】manacher 算法
给出一个只由小写英文字符
a
,
b
,
c
,
…
y
,
z
\texttt a,\texttt b,\texttt c,\ldots\texttt y,\texttt z
a,b,c,…y,z 组成的字符串
S
S
S ,求
S
S
S 中最长回文串的长度 。
O ( n ) O(n) O(n)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 22e6 + 10;
char b[N];
int R[N];
void solve() {
string s;
cin >> s;
int l = s.length(),len=0;
b[0] = '~';
for (int i = 0; i < l; ++i) {
b[++len] = '|';
b[++len] = s[i];
}
b[++len] = '|';
int mid = 0, r = 0;
for (int i = 1; i <= len; ++i) {
if (i > r) {
for (int j = 1; i - j > 0 && i + j <= len; ++j) {
if (b[i - j] == b[i + j]) R[i]++;
else break;
}
mid = i;
r = mid + R[i];
}
else {
R[i] = min(R[2 * mid - i], r - i);
for (int j = R[i] + 1; i - j > 0 && i + j <= len; ++j) {
if (b[i - j] == b[i + j]) R[i]++;
else break;
}
if (i + R[i] > r) {
mid = i;
r = mid + R[i];
}
}
}
int ans = 0;
for (int i = 1; i <= len; ++i) {
ans = max(ans, R[i]);
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
3.字典树
P8306 【模板】字典树
给定
n
n
n 个模式串
s
1
,
s
2
,
…
,
s
n
s_1, s_2, \dots, s_n
s1,s2,…,sn 和
q
q
q 次询问,每次询问给定一个文本串
t
i
t_i
ti,请回答
s
1
∼
s
n
s_1 \sim s_n
s1∼sn 中有多少个字符串
s
j
s_j
sj 满足
t
i
t_i
ti 是
s
j
s_j
sj 的前缀。
一个字符串
t
t
t 是
s
s
s 的前缀当且仅当从
s
s
s 的末尾删去若干个(可以为 0 个)连续的字符后与
t
t
t 相同。
O ( n ) O(n) O(n)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 3e6 + 10;
int trie[N][70], cnt[N], id;
void insert(string s) {
int p = 0;
int len = s.length();
int temp;
for (int i = 0; i < len; ++i) {
if (isdigit(s[i])) temp = s[i] - '0';
else if (isupper(s[i])) temp = s[i] - 'A' + 10;
else temp = s[i] - 'a' + 36;
if (trie[p][temp] == 0) trie[p][temp] = ++id;
p = trie[p][temp];
cnt[p]++;
}
}
int find(string s) {
int p = 0;
int len = s.length();
int temp;
for (int i = 0; i < len; ++i) {
if (isdigit(s[i])) temp = s[i] - '0';
else if (isupper(s[i])) temp = s[i] - 'A' + 10;
else temp = s[i] - 'a' + 36;
if (trie[p][temp] == 0) return 0;
p = trie[p][temp];
}
return cnt[p];
}
void solve() {
for (int i = 0; i <= id; ++i) {
cnt[i] = 0;
for (int j = 0; j <= 70; ++j) {
trie[i][j] = 0;
}
}
id = 0;
int n, q;
cin >> n >> q;;
for (int i = 1; i <= n; ++i) {
string s;
cin >> s;
insert(s);
}
for (int i = 1; i <= q; ++i) {
string s;
cin >> s;
cout << find(s) << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
//_t = 1;
cin >> _t;
while (_t--) {
solve();
}
return 0;
}
4.KMP算法
P3375 【模板】KMP 字符串匹配
给出两个字符串
s
1
s_1
s1 和
s
2
s_2
s2,若
s
1
s_1
s1 的区间
[
l
,
r
]
[l, r]
[l,r] 子串与
s
2
s_2
s2 完全相同,则称
s
2
s_2
s2 在
s
1
s_1
s1 中出现了,其出现位置为
l
l
l。
现在请你求出
s
2
s_2
s2 在
s
1
s_1
s1 中所有出现的位置。
定义一个字符串
s
s
s 的 border 为
s
s
s 的一个非
s
s
s 本身的子串
t
t
t,满足
t
t
t 既是
s
s
s 的前缀,又是
s
s
s 的后缀。
对于
s
2
s_2
s2,你还需要求出对于其每个前缀
s
′
s'
s′ 的最长 border
t
′
t'
t′ 的长度。
O ( m + n ) O(m+n) O(m+n)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e6 + 10;
string s1, s2;
int len1, len2,n[N],sum=0,ans[N];
void creatnext() {
int j = 0, k = -1;
n[0] = -1;
while (j <= len2 - 1) {
if (k == -1 || s2[k] == s2[j]) {
j++;
k++;
//if (p[k] != p[j]) n[j] = k;
//else n[j] = n[k];
n[j] = k;
}
else k = n[k];
}
}
void kmp() {
int i = 0, j = 0;
while (i <len1) {
if (j == -1 || s1[i] == s2[j]) {
j++;
i++;
}
else j = n[j];
if (j == len2) {
ans[++sum] = i - len2 + 1;
j = n[j];
}
}
}
void solve() {
cin >> s1 >> s2;
len1 = s1.length();
len2 = s2.length();
creatnext();
kmp();
for (int i = 1; i <= sum; ++i) cout << ans[i] << '\n';
for (int i = 1; i <= len2; ++i) {
if (n[i] == -1) n[i] = 0;
cout << n[i] << " ";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
最短循环节问题,在S中删除所有P
5.拓展KMP
P5410 【模板】扩展 KMP(Z 函数)
给定两个字符串
a
,
b
a,b
a,b,你要求出两个数组:
- b b b 的 z z z 函数数组 z z z,即 b b b 与 b b b 的每一个后缀的 LCP(最长公共前缀)长度。
- b b b 与 a a a 的每一个后缀的 LCP 长度数组 p p p。
对于一个长度为 n n n 的数组 a a a,设其权值为 xor i = 1 n i × ( a i + 1 ) \operatorname{xor}_{i=1}^n i \times (a_i + 1) xori=1ni×(ai+1)。
O ( n ) O(n) O(n)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 2e7 + 10;
int nxt[N], ex[N], lena, lenb;
string a, b;
void creatnext() {
int p = 0, k = 1, l;
nxt[0] = lenb;
while (p + 1 < lenb && b[p] == b[p + 1]) p++;
nxt[1] = p;
for (int i = 2; i < lenb; ++i) {
p = k + nxt[k] - 1;
l = nxt[i - k];
if (i + l <= p) nxt[i] = l;
else {
int j = max(0, p - i + 1);
while (i + j < lenb && b[j] == b[i + j]) j++;
nxt[i] = j;
k = i;
}
}
}
void exkmp() {
int p = 0, k = 0, l;
while (p < lena && p < lenb && a[p] == b[p]) p++;
ex[0] = p;
for (int i = 1; i < lena; ++i) {
p = k + ex[k] - 1; //最远已匹配位置
l = nxt[i - k];
if (i + l <= p) ex[i] = l;
else {
int j = max(0, p - i + 1);
while (i + j < lena && j < lenb && b[j] == a[i + j]) j++;
ex[i] = j;
k = i;
}
}
}
void solve() {
cin >> a >> b;
lena = a.length(), lenb = b.length();
creatnext();
ll ans = 0;
for (int i = 0; i < lenb; ++i) ans ^= 1ll * (i + 1) * (nxt[i] + 1);
cout << ans << '\n';
exkmp();
ans = 0;
for (int i = 0; i < lena; ++i) ans ^= 1ll * (i + 1) * (ex[i] + 1);
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
6.AC自动机
多模匹配问题
P3796 【模板】AC 自动机(加强版)
有
N
N
N 个由小写字母组成的模式串以及一个文本串
T
T
T。每个模式串可能会在文本串中出现多次。你需要找出哪些模式串在文本串
T
T
T 中出现的次数最多。
O ( k m + n m ) O(km+nm) O(km+nm)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e6 + 10;
int trie[N][26], cnt[N], fail[N], id = 0, maxnum,num[N];
string ss[N];
string s;
void insert(string s) {
int p = 0;
int len = s.size();
for (int i = 0; i < len; ++i) {
int temp = s[i] - 'a';
if (trie[p][temp] == 0) {
trie[p][temp] = ++id;
}
p = trie[p][temp];
}
cnt[p]++;
ss[p] = s;
}
void creatfail() {
queue<int> q;
int p = 0;
for (int i = 0; i <= 25; ++i) {
if (trie[p][i]) q.push(trie[p][i]);
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i <= 25; ++i) {
if (trie[u][i]) {
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else trie[u][i] = trie[fail[u]][i];
}
}
}
void query(string s) {
int p = 0;
int len = s.size();
for (int i = 0; i < len; ++i) {
int temp = s[i] - 'a';
p = trie[p][temp];
for (int j = p; j != 0 ; j = fail[j]) {
if (cnt[j]) {
num[j]++;
if (num[j] > maxnum) {
maxnum = num[j];
}
}
}
}
}
void solve() {
while (1) {
int n;
cin >> n;
if (n == 0) break;
for (int i = 0; i <= id; ++i) {
cnt[i] = fail[i] =num[i] = 0;
for (int j = 0; j <= 25; ++j) {
trie[i][j] = 0;
}
}
id =maxnum= 0;
for (int i = 0; i < n; ++i) {
cin >> s;
insert(s);
}
creatfail();
cin >> s;
query(s);
cout << maxnum << '\n';
for (int i = 1; i <= id; ++i) {
if (num[i] == maxnum) cout << ss[i] << '\n';
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin >> _t;
while (_t--) {
solve();
}
return 0;
}
7.AC自动机优化(dfs)
P5357 【模板】AC 自动机(二次加强版)
给你一个文本串
S
S
S 和
n
n
n 个模式串
T
1
∼
n
T_{1 \sim n}
T1∼n,请你分别求出每个模式串
T
i
T_i
Ti 在
S
S
S 中出现的次数。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e6 + 10;
int trie[N][26], cnt[N], fail[N], id = 0, num[N],MAP[N];
string s;
vector<vector<int> > v(N);
void insert(int cur,string s) {
int p = 0;
int len = s.size();
for (int i = 0; i < len; ++i) {
int temp = s[i] - 'a';
if (trie[p][temp] == 0) {
trie[p][temp] = ++id;
}
p = trie[p][temp];
}
MAP[cur] = p;
}
void creatfail() {
queue<int> q;
int p = 0;
for (int i = 0; i <= 25; ++i) {
if (trie[p][i]) q.push(trie[p][i]);
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i <= 25; ++i) {
if (trie[u][i]) {
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else trie[u][i] = trie[fail[u]][i];
}
}
}
void query(string s) {
int p = 0;
int len = s.size();
for (int i = 0; i < len; ++i) {
int temp = s[i] - 'a';
p = trie[p][temp];
num[p]++;
}
}
void build() {
for (int i = 1; i <= id; ++i) {
v[fail[i]].push_back(i);
}
}
void dfs(int u) {
for (auto i : v[u]) {
dfs(i);
num[u] += num[i];
}
}
void solve() {
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
cin >> s;
insert(i,s);
}
creatfail();
cin >> s;
query(s);
build();
dfs(0);
for (int i = 0; i <n; ++i) {
cout<<num[MAP[i]] << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin >> _t;
while (_t--) {
solve();
}
return 0;
}
//此外还有拓扑排序、树上差分等做法
8.回文自动机
P5496 【模板】回文自动机(PAM)
给定一个字符串
s
s
s。保证每个字符为小写字母。对于
s
s
s 的每个位置,请求出以该位置结尾的回文子串个数。
这个字符串被进行了加密,除了第一个字符,其他字符都需要通过上一个位置的答案来解密。
具体地,若第
i
(
i
≥
1
)
i(i\geq 1)
i(i≥1) 个位置的答案是
k
k
k,第
i
+
1
i+1
i+1 个字符读入时的
A
S
C
I
I
\rm ASCII
ASCII 码为
c
c
c,则第
i
+
1
i+1
i+1 个字符实际的
A
S
C
I
I
\rm ASCII
ASCII 码为
(
c
−
97
+
k
)
m
o
d
26
+
97
(c-97+k)\bmod 26+97
(c−97+k)mod26+97。所有字符在加密前后都为小写字母。
O ( n ) O(n) O(n)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 5e5 + 10;
int last,cnt=1,s[N];
struct node {
int len, fail, siz, son[26];
}tree[N];
int getfail(int x,int pos) {
while (pos-tree[x].len-1<0||s[pos - tree[x].len - 1] != s[pos]) x = tree[x].fail;
return x;
}
int pam(int pos) {
int fa = getfail(last, pos);
int now = tree[fa].son[s[pos]];
if (!now) {
now = ++cnt;
tree[now].fail = tree[getfail(tree[fa].fail, pos)].son[s[pos]];
tree[now].len = tree[fa].len + 2;
tree[now].siz = tree[tree[now].fail].siz + 1;
tree[fa].son[s[pos]] = now;
}
last = now;
return tree[now].siz;
}
void solve() {
string ss;
cin >> ss;
int len = ss.length(),k;
tree[0].fail = 1;
tree[1].len = -1;
for (int i = 0; i < len; ++i) {
if (!i) s[i]= ss[i]-'a';
else s[i] = (ss[i] - 'a' + k) % 26;
k = pam(i);
cout << k << " ";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
9.后缀数组
sa[]排第i位的后缀起始下标,rk[]起始下标为i的后缀的排名
sa[rk[i]]=rk[sa[i]]=i
P3809 【模板】后缀排序
读入一个长度为
n
n
n 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序(用 ASCII 数值比较)从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。位置编号为
1
1
1 到
n
n
n。
sort排序
构建
O
(
n
l
o
g
2
n
)
O(nlog^2n)
O(nlog2n)
查询
O
(
m
l
o
g
n
)
O(mlogn)
O(mlogn)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e6 + 10;
string s;
int sa[N], rk[N],temp[N] ,k,n;
bool cmpsa(int i, int j) {
if (rk[i] != rk[j]) { //先比较高位rk[i]和rk[j]
return rk[i] < rk[j];
}
else { //再比较低位rk[i+k]和rk[j+k]
int ri = i + k < n ? rk[i + k] : -1;
int rj = j + k < n ? rk[j + k] : -1;
return ri < rj;
}
}
void creatsa() {
for (int i = 0; i < n; ++i) {
rk[i] = s[i];
sa[i] = i;
}
for (k = 1; k < n; k *= 2) {
sort(sa, sa + n, cmpsa);
temp[sa[0]] = 0;
for (int i = 0; i < n - 1; ++i) temp[sa[i + 1]] = temp[sa[i]] + (cmpsa(sa[i], sa[i + 1] )? 1:0);
for (int i = 0; i < n; ++i) rk[i] = temp[i];
}
}
void solve() {
cin >> s;
n = s.length();
creatsa();
for (int i = 0; i < n; ++i) cout << sa[i]+1 << " ";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
基数排序
构建
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
查询
O
(
m
l
o
g
n
)
O(mlogn)
O(mlogn)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e6 + 10;
string s;
int c[N],sa[N], rk[N],tp[N] ,n,m=127;
void creatsa() {
for (int i = 0; i < n; ++i) {
rk[i] = s[i];
c[rk[i]]++;
}
for (int i = 1; i < m; ++i) c[i] += c[i - 1];
for (int i = n-1; i >=0; --i) sa[--c[rk[i]]] = i;
for (int k = 1; k < n; k *= 2) {
int num = 0;
for (int i = n - k; i < n; ++i) tp[num++] = i;
for (int i = 0; i < n; ++i) if (sa[i] >= k) tp[num++] = sa[i] - k;
for (int i = 0; i < m; ++i) c[i] = 0;
for (int i = 0; i < n; ++i) c[rk[tp[i]]]++;
for (int i = 1; i < m; ++i) c[i] += c[i - 1];
for (int i = n - 1; i >= 0; --i) sa[--c[rk[tp[i]]]] = tp[i];
num = 1;
swap(rk, tp);
rk[sa[0]] = 0;
for (int i = 1; i < n; ++i) {
num += tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i]+k] == tp[sa[i - 1]+k] ? 0 : 1;
rk[sa[i]] = num-1;
}
if (num == n) return;
m = num;
}
}
void solve() {
cin >> s;
n = s.length();
creatsa();
for (int i = 0; i < n; ++i) cout << sa[i] + 1 << " ";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
求height数组
sa[i-1]和sa[i](也就是排名相邻的两个后缀)的最长公共前缀长度
O ( n ) O(n) O(n)
void getheight() {
int j, k = 0;
for (int i = 0; i < n; ++i) rk[sa[i]] = i;
for (int i = 0; i < n; ++i) {
if (k) k--;
if (rk[i] == 0) continue;
int j = sa[rk[i] - 1];
while (s[i + k] == s[j + k]) k++;
height[rk[i]] = k;
}
}
- 在字符串S中查找子串T。
(2)在字符串S中找最长重复子串。先求 height数组,其中的最大值height[i]就是最长重复子串的长度。如果需要打印最长重复子串,它就是后缀子串sa[i-1]和sa[i]的最长公共前缀。 - 找字符串 S 1 S_1 S1和 S 2 S_2 S2的最长公共子串,以及扩展到求多个字符串的最长公共子串和最长公共子序列。用特殊字符连接字符串,查找最大的height[i],而且它对应的sa[i-1]和sa[i]分别属于被特殊字符分隔的前后两个字符串时就是解。
- S中有多少不同的子串。每一个后缀的长度减去其height之和。
- 最长公共前缀
给定一个字符串,询问某两个后缀的最长公共前缀。
对于给定的两个后缀,我们可以先求出它们的rk,分别设为 i 和 j (i <= j),那么ans = min{ height[i] , height[i+1] , … , height[j - 1] },也就相当于求RMQ问题,所以接下来的询问可以当做RMQ问题来采用合适的算法(例如st表)。 - 不可重叠最长重复子串
给定一个字符串,求最长重复子串,这两个子串不能重叠。
先二分答案,将问题转化为判定性问题。假设我们二分的长度为k,那么答案的两个串它们在SA中的之间的Height值都≥k,所以我们把连续一段Height≥k的后缀划分成一段,如果有某一段满足段中最大的SA值与最小值之差大于等于k,那么当前解就是可行的,因为满足这一条件的串一定不重叠。
注意:这种分段的思想在后缀数组相关问题中很常用 - 可重叠的k次最长重复子串
求可重叠的,出现k次的最长重复子串。
做法和上面的差不多,还是先二分,但是条件改变了,不是不重叠,而是出现至少k次。只需判断当前段内是否出现k个后缀即可。 - 公共子串的数量:
给定2个字符串A和B,以及一个整数k。目标是求出这两个字符串中公共子串的数量。
将A和B拼接在一起,中间用一个特殊字符间隔,然后对S求高度数组,利用高度数组来求解。我们从前向后遍历height数组,将属于A的后缀的 height 压入单调栈;由于两个字符串的后缀是取它们中间的最小值,所以我们应该维护单调递减栈,同时需要维护的是 sum代表当前 A 中相同的子串数量,也就是说如果当前后缀属于 B ,则直接加上sum即可。由于我们是顺序遍历的,只统计了A对B的贡献,再反过来统计一次B对A的贡献即可。 - 重复出现子串计数问题
求一个字符串中有多少个至少出现两次的子串
这是比较简单的SA题,设每个后缀rk为i,其最多能贡献Height[i]−Height[i−1]个不同重复子串。Ans=∑max(Height[i]−Height[i−1],0) - *字典序第K子串问题
给出一个字符串S,问该字符串的所有不同的子串中,按字典序排第K的字串,如果有多个则输出最左边的。其实就是不相同子串个数的扩展,算出每个后缀贡献的不同子串个数,在二分找出最终子串位置(必然是某个后缀的前缀)。 - 字符串不同种连续子串问题P4070 [SDOI2016] 生成魔咒
给定n个操作,每个操作在字符串S尾插入一个字符,求当前操作后共有多少不同种连续子串。
我们将整个字符串倒置过来,显然本质不同的子串个数不会变化,而每往前添加一个字符串,height 的变化是(1)的, 具体来说就是将字符串翻转后求一边SA,此时所得就是原串的前缀数组。然后在线段树维护一下。
10.后缀自动机
endpos为子串T在S中所有出现位置的右端点集合,endpos相等的称为等价类。同一个等价类中较短子串是较长子串的后缀;如果一个子串u是另一个子串v的后缀,则 e n d p o s ( v ) ⊂ e n d p o s ( u ) endpos(v)\subset endpos(u) endpos(v)⊂endpos(u)。后缀链连接两个等价类,父亲类属于儿子类即父亲类中子串是儿子类中子串后缀
P3804 【模板】后缀自动机(SAM)
给定一个只包含小写字母的字符串
S
S
S。
请你求出
S
S
S 的所有出现次数不为
1
1
1 的子串的出现次数乘上该子串长度的最大值。
O ( n ) O(n) O(n)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e6 + 10;
struct node {
int fa, len, son[26], num;//fa后缀链,len等价类最大子串长度
}tr[N * 2];
vector<vector<int> >e(N * 2);
int cnt = 0, last = 0; //last最后添加节点
ll ans = 0;
void insert(int x) {
tr[++cnt].len = tr[last].len + 1;
tr[cnt].num = 1;
int p = last, now = cnt;
while (p != -1 && !tr[p].son[x]) { //沿后缀链向上找
tr[p].son[x] = now;
p = tr[p].fa;
}
if (p == -1) tr[now].fa = 0; //未出现过
else { //到达q这个节点的字符串并不全属于一个等价类,属于q的长度不大于len(p)+1的串是新串的后缀,endpos增加,大于的串不是。
int q = tr[p].son[x];
if (tr[q].len == tr[p].len + 1) tr[now].fa = q; //到达q的所有串都是新串后缀
else {
tr[++cnt].len = tr[p].len + 1;
int nq = cnt;
memcpy(tr[nq].son, tr[q].son, sizeof(tr[q].son)); //复制节点
tr[nq].fa = tr[q].fa;
tr[now].fa = tr[q].fa = nq;
while (p != -1 && tr[p].son[x] == q) {
tr[p].son[x] = nq;
p = tr[p].fa;
}
}
}
last = now;
}
void dfs(int x) {
for (int i : e[x]) {
dfs(i);
tr[x].num += tr[i].num; //全部子树数量即为出现次数
}
if (tr[x].num != 1) ans = max(ans, 1ll * tr[x].num * tr[x].len);
}
void solve() {
string s;
cin >> s;
int n = s.length();
tr[0].fa = -1; //根指向-1
for (int i = 0; i < n; ++i) insert(s[i] - 'a');
for (int i = 1; i <= cnt; ++i) e[tr[i].fa].push_back(i); //建立后缀链parent树
dfs(0);
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
- S中有多少不同的子串:
对字符串 S 构造后缀自动机。每个 S 的子串都相当于自动机中的一些路径。因此不同子串的个数等于自动机中以 t 0 t_0 t0 为起点的不同路径的条数。考虑到 SAM 为有向无环图,不同路径的条数可以通过动态规划计算。即令 d v d_v dv 为从状态 v 开始的路径数量(包括长度为零的路径),则我们有如下递推方程: d v = 1 + ∑ w : ( v , w , c ) ∈ D A W G d w d_v =1+ \sum\limits_{w:(v,w,c)∈DAWG}d_w dv=1+w:(v,w,c)∈DAWG∑dw,即 d v d_v dv可以表示为所有 v 的转移的末端的和。所以不同子串的个数为 d t 0 − 1 d_{t_0}−1 dt0−1(因为要去掉空子串)。 - 所有不同子串的总长度:
我们需要考虑分两部分进行动态规划:不同子串的数量 d v d_v dv和它们的总长度 a n s v ans_v ansv。我们已经在上一题中介绍了如何计算 d v d_v dv。 a n s v ans_v ansv的值可以通过以下递推式计算: a n s v = ∑ w : ( v , w , c ) ∈ D A G d w + a n s w ans_v = \sum\limits_{w:(v,w,c)\in DAG}d_w+ans_w ansv=w:(v,w,c)∈DAG∑dw+answ
我们取每个邻接结点 w 的答案,并加上 d w d_w dw(因为从状态 v 出发的子串都增加了一个字符)。 - 字典序第k小子串P3975 [TJOI2015] 弦论:
本质不同才算不同:对于每个点u,维护一个 f u f_u fu 表示经过u的本质不同子串数量。这等价于在SAM的DAG上,u所能到达的点(如果u不是根,那么也算)的数量(等价于除根之外的点点权为1,所能到达的点权和),可以dfs或按拓扑序递推求得。
位置不同即不同:对于每个点u,维护一个 f u f_u fu 表示经过u的位置不同子串数量。显然经过它的每个串s的贡献是endpos(s)的大小(这就是出现的s次数)。而根到u表示的串的endpos大小就是parent树上u的子树中的前缀点数量。先dfs求出siz[u]表示u的子树大小,并将它作为点权。
考虑查询:(假设现在的节点是u,要查第k小子串)先将k减去u的点权。按字典序递增考虑u能直接到达的点v,如果 k ≤ f v k\leq f_v k≤fv ,则输出v表示的字符,并递归查询(v,k),否则k减去 f v f_v fv 。注意判断-1的情况。 - 最小循环移位:
容易发现字符串 S+S 包含字符串 S 的所有循环移位作为子串。所以问题简化为在 S+S 对应的后缀自动机上寻找最小的长度为 ∣S∣ 的路径,这可以通过平凡的方法做到:我们从初始状态开始,贪心地访问最小的字符即可。 - 两个字符串的最长公共子串
我们对字符串 S 构造后缀自动机。对于每个字符串 T 中的位置,我们想要找到这个位置结束的 S 和 T 的最长公共子串的长度。为了达到这一目的,我们使用两个变量, 当前状态 v 和 当前长度 l 。一开始 v=0 且 l=0 ,即匹配为空串。
现在我们来描述如何添加一个字符 T[i] 并为其重新计算答案:如果存在一个从 v 到字符 T[i] 的转移,我们只需要转移并让 l 自增一。如果不存在这样的转移,我们需要缩短当前匹配的部分,这意味着我们需要按照后缀链接进行转移:v=tr[v].fa。与此同时,需要缩短当前长度,将 l 赋值为 tr[v].len ,因为经过这个后缀链接后我们到达的状态所对应的最长字符串是一个子串。如果仍然没有使用这一字符的转移,我们继续重复经过后缀链接并减小 l ,直到我们找到一个转移或到达虚拟状态 −1(这意味着字符 T[i] 根本没有在 S 中出现过,所以我们设置 v=l=0 )。问题的答案就是所有 l 的最大值。 - 多个字符串间的最长公共子串
AcWing 2811. 最长公共子串
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e4 + 10;
struct node {
int fa, len, son[26];
}tr[N * 2];
vector<vector<int> >e(N * 2);
int cnt = 0, last = 0;
ll ans[N * 2], mx[N * 2];
void insert(int x) {
tr[++cnt].len = tr[last].len + 1;
int p = last, now = cnt;
while (p != -1 && !tr[p].son[x]) {
tr[p].son[x] = now;
p = tr[p].fa;
}
if (p == -1) tr[now].fa = 0;
else {
int q = tr[p].son[x];
if (tr[q].len == tr[p].len + 1) tr[now].fa = q;
else {
tr[++cnt].len = tr[p].len + 1;
int nq = cnt;
memcpy(tr[nq].son, tr[q].son, sizeof(tr[q].son));
tr[nq].fa = tr[q].fa;
tr[now].fa = tr[q].fa = nq;
while (p != -1 && tr[p].son[x] == q) {
tr[p].son[x] = nq;
p = tr[p].fa;
}
}
}
last = now;
}
void dfs(int x) {
for (int i : e[x]) {
dfs(i);
mx[x] = max(mx[x], mx[i]);
}
}
void solve() {
int n;
cin >> n;
string s;
cin >> s;
int len = s.length();
tr[0].fa = -1;
for (int i = 0; i < len; ++i) insert(s[i] - 'a');
for (int i = 1; i <= cnt; ++i) ans[i] = tr[i].len, e[tr[i].fa].push_back(i);
for (int i = 1; i < n; ++i) {
cin >> s;
len = s.length();
memset(mx, 0, sizeof(mx));
ll p = 0, t = 0;
for (int j = 0; j < len; ++j) {
while (p && !tr[p].son[s[j] - 'a']) p = tr[p].fa, t = tr[p].len;
if (tr[p].son[s[j] - 'a']) {
p = tr[p].son[s[j] - 'a'];
t++;
mx[p] = max(mx[p], t);
}
}
dfs(0);
for (int i = 1; i <= cnt; ++i) ans[i] = min(ans[i], mx[i]);
}
ll res = 0;
for (int i = 1; i <= cnt; ++i) res = max(res, ans[i]);
cout << res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
- 在S中查找模式串P、模式串P在S中的出现次数、P在S中第1次出现的位置、P在S中出现的所有位置等。
11.广义后缀自动机
P6139 【模板】广义后缀自动机(广义 SAM)
给定
n
n
n 个由小写字母组成的字符串
s
1
,
s
2
…
s
n
s_1,s_2\ldots s_n
s1,s2…sn,求本质不同的子串个数。(不包含空串)
进一步,设
Q
Q
Q 为能接受
s
1
,
s
2
,
…
,
s
n
s_1,s_2,\dots,s_n
s1,s2,…,sn 的所有后缀的最小 DFA,请你输出
Q
Q
Q 的点数。(如果你无法理解这句话,可以认为就是输出
s
1
,
s
2
,
…
,
s
n
s_1,s_2,\dots,s_n
s1,s2,…,sn 建成的“广义后缀自动机”的点数)。
离线
O ( k m ) O(km) O(km)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e6 + 10;
struct node {
int fa, len, son[26];
}tree[2 * N];
int cnt = 0, last = 0, trie[N][26], fa[N], val[N], pos[N];
void trie_insert(string s) {
int p = 0, len = s.length();
for (int i = 0; i < len; ++i) {
int x = s[i] - 'a';
if (!trie[p][x]) {
trie[p][x] = ++cnt;
fa[cnt] = p;
val[cnt] = x;
}
p = trie[p][x];
}
}
int sam_insert(int x) {
/*if (tree[last].son[x]) {
int p = last, q = tree[last].son[x];
tree[++cnt].len = tree[p].len + 1;
int nq = cnt;
memcpy(tree[nq].son, tree[q].son, sizeof(tree[q].son));
tree[nq].fa = tree[q].fa;
tree[q].fa = nq;
while (p != -1 && tree[p].son[x] == q) {
tree[p].son[x] = nq;
p = tree[p].fa;
}
return nq;
}
else {*/
tree[++cnt].len = tree[last].len + 1;
int p = last, now = cnt;
while (p != -1 && !tree[p].son[x]) {
tree[p].son[x] = now;
p = tree[p].fa;
}
if (p == -1) tree[now].fa = 0;
else {
int q = tree[p].son[x];
if (tree[q].len == tree[p].len + 1) tree[now].fa = q;
else {
tree[++cnt].len = tree[p].len + 1;
int nq = cnt;
memcpy(tree[nq].son, tree[q].son, sizeof(tree[q].son));
tree[nq].fa = tree[q].fa;
tree[now].fa = tree[q].fa = nq;
while (p != -1 && tree[p].son[x] == q) {
tree[p].son[x] = nq;
p = tree[p].fa;
}
}
}
return now;
//}
}
void dfs(int x) {
for (int i = 0; i < 26; ++i) {
if (trie[x][i]) {
int u = trie[x][i];
last = pos[fa[u]];
pos[u] = sam_insert(val[u]);
dfs(trie[x][i]);
}
}
}
void bfs() {
queue<int> q;
for (int i = 0; i < 26; ++i) if (trie[0][i]) q.push(trie[0][i]);
while (!q.empty()) {
int u = q.front();
q.pop();
last = pos[fa[u]];
pos[u] = sam_insert(val[u]);
for (int i = 0; i < 26; ++i) if (trie[u][i]) q.push(trie[u][i]);
}
}
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
string s;
cin >> s;
trie_insert(s);
}
cnt = 0;
tree[0].fa = -1;
//dfs(0);
bfs();
ll ans = 0;
for (int i = 1; i <= cnt; ++i) ans += tree[i].len - tree[tree[i].fa].len;
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
在线
O ( k m ) O(km) O(km)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e6 + 10;
struct node {
int fa, len, son[26];
}tree[2 * N];
int cnt = 0, last = 0;
void insert(int x) {
if (tree[last].son[x]) {
int p = last, q = tree[last].son[x];
if (tree[q].len == tree[p].len + 1) last = q;
else {
tree[++cnt].len = tree[p].len + 1;
int nq = cnt;
memcpy(tree[nq].son, tree[q].son, sizeof(tree[q].son));
tree[nq].fa = tree[q].fa;
tree[q].fa = nq;
while (p != -1 && tree[p].son[x] == q) {
tree[p].son[x] = nq;
p = tree[p].fa;
}
last = nq;
}
}
else {
tree[++cnt].len = tree[last].len + 1;
int p = last, now = cnt;
while (p != -1 && !tree[p].son[x]) {
tree[p].son[x] = now;
p = tree[p].fa;
}
if (p == -1) tree[now].fa = 0;
else {
int q = tree[p].son[x];
if (tree[q].len == tree[p].len + 1) tree[now].fa = q;
else {
tree[++cnt].len = tree[p].len + 1;
int nq = cnt;
memcpy(tree[nq].son, tree[q].son, sizeof(tree[q].son));
tree[nq].fa = tree[q].fa;
tree[now].fa = tree[q].fa = nq;
while (p != -1 && tree[p].son[x] == q) {
tree[p].son[x] = nq;
p = tree[p].fa;
}
}
}
last = now;
}
}
void solve() {
int n;
cin >> n;
tree[0].fa = -1;
for (int i = 1; i <= n; ++i) {
string s;
cin >> s;
int len = s.length();
last = 0;
for (int j = 0; j < len; ++j) insert(s[j] - 'a');
}
ll ans = 0;
for (int i = 1; i <= cnt; ++i) ans += tree[i].len - tree[tree[i].fa].len;
cout << ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
- 查找子串的出现次数。
对于子串 t, 出现次数其实就是结束位置的数量,也就是 ∣endpos(t)∣。parent树中子树大小。 - 查找所有不同子串的出现次数。
计算 ∣endpos(t)∣ 即可,同一个 endpos 包含的子串数量可用 len 计算出来。 - 查找所有不同子串的总长。
同一个 endpos 包含的子串总长也可以用 len 计算出来。 - 查找两个串的最长公共后缀/前缀。
后缀其实就是求 fail 树上的 LCA,前缀对反串建 SAM 即可。 - 求两个字符串的相同子串数量。
两个串的 |endpos|分开计算,开一个二维数组,用 siz[x][id] 表示节点 x在串 id上的 |endpos|大小。则答案为: ∑ s i z [ i ] [ 0 ] × s i z [ i ] [ 1 ] × ( t r [ i ] . l e n − t r [ t r [ i ] . f a ] . l e n ) \sum siz[i][0]\times siz[i][1]\times (tr[i].len−tr[tr[i].fa].len) ∑siz[i][0]×siz[i][1]×(tr[i].len−tr[tr[i].fa].len) - 线段树合并维护 siz,给出主串 S以及 m 个字符串 T[1…m]。有若干次询问,每次查询 S的子串 S[pl…pr]在 T[l…r]中的哪个 串 Ti 里的出现次数最多,输出 i以及出现次数,有多解则取最靠前的那一个。
先把所有字符串都插入到广义 SAM中,对于每个节点开一颗下标为 [1,m]的动态开点线段树维护 siz (注意插入 S 时就不要在线段树上进行修改操作了)。由于 siz的维护是统计子树和,所以插入结束后 要在 parent树上跑一下线段树合并。
查询时先在 parent 树上倍增找到包含子串 S[pl,pr]的等价类状态节点,然后在该点的线段树上查询区 间 [l,r]中的最大值,顺便维护下最大值所处位置即可。 - 树上本质不同路径数,给出一颗叶子结点不超过 20 个的无根树,每个节点上都有一个不超过 10 的数字,求树上本质不同的路径个数(两条路径相同定义为:其路径上所有节点上的数字依次相连组成的字符串相同)。
一颗无根树上任意一条路径必定可以在以某个叶节点为根时,变成一条从上到下的路径(利于广义 SAM的使用)
注意到题目中说叶节点不超过 20 个,这意味着暴力枚举每一个叶节点作为根节点遍历整棵树。将一共 c n t l e a f cnt_{leaf} cntleaf颗树中的所有前缀串都抽出来建立广义 SAM,然后直接求本质不同的子串个数。 其中前缀串定义为从根节点(无根树的某个叶子结点)到任意一个节点的路径所构成的字符串。 - 给出主串 S 和 n 个询问串。对于每个询问串,求出它的所有循环同构在主串中的出现次数总和。
12.最小表示法
P1368 【模板】最小表示法
小敏和小燕是一对好朋友。
他们正在玩一种神奇的游戏,叫 Minecraft。
他们现在要做一个由方块构成的长条工艺品。但是方块现在是乱的,而且由于机器的要求,他们只能做到把这个工艺品最左边的方块放到最右边。
他们想,在仅这一个操作下,最漂亮的工艺品能多漂亮。
两个工艺品美观的比较方法是,从头开始比较,如果第
i
i
i 个位置上方块不一样那么谁的瑕疵度小,那么谁就更漂亮,如果一样那么继续比较第
i
+
1
i+1
i+1 个方块。如果全都一样,那么这两个工艺品就一样漂亮。
O ( n ) O(n) O(n)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 3e5 + 10;
int a[N << 1];
void solve() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
a[i + n] = a[i];
}
int i = 1, j = 2, k;
while (i <= n && j <= n) {
k = 0;
while (k < n && a[i + k] == a[j + k]) k++;
if (k == n) break;
if (a[i + k] > a[j + k]) i += k + 1;
else j += k + 1;
if (i == j) j++;
}
k = min(i, j);
for (int i = 1; i <= n; ++i) cout << a[k + i - 1] << " ";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t;
_t = 1;
//cin>>_t;
while (_t--) {
solve();
}
return 0;
}
13.子序列自动机
P5826 【模板】子序列自动机
给定一个长度为
n
n
n 的正整数序列
a
a
a ,有
q
q
q 次询问,第
i
i
i 次询问给定一个长度为
L
i
L_i
Li 的序列
b
i
b_i
bi,请你判断
b
i
b_i
bi 是不是
a
a
a 的子序列。序列
a
a
a 和所有
b
i
b_i
bi 中的元素都不大于一个给定的正整数
m
m
m。
m是字符集大小,∣s∣=n,nxt[i][j] 表示 i 以后的第一个字符 j 的位置,0为根节点,整个图是一个DAG
构建
O
(
n
m
)
O(nm)
O(nm)
for(ll i=n;i>=1;--i){
for(ll j=1;j<=m;++j) nxt[i-1][j]=nxt[i][j];
nxt[i-1][s[i]]=i;
}
i 后面第一个 j 的位置,只需要在所有是 j 的下标中二分就行了!开 m 个 vector 存下这些下标,每次二分。
O
(
n
+
(
∑
l
)
log
m
)
O(n+(\sum l)\log m)
O(n+(∑l)logm)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e6 + 10;
int t, n, q, m, a[N], b[N];
void solve() {
cin >> t >> n >> q >> m;
vector<vector<int> >v(N);
for (int i = 1; i <= n; ++i) cin >> a[i], v[a[i]].push_back(i);
for (int i = 1; i <= q; ++i) {
int len;
cin >> len;
for (int j = 1; j <= len; ++j) cin >> b[j];
int now = 0, f = 1;
for (int j = 1; j <= len; ++j) {
auto p = upper_bound(v[b[j]].begin(), v[b[j]].end(), now);
if (p == v[b[j]].end()) {
f = 0;
break;
}
now = *p;
}
if (f) cout << "Yes\n";
else cout << "No\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int _t = 1;
//_t = read();
while (_t--) {
solve();
}
return 0;
}
当字符集较大时,可套用可持久化。注意到构造自动机时,第 i 位与第 i−1 位只有
a
i
a_i
ai 一项不一样,第 i−1 位的转移可以看做第 i 位的转移的基础上修改了一个位置,因此我们可以从后向前使用可持久化线段树来维护每个位置的转移数组。
O
(
(
n
+
∑
l
)
log
m
)
O((n+\sum l)\log m)
O((n+∑l)logm)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
struct Tree {
Tree* ls, * rs;
int l, r, v;
Tree(const int L, const int R) : l(L), r(R), v(-1) {
if (l != r) {
int mid = (l + r) >> 1;
ls = new Tree(l, mid);
rs = new Tree(mid + 1, r);
}
}
Tree(Tree* pre, const int P, const int V) : l(pre->l), r(pre->r), v(0) {
if (l == r) v = V;
else {
if (pre->ls->r >= P) {
rs = pre->rs;
ls = new Tree(pre->ls, P, V);
}
else {
ls = pre->ls;
rs = new Tree(pre->rs, P, V);
}
}
}
int query(const int x) {
if (this->l == this->r) return this->v;
else return (this->ls->r >= x) ? this->ls->query(x) : this->rs->query(x);
}
};
Tree* rot[N];
int tp, n, q, m, a[N];
int main() {
cin >> tp >> n >> q >> m;
rot[n] = new Tree(1, m);
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = n; i; --i) rot[i - 1] = new Tree(rot[i], a[i], i);
for (int i = 1; i <= q; ++q) {
int len, x, pos = 0;
cin >> len;
while ((len--) && (pos != -1)) {
cin >> x;
if ((pos = rot[pos]->query(x)) == -1) {
while (len--) cin >> x;
break;
}
}
if (pos != -1) cout << "Yes\n";
else cout << "No\n";
}
return 0;
}
相关例题:字符串K小子序列,可持久化序列自动机,维护节点大小。一步一步(从首到尾)走,有序确定。
应用:
- 判断是否是原字符串的子序列
构造出了nxt后,从根跑一遍就好了。 - 求子序列个数
从根跑,记忆化搜索,f[x]为点x为首的子序列个数, f [ y ] = ( ∑ x ∈ y ′ s o n f [ x ] ) + 1 f[y]=(\sum\limits_{x∈y′son}f[x])+1 f[y]=(x∈y′son∑f[x])+1 - 求两串的公共子序列个数
两串都构造一下,之间跑就好了。
ll dfs(ll x,ll y){
if(f[x][y]) return f[x][y];
for(ll i=1;i<=a;++i)
if(nxt1[x][i]&&nxt2[y][i])
f[x][y]+=dfs(nxt1[x][i],nxt2[y][i]);
return ++f[x][y];
}
- 求字符串的回文子序列个数
原串与反串都建一遍
⟶1 2 3 4 5 6 7 8 9 10
10 9 8 7 6 5 4 3 2 1⟵
就相当于从左右端点这样跑。求的时候显然x+y≤n+1这个序列才是合法的。x+y=n+1时就是会合了一样,在之后的遍历过程会++f[x][y],所以暂时不统计。但是其他情况我们都是匹配的两个字符,也就是只会统计abba,而统计不了aba,所以在过程中++f[x][y]
ll dfs(ll x,ll y){
if(f[x][y]) return f[x][y];
for(ll i=1;i<=a;++i)
if(nxt1[x][i]&&nxt2[y][i]){
if(nxt1[x][i]+nxt2[y][i]>n+1) continue;
if(nxt1[x][i]+nxt2[y][i]<n+1) f[x][y]++;
f[x][y]=(f[x][y]+dfs(nxt1[x][i],nxt2[y][i]))%mod;
}
return ++f[x][y];
}
- 求一个A,B的最长公共子序列S,使得C是S的子序列
还是同样的dfs(x,y,z),表示一匹配到C的z位。改变一下C的构建方法
for(ll i=1;i<=a;++i) nxt[n][i]=n;
for(ll i=0;i<n;++i){
for(ll j=1;j<=a;++j) nxt[i][j]=i;
nxt[i][c[i+1]]=i+1;
}