出于对字符串的恐惧,我决定小小的总结一下FFT/NTT在字符串匹配方面的使用。
NOTICE:这是我在做题目时的总结,内容并非全部原创,可能存在对相关题解的借鉴。
$对于含通配符的匹配问题
- 描述:在某些题目中,我们可能会遇到可以匹配任何字符的通配符
通常,我们定义匹配函数 (P(x)):
[P(x)=sum_{i=0}^{m-1}{(a(i)-b(x-m+i+1))^2a(i)b(x-m+i+1)}
]
其中, (m) 为 (a) 串长度, (a(i)) 表示 (a) 串的第 (i) 项的字母对应的数值,其中特别定义通配符对应 (0)。
当 (P(x)=0) 时, (b) 串以 (x) 为结束位置的连续 (m) 位与 (a) 串匹配。
将 (a) 串反转并化简式子之后得到
[P(x)=sum_{i=0}^{m-1}{a^3(x-i)b(i)+b^3(i)a(x-i)-2a^2(x-i)b^2(i)}
]
发现此时可以用FFT优化计算。复杂度 (O(nlog_2n))。
有了这个算法,P4173 残缺的字符串 就不是问题了
参考代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
#define LL long long
using namespace std;
const int maxn = 2e6 + 10, P = 998244353, G = 3;
int n,m,rev[maxn],len;
int a1[maxn],a2[maxn],a3[maxn],b1[maxn],b2[maxn],b3[maxn],p[maxn];
int cnt,ans[maxn];
int Pow(LL x, LL y){
LL ans = 1;
while(y){
if(y & 1) ans *= x, ans %= P;
x *= x, x %= P;
y >>= 1;
} return ans;
}
void NTT(int *a, int opt){
for(int i = 0; i < n; ++ i) if(i < rev[i]) swap(a[i], a[rev[i]]);
for(int i = 1; i < n; i <<= 1){
int gn = Pow(G, (P - 1) / (i << 1));
if(opt < 0) gn = Pow(gn, P - 2);
for(int j = 0; j < n; j += (i << 1)){
int g = 1;
for(int k = 0; k < i; ++ k, g = (LL)g * gn % P){
int t1 = a[j + k], t2 = (LL)a[i + j + k] * g % P;
a[j + k] = ((LL)t1 + t2) % P; a[i + j + k] = ((LL)t1 - t2 + P) % P;
}
}
}
if(opt < 0){
int val = Pow(n, P - 2);
for(int i = 0; i < n; ++ i) a[i] = (LL)a[i] * val % P;
}
}
char s[maxn];
void get(int *a, int opt){
scanf("%s", s + 1);
int lens = strlen(s + 1);
for(int i = 1; i <= lens; ++ i)
if(s[i] != '*') a[i] = s[i] - 'a' + 1;
if(opt) reverse(a + 1, a + 1 + lens);
}
int main(){
scanf("%d%d", &n, &m); int N = n, M = m;
get(a1, 1); get(b1, 0);
for(int i = 1; i <= n; ++ i) a2[i] = (LL)a1[i] * a1[i] % P, a3[i] = (LL)a2[i] * a1[i] % P;
for(int i = 1; i <= m; ++ i) b2[i] = (LL)b1[i] * b1[i] % P, b3[i] = (LL)b2[i] * b1[i] % P;
m = n + m;
for(n = 1; n <= m; n <<= 1) len++;
for(int i = 0; i < n; ++ i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len - 1));
NTT(a1, 1); NTT(a2, 1); NTT(a3, 1);
NTT(b1, 1); NTT(b2, 1); NTT(b3, 1);
for(int i = 0; i < n; ++ i)
p[i] += ((LL)a1[i] * b3[i] % P - (LL)a2[i] * b2[i] * 2 % P + (LL)a3[i] * b1[i] % P + P) % P, p[i] %= P;
NTT(p, -1);
for(int i = N + 1; i <= M + 1; ++ i){
if(!p[i] && i - N <= M - N + 1) ans[++cnt] = i - N;
}
printf("%d
", cnt);
for(int i = 1; i <= cnt; ++ i) printf("%d ", ans[i]);
printf("
");
return 0;
}
$对于移位匹配问题
- 描述:有些题目可能允许在 (k) 个单位以内匹配,这一类题目一般会有一些其他的有用的性质。
我们可以以 CF528D Fuzzy Search 为例:
题意:给定原串 (S) 和匹配串 (T) (只由 (A,C,G,T) 组成),允许错开不超过 (k) 位匹配(即 (T) 中第 (i) 位字符可以与 (S) 中 ([i-k,i+k]) 中的相同字符匹配)。问 (T) 在 (S) 中出现次数。(|S|,|T|<200000)
题中有一个重要性质:不同字符只有 (4) 个,这就是提示我们对于每一个字符分开处理。即对于每一个字符处理出成功匹配的数量,最后加在一起。
具体地,就是将匹配的字符设为 (1),其他设为 (0),反转再卷积即可。
那如何处理 (k) 的问题呢?
事实上,我们可以把 (k) 理解为 (S) 串中的字符可以向周围扩撒 (k) 位,换句话说,就是只要某一字符与离它最近的匹配字符距离小于等于 (k),该位置就可以是 (1)。而最近距离可以从前往后与从后自前扫两边得出。
这里给出处理的核心代码:
void deal(char *s, int len, char c, int *a){
LL loc = -1e18;
for(int i = 0; i < n; ++ i){
if(s[i] == c) loc = i;
if((LL)i - loc <= k) a[i] = 1;
}
loc = 1e18;
for(int i = n - 1; i >= 0; -- i){
if(s[i] == c) loc = i;
if((LL)loc - i <= k) a[i] = 1;
}
}
void solve(char c){
memset(a, 0, sizeof(a)); memset(b, 0, sizeof(b));
deal(s, n, c, a);
for(int i = 0; i < m; ++ i) b[i] = (t[m - i - 1] == c);
NTT(a, lim, 1); NTT(b, lim, 1);
for(int i = 0; i < lim; ++ i) a[i] = (LL)a[i] * b[i] % P;
NTT(a, lim, -1);
for(int i = 0; i < lim; ++ i) ans[i] += a[i];
}
持续更新……