FFT字符串匹配

用 FFT 解决(带通配符的)字符串匹配。

普通字符串匹配

设 $S$ 为模式串,$T$ 为文本串,$n=\lvert S \rvert,m=\lvert T \rvert$(字符串下标从 $0$ 开始)。

显然,两个字符串相同,当且仅当它们每一位都相同,这是一句废话。

我们联想到初中一年级的一句重要的话:**如果若干个非负数的和为 $0$,那么它们都为 $0$**。

和为 $0$ 正好对应了 每一位都相同,所以很容易得出这个 完全匹配函数(很重要!之后要考)

$$f(x)=\sum\limits_{i=0}^{n-1} (S_i-T_{x-n+i+1})^2$$

$f(x)$ 表示 $S$ 与 $T_{(x-n+1)\dots x}$ 的 匹配值

其中平方是为了保证每一项都是非负数。这样,只有每一项都为 $0$(即 $S_i=T_{x+i-1}$),$f(x)$ 才会为 $0$。

显然这个柿子不是很好化简,所以我们要对它进行一点点的变形。

什么柿子容易计算呢?当然是卷积,即:

$$f(x)=\sum\limits_{i+j=x} A_iB_j$$

然而之前那个柿子中,$i$ 和 $x-n+i+1$ 加起来显然不是一个定值。如何把这个 $i$ 消掉呢?

我们考虑把 $S$ 整个串反转,这样原来的 $i$ 就变成了 $n-i-1$。

于是变成:

$$f(x)=\sum\limits_{i=0}^{n-1} (S_{n-i+1}-T_{x-n+i+1})^2$$

两个下标相加:

$$(n-i+1)+(x-n+i+1)=x$$

好耶!消掉了!

于是那个柿子就可以写成:

$$f(x)=\sum\limits_{i+j=x}(S_i-T_j)^2$$

用完全平方公式拆开:

$$f(x)=\sum\limits_{i+j=x}S_i^2-2S_iT_j+T_j^2$$

显然第一项和第三项可以预处理得到,第二项就是 $S\cdot T$ 中 $x$ 次项的系数乘上 $-2$,只需要把 $S$ 和 $T$ 看成多项式,FFT 相乘即可。

复杂度 $O(n \log n)$。

带通配符的字符串匹配

通配符,即可以匹配任意字符的字符(当然也可以匹配通配符)。

显然这种问题 KMP 就无能为力了。然而,对于 FFT 做法,我们仍然可以用相似的思路解决。

我们发现,原来 完全匹配函数 为 $0$,当且仅当对应的两个字符相同。但是,现在有了通配符,条件应该改为满足以下两个中任意一个:

  • 对应两个字符相同
  • 对应两个字符中有任意一个是通配符

很容易想到设通配符为 $0$(当然其它任何字符都不能为 $0$)。完全匹配函数 就变成了:

$$f(x)=\sum\limits_{i=0}^{n-1} (S_i-T_{x-n+i+1})^2 \cdot S_iT_j$$

如果 $S_i=0$ 或 $T_i=0$,那么 $f(x)$ 仍然为 $0$。

老规矩,反转 $S$:

$$f(x)=\sum\limits_{i=0}^{n-1} (S_{n-i+1}-T_{x-n+i+1})^2 \cdot S_iT_j$$

化成卷积形式:

$$f(x)=\sum\limits_{i+j=x}(S_i-T_j)^2 \cdot S_iT_j$$

完全平方公式:

$$f(x)=\sum\limits_{i+j=x}S_i^3T_j-2S_i^2T_j^2+S_iT_j^3$$

看起来很复杂?但每一项还是卷积的形式,于是就用很多次 FFT 解决了。

复杂度 $O(n \log n)$。

例题

模板题

残缺的字符串

做法

就是模板题,按上面方法做就完了。

注意数组大小好好算,$eps$ 我取的 $1$ 过了。

常数有点大,吸氧能过。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const double pi = acos(-1);
int T_, case_;
int n1, n2, n = 1, len = 0;
string s, t;
int S[MAXN], T[MAXN];
struct Complex {
double r, i;
Complex() { r = i = 0; }
Complex(double r_, double i_) :r(r_), i(i_) {};
Complex operator+(const Complex& x)const { return Complex(r + x.r, i + x.i); };
Complex operator-(const Complex& x)const { return Complex(r - x.r, i - x.i); };
Complex operator*(const Complex& x)const { return Complex(r * x.r - i * x.i, i * x.r + r * x.i); };
};
int rev[MAXN];
void FFT(Complex* a, int f) {
rep(i, n)if (i < rev[i])swap(a[i], a[rev[i]]);
repp(d, log2(n)) {
int l = 1 << d;
Complex wn = Complex(cos(2.0 * pi / l), f * sin(2.0 * pi / l));
for (int i = 0;i < n;i += l) {
Complex w = Complex(1, 0);
rep(j, l / 2) {
Complex t = w * a[i + j + l / 2], u = a[i + j];
a[i + j] = u + t;
a[i + j + l / 2] = u - t;
w = w * wn;
}
}
}
if (f == -1)rep(i, n)a[i].r /= n;
}
Complex a[MAXN], b[MAXN], ans[MAXN];
vector<int>v;
void solve() {
cin >> n1 >> n2 >> s >> t;
reverse(s.begin(), s.end());
while (n < n1 + n2)n *= 2, len++;
rep(i, n)rev[i] = (rev[i / 2] / 2 | ((i & 1) << (len - 1)));
rep(i, n1)S[i] = (isalpha(s[i]) ? s[i] - 'a' + 1 : 0);
rep(i, n2)T[i] = (isalpha(t[i]) ? t[i] - 'a' + 1 : 0);
rep(i, n)a[i] = Complex(S[i] * S[i] * S[i], 0), b[i] = Complex(T[i], 0);
FFT(a, 1), FFT(b, 1);
rep(i, n)ans[i] = a[i] * b[i];
rep(i, n)a[i] = Complex(S[i] * S[i], 0), b[i] = Complex(T[i] * T[i], 0);
FFT(a, 1), FFT(b, 1);
rep(i, n)ans[i] = ans[i] - Complex(2, 0) * a[i] * b[i];
rep(i, n)a[i] = Complex(S[i], 0), b[i] = Complex(T[i] * T[i] * T[i], 0);
FFT(a, 1), FFT(b, 1);
rep(i, n)ans[i] = ans[i] + a[i] * b[i];
FFT(ans, -1);
forr(i, n1 - 1, n2 - 1)if (fabs(ans[i].r) <= 1)v.pb(i - n1 + 2);
if (v.empty()) {
cout << 0;
return;
}
cout << v.size() << endl;
rep(i, v.size())cout << v[i] << ' ';
}

练习题

CF528D Fuzzy Search

题目大意

有两个字符串 $S,T$,长度分别为 $n,m$,字符集为 ${A,C,G,T}$,和一个整数 $k$。

我们称 $S$ 在 $T$ 的第 $i$ 位出现了,当且仅当把 $S$ 的首字符和 $T$ 的第 $i$ 个字符对齐后,$S$ 中的每一个字符能够在 $T$ 中找到一个位置偏差不超过 $k$ 的相同字符。

即对于所有的 $j \in[1,n]$,都存在一个 $p \in [1,m]$ 使得 $|(i+j-1)-p| \leq k$ 且 $S_p=T_j$ 。

请求出 $S$ 在 $T$ 中出现的次数。

做法

首先把四种字母分开来考虑,最后取四个集合的交即可。

接下来假设只考虑字母 A,其余三个同理。

先把 $S$ 和 $T$ 除了 A 的部分都去掉(即空白),其余位置涂色。

接下来对 $T$ 处理:由于 $T$ 中的一个 A(假设在第 $i$ 位)可以影响到 $[i-k,i+k]$ 这些位置,所以把这些位置全都涂色。

接下来直接匹配即可。对于 $S$ 上有颜色的位置,$T$ 对应位置一定要有颜色;而对于 $S$ 上空白的位置,$T$ 对应位置可以有颜色,也可以是空白。

于是把 $T$ 的涂色部分看成 $1$,空白部分看成 $2$,$S$ 的涂色部分看成 $1$,空白部分看成 $0$(即通配符)。

复杂度 $O(n \log n)$。

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const double pi = acos(-1);
int T_, case_;
int n1, n2, n = 1, len = 0, k;
string s, t;
int S[MAXN], T[MAXN];
struct Complex {
double r, i;
Complex() { r = i = 0; }
Complex(double r_, double i_) :r(r_), i(i_) {};
Complex operator+(const Complex& x)const { return Complex(r + x.r, i + x.i); };
Complex operator-(const Complex& x)const { return Complex(r - x.r, i - x.i); };
Complex operator*(const Complex& x)const { return Complex(r * x.r - i * x.i, i * x.r + r * x.i); };
};
int rev[MAXN];
void FFT(Complex* a, int f) {
rep(i, n)if (i < rev[i])swap(a[i], a[rev[i]]);
repp(d, log2(n)) {
int l = 1 << d;
Complex wn = Complex(cos(2.0 * pi / l), f * sin(2.0 * pi / l));
for (int i = 0;i < n;i += l) {
Complex w = Complex(1, 0);
rep(j, l / 2) {
Complex t = w * a[i + j + l / 2], u = a[i + j];
a[i + j] = u + t;
a[i + j + l / 2] = u - t;
w = w * wn;
}
}
}
if (f == -1)rep(i, n)a[i].r /= n;
}
Complex a[MAXN], b[MAXN], ans[MAXN];
char C[] = { 'A','C','G','T' };
int d;
int sum[MAXN], SUM = 0;
void solve() {
cin >> n2 >> n1 >> k >> t >> s;
reverse(s.begin(), s.end());
while (n < n1 + n2)n *= 2, len++;
rep(i, n)rev[i] = (rev[i / 2] / 2 | ((i & 1) << (len - 1)));
rep(c, 4) {
rep(i, n1)S[i] = (s[i] == C[c]);
d = 0;
rep(i, min(k, n2))d += (t[i] == C[c]);
rep(i, n2) {
if (i + k < n2)d += (t[i + k] == C[c]);
T[i] = (d > 0 ? 1 : 2);
if (i - k >= 0)d -= (t[i - k] == C[c]);
}
rep(i, n)a[i] = Complex(S[i] * S[i] * S[i], 0), b[i] = Complex(T[i], 0);
FFT(a, 1), FFT(b, 1);
rep(i, n)ans[i] = a[i] * b[i];
rep(i, n)a[i] = Complex(S[i] * S[i], 0), b[i] = Complex(T[i] * T[i], 0);
FFT(a, 1), FFT(b, 1);
rep(i, n)ans[i] = ans[i] - Complex(2, 0) * a[i] * b[i];
rep(i, n)a[i] = Complex(S[i], 0), b[i] = Complex(T[i] * T[i] * T[i], 0);
FFT(a, 1), FFT(b, 1);
rep(i, n)ans[i] = ans[i] + a[i] * b[i];
FFT(ans, -1);
forr(i, n1 - 1, n2 - 1)if (fabs(ans[i].r) <= 1)sum[i - n1 + 1]++;
}
rep(i, n2 - n1 + 1)if (sum[i] == 4)SUM++;
cout << SUM;
}
作者

Rushroom

发布于

2023-01-06

更新于

2023-03-16

许可协议

CC BY-NC-SA 4.0

评论