[HAOI2016]找相同字符
题目描述
给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。两个方案不同当且仅当这两个子串中有一个位置不同。
输入格式
两行,两个字符串 s 1 , s 2 s_1,s_2 s1,s2,长度分别为 n 1 , n 2 n_1,n_2 n1,n2。
解法1:后缀数组SA
该题和这道题很像,可以先去学习一下该题的解法我的博客。
我们注意到一个性质:对于两个后缀子串,它们的最长公共前缀就是我们可以取的子串的开头位置在后缀的起点的个数。比如
a
a
b
b
和
a
a
a
a
aabb和aaaa
aabb和aaaa的最长公共前缀是
a
a
aa
aa,那么我们可以取的相同子串就是
a
和
a
a
a和aa
a和aa
那么这题有两个串,我们参考那道题的解法,利用容斥,先把两个串合并求一下答案,再分别对两个串求一下,减去它们即可。
int height[N];
int n, sa[N], rk[N << 1], oldrk[N << 1], id[N], cnt[N], px[N];
//rk[i]编号为i的后缀的排名,sa[i]第i小的后缀的排名,px[i] = rk[id[i]];
bool cmp(int x, int y, int w) {
return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}
void init() {
memset(height, 0, sizeof height);
memset(cnt, 0, sizeof cnt);
memset(rk, 0, sizeof rk);
memset(cnt, 0, sizeof cnt);
memset(id, 0, sizeof id);
}
LL ans;
void solve(string s, int flag) {
init();
n = s.size() - 1;
int m = max(n, 300);
for(int i = 1; i <= n; i++) {
rk[i] = s[i] - '0', sa[i] = i;
}
for(int i = 1; i <= n; i++) ++cnt[rk[i]];
for(int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for(int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
int p = 0, i;
for(int w = 1; ; w <<= 1, m = p) {
for(p = 0, i = n; i > n - w; i--) id[++p] = i;
for(int i = 1; i <= n; i++) {
if(sa[i] > w) {
id[++p] = sa[i] - w;
}
}
memset(cnt, 0, sizeof cnt);
for(int i = 1; i <= n; i++) ++cnt[px[i] = rk[id[i]]];
for(int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for(int i = n; i >= 1; i--) sa[cnt[px[i]]--] = id[i];
memcpy(oldrk, rk, sizeof rk);
for(p = 0, i = 1; i <= n; i++)
rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
if(p == n) {
for(int i = 1; i <= n; i++) sa[rk[i]] = i;
break;
}
}
for(int i = 1, k = 0; i <= n; i++) {
if(k) --k;
while(s[i + k] == s[sa[rk[i] - 1] + k]) k++;
height[rk[i]] = k;
}
stack<int> st;
vector<LL> f(n + 1);
for(int i = 2; i <= n; i++) {
while(st.size() && height[st.top()] >= height[i]) st.pop();
if(st.size() == 0) f[i] = height[i] * 1LL * (i - 1);
else {
f[i] = f[st.top()] + 1LL * height[i] * (i - st.top());
}
st.push(i);
ans = ans + flag * 1LL * f[i];
}
}
signed main() {
#ifdef JANGYI
freopen("input.in", "r", stdin);
freopen("out.out", "w", stdout);
// auto now = clock();
#endif
string a, b;
cin >> a >> b;
solve(' ' + a + "~" + b, 1);
solve(' ' + a, -1);
solve(' ' + b, -1);
cout << ans << '\n';
// #ifdef JANGYI
// cout << "================================" << endl;
// cout << "Program run for " << (clock() - now) / (double)CLOCKS_PER_SEC * 1000 << " ms." << endl;
// #endif
return 0;
}
解法二:后缀自动机SAM
我们先对两个串建自动机,我们知道在
p
a
r
e
n
t
parent
parent树中从初始状态到每个点的路径都是一个子串,那么我们做一遍
d
f
s
dfs
dfs求每个子串出现的次数。
对于在两个SAM上通过相同转移而到达的两个结点(即使它们编号不同,且不在一个SAM上),它们表示的是同一子串,那么计算即可。
char s1[N], s2[N];
struct Suffix_Automation {
int len[M], link[M], sz[M];
int h[M], ne[M], e[M], idx;
int ch[M][26];
int siz, last;
inline void init() {
memset(len, 0, sizeof len);
memset(h, -1, sizeof h);
memset(link, 0, sizeof link);
memset(ch, 0, sizeof ch);
siz = last = 0;
link[0] = -1;
}
inline void extend(char *str) {
int n = strlen(str);
for(int i = 0; i < n; i++) {
int cur = ++siz, p = last, c = str[i] - 'a';
sz[cur] = 1;
len[cur] = len[p] + 1;//初始化新节点
while(p != -1 && !ch[p][c]) { //跳link添加边
ch[p][c] = cur;
p = link[p];
}
if(p == -1) link[cur] = 0; //跳到了初始节点
else {
int q = ch[p][c]; //找到出边
if(len[q] == len[p] + 1) link[cur] = q;
else { //分裂节点
int copy = ++ siz;
len[copy] = len[p] + 1;
link[copy] = link[q];
for(int i = 0; i < 26; i++) ch[copy][i] = ch[q][i];
while(p != -1 && ch[p][c] == q) {
ch[p][c] = copy;
p = link[p];
}
link[q] = link[cur] = copy;
}
}
last = cur;
}
}
inline void add(int a, int b) {
e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
}
inline void dfs(int u) {
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
dfs(j);
sz[u] += sz[j];
}
}
inline void add_tree() {
for(int i = 1; i <= siz; i++) add(link[i], i);
dfs(0);
}
}SAM1, SAM2;
LL ans = 0;
void dfs(int x, int y) {
for(int i = 0; i < 26; i++) {
if(SAM1.ch[x][i] && SAM2.ch[y][i])
dfs(SAM1.ch[x][i], SAM2.ch[y][i]);
}
if(x != 0 && y != 0) ans += SAM1.sz[x] * 1LL * SAM2.sz[y];
}
signed main() {
#ifdef JANGYI
freopen("input.in", "r", stdin);
freopen("out.out", "w", stdout);
// auto now = clock();
#endif
scanf("%s%s", s1, s2);
SAM1.init(); SAM2.init();
SAM1.extend(s1); SAM2.extend(s2);
SAM1.add_tree(); SAM2.add_tree();
dfs(0, 0);
cout << ans << '\n';
// #ifdef JANGYI
// cout << "================================" << endl;
// cout << "Program run for " << (clock() - now) / (double)CLOCKS_PER_SEC * 1000 << " ms." << endl;
// #endif
return 0;
}