[HEOI2015]最短不公共子串
题意:
给出两个字符串 \(A\) 和 \(B\),分别求出:
1.最短的串,是 \(A\) 的子串,不是 \(B\) 的子串。
2.最短的串,是 \(A\) 的子串,不是 \(B\) 的子序列。
3.最短的串,是 \(A\) 的子序列,不是 \(B\) 的子串。
4.最短的串,是 \(A\) 的子序列,不是 \(B\) 的子序列。
一道四合一题,如果分开做前两个好说,后两个得有点操作 ,不过由于我太菜做不得。而且暂时还不想学后两个点单独怎么做。
那我们把这四个看成一类问题。
考虑自动机:一个节点表示一个状态,每条边表示状态的转移。由于子串和子序列都能建出自动机来,那么我们只需要对两个自动机进行相同的转移,则 \(A\) 中有而 \(B\) 中没有的状态就是我们要找的答案。
子序列建成自动机这个应该都会,由于希望最长的匹配,肯定相同的字符越靠前越好。每个位置是一个点,向后每个字符往离它最近的点连边,时间复杂度与空间复杂度均为 \(O(n*\) 字符集大小\()\) 。
子串建成自动机可以选择前缀自动机也可选择后缀自动机,而这题数据范围是 \(2000\),因为复杂度瓶颈并不在这里,当然如果不想考虑空间的话可以写 \(sam\)。我们这里只需要用 \(trie\) 当自动机就可以了,把每个子串取出来建立一个 \(trie\),字符总长度 \(O(n^2)\),时间复杂度与空间复杂度均为 \(O(n^2*字符集大小)\)。看着大算算其实可过。
那么自动机就建好了,每次询问是什么,就把两个字符串建成相应的自动机就好了。那么现在考虑如何找答案。
因为一条边相当于加进去一个字符,我们进行 \(BFS\),遇到第一个 \(A\) 中存在而 \(B\) 中不存在的状态就把长度 \(+1\) 输出出来。如果遍历 \(A\) 的完整个自动机(自动机肯定不会有环的),还没有找到一个 \(B\) 中没有的状态,那 \(A\) 就太逊了,输出 \(-1\)。
考虑 \(BFS\) 复杂度:\(trie\) 进行 \(BFS\) 时,由于 \(trie\) 是像树一样的结构,每个点只会被遍历一次,复杂度不会超过 \(O(n^2)\) 。而当两个序列自动机进行遍历的时候,由于到达一个点可以通过不同的路径,如果无脑加点会被卡成指数级。我们需要用 \(vis\) 数组记录一下哪些状态到达过,由于序列自动机节点有 \(n\) 个,这样时间复杂度与空间复杂度也都是 \(O(n^2)\) 的,而且这个复杂度瓶颈不可避,前面自然也就不需要那么优了。
不过要注意只有两个自动机同时为序列自动机的时候才用 \(vis\),否则 \(trie\) 那么多点也开不下是吧~
#include <bits/stdc++.h>
using namespace std;
const int N=101010;
const int qwq=2000100;
const int inf=0x3f3f3f3f;
struct T {
char s[N];
int n;
int ch[qwq][26],cnt,now,rt;
void trie() {
memset(ch,0,sizeof(ch));
rt = now = cnt = 1;
for(int i=1;i<=n;i++) {
now = rt;
for(int j=i;j<=n;j++) {
int c = s[j] - 'a';
if(!ch[now][c]) ch[now][c] = ++cnt;
now = ch[now][c];
}
}
}
void lie() {
memset(ch,0,sizeof(ch));
cnt = n; rt = 0;
for(int i=1;i<=n;i++) {
for(int j=i-1;j>=0;j--) {
ch[j][ s[i]-'a' ] = i;
if(s[j]==s[i]) break;
}
}
}
} A,B;
struct E{ int x,y,le; };
queue <E> q;
bool vis[2333][2333];
int query() {
bool flag = 0;
if(A.rt==0 && B.rt==0) flag = 1;
q.push( (E){A.rt,B.rt,0} );
vis[A.rt][B.rt] = 1;
while(!q.empty()) {
E now = q.front(); q.pop();
for(int i=0;i<25;i++) {
int u = A.ch[now.x][i], v = B.ch[now.y][i];
if(flag) { if(vis[u][v]) continue; }
if(!u) continue;
if(u && !v) { while(!q.empty()) q.pop(); return now.le+1; }
q.push( (E){u,v,now.le+1} );
if(flag) vis[u][v] = 1;
}
}
return -1;
}
int main() {
int ans4;
scanf("%s%s",A.s+1,B.s+1);
A.n = strlen(A.s+1); B.n = strlen(B.s+1);
A.trie(); B.trie(); cout<<query()<<"\n";
B.lie(); cout<<query()<<"\n";
A.lie(); ans4 = query();
B.trie(); cout<<query()<<"\n"<<ans4;
return 0;
}
\(sam\) 的做法:
后缀自动机时间建立的复杂度是 \(O(n)\) 的,空间复杂度是 \(O(n*26)\) 的,这让人很舒服,但是由于到达一个点同样有多条路径,我们每次 \(BFS\) 都要记录 \(vis\)。
要注意 \(sam\) 数组要开两倍。
#include <bits/stdc++.h>
using namespace std;
const int N=101010;
const int qwq=2000100;
const int inf=0x3f3f3f3f;
struct T {
char s[2333];
int n;
int ch[4333][26],fa[4333],len[4333],cnt,now,rt;
void insert(int c) {
int u = now; now = ++cnt;
len[cnt] = len[u] + 1;
for(; u&&!ch[u][c]; u=fa[u]) ch[u][c] = cnt;
if(!u) fa[cnt] = 1;
else {
int zjt = ch[u][c];
if(len[zjt]==len[u]+1) fa[cnt] = zjt;
else {
int ob = cnt+1;
memcpy(ch[ob],ch[zjt],sizeof(ch[zjt]));
fa[ob] = fa[zjt]; len[ob] = len[u] + 1;
fa[cnt] = fa[zjt] = ob;
for(; u&&ch[u][c]==zjt; u=fa[u]) ch[u][c] = ob;
cnt++;
}
}
}
void sam() {
memset(ch,0,sizeof(ch));
now = cnt = rt = 1;
for(int i=1;i<=n;i++) insert(s[i]-'a');
}
void lie() {
memset(ch,0,sizeof(ch));
cnt = n; rt = 0;
for(int i=1;i<=n;i++) {
for(int j=i-1;j>=0;j--) {
ch[j][ s[i]-'a' ] = i;
if(s[j]==s[i]) break;
}
}
}
} A,B;
struct E{ int x,y,le; };
queue <E> q;
bool vis[4333][4333];
int query() {
memset(vis,0,sizeof(vis));
q.push( (E){A.rt,B.rt,0} );
vis[A.rt][B.rt] = 1;
while(!q.empty()) {
E now = q.front(); q.pop();
for(int i=0;i<25;i++) {
int u = A.ch[now.x][i], v = B.ch[now.y][i];
if(vis[u][v] || !u) continue;
if(u && !v) { while(!q.empty()) q.pop(); return now.le+1; }
q.push( (E){u,v,now.le+1} );
vis[u][v] = 1;
}
}
return -1;
}
int main() {
int ans4;
scanf("%s%s",A.s+1,B.s+1);
A.n = strlen(A.s+1); B.n = strlen(B.s+1);
A.sam(); B.sam(); cout<<query()<<"\n";
B.lie(); cout<<query()<<"\n";
A.lie(); ans4 = query();
B.sam(); cout<<query()<<"\n"<<ans4;
return 0;
}