有些字符串匹配问题可以推出卷积公式,并通过FFT加速计算过程,下面通过几个简单的例子来了解一下。
卷积是什么
首先给出离散卷积的公式:
f
(
x
)
=
∑
i
=
−
∞
∞
g
(
i
)
h
(
n
−
i
)
f(x) = \sum_{i=-\infty}^{\infty}g(i)h(n-i)
f(x)=i=−∞∑∞g(i)h(n−i)
FFT虽说是求多项式相乘,但实质上就是对于所有可能取到的x求f(x)。怎么理解呢,比方说,
x
k
{x^k}
xk这个多项式系数,无非就是求
x
i
x^i
xi与
x
j
x^j
xj系数乘积之和,注意是i+j = k,这就是卷积。
FFT板子:
// ---------------------------- FFT -------------------------------/
typedef long double db;
const db PI = acos(-1.0);
struct Complex {
db x, y;
Complex(db x=0.0, db y=0.0) : x(x), y(y) {}
Complex operator - (const Complex &b) const {
return Complex(x-b.x, y-b.y);
}
Complex operator + (const Complex &b) const {
return Complex(x+b.x, y+b.y);
}
Complex operator * (const Complex &b) const {
return Complex(x*b.x-y*b.y, x*b.y+y*b.x);
}
};
void change(Complex y[], int len) {
for(int i = 1, j = len / 2; i < len - 1; i++) {
if(i < j) swap(y[i], y[j]);
int k = len / 2;;
while(j >= k) {
j -= k;
k /= 2;
}
if(j < k) j+=k;
}
}
void fft(Complex y[], int len, int on) {
change(y, len);
for(int h = 2; h <= len; h<<=1) {
Complex wn(cos(-on*2*PI/h), sin(-on*2*PI/h));
for(int j = 0; j < len; j+=h) {
Complex w(1, 0);
for(int k = j; k < j + h / 2; k++) {
Complex u = y[k];
Complex t = w*y[k+h/2];
y[k] = u + t;
y[k+h/2] = u - t;
w = w*wn;
}
}
}
if(on == -1) {
for(int i = 0; i < len; i++) {
y[i].x /= len;
}
}
}
// ---------------------------- FFT ------------------------------- /
下面的代码不给出FFT板子的部分。
例题一
Rock Paper ScissorsGym - 101667H
题目大意
剪子(S)包袱§锤®的游戏,一个原串s,一个模式串p,模式串可以从前往后移,问模式串可以赢的最多次数是多少。
EXp:
第三组样例
12 4
RRRRRRRRRSSS
RRRS
模式串移到最后的位置
RRRRRRRRRSSS
RRRS
可以赢三次。
思路
可以先把模式串变换一下,P换成R,S换成P,R换成S,这样就是求最大匹配,就是匹配次数最多的问题。假如模式串长度为m,我们要计算原串[x-m+1: x]与模式串的匹配次数,可以先把模式串翻转(为了构成卷积的形式),然后分别计算PSR三种匹配情况,最后求和并找到最大的和。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// ---------------------------- FFT ------------------------------- /
// ---------------------------- FFT ------------------------------- /
const int maxn = 6e5 + 10;
Complex x1[maxn], x2[maxn];
char s[maxn / 6], ss[maxn / 6];
int n, m, sum[maxn];
void update(char c) {
int len = 1;
while(len < 2*n || len < 2*m ) len *= 2;
for(int i = 0; i < n; i++) {
x1[i] = Complex(s[i]==c, 0.0);
}
for(int i = n; i < len; i++) x1[i] = Complex(0.0, 0.0);
for(int i = 0; i < m; i++) {
x2[i] = Complex(ss[i]==c, 0.0);
}
for(int i = m; i < len; i++) x2[i] = Complex(0.0, 0.0);
fft(x1, len, 1);
fft(x2, len, 1);
for(int i = 0; i < len; i++) {
x1[i] = x1[i] * x2[i];
}
fft(x1, len, -1);
len = n + m + 1;
for(int i = 0; i < len; i++) {
sum[i] += (int)(x1[i].x + 0.5);
}
}
int main()
{
// freopen("/Users/maoxiangsun/MyRepertory/acm/i.txt", "r", stdin);
scanf("%d%d", &n, &m);
scanf("%s%s", s, ss);
for(int i = 0; i < m; i++) {
if(ss[i] == 'S') ss[i] = 'P';
else if(ss[i] == 'P') ss[i] = 'R';
else if(ss[i] == 'R') ss[i] = 'S';
}
reverse(ss, ss + m);
memset(sum, 0, sizeof(sum));
update('P');
update('R');
update('S');
int ans = 0;
for(int i = m - 1; i < n + m + 1; i++) {
ans = max(ans, sum[i]);
}
printf("%d\n", ans);
return 0;
}
close
例题二
K-InversionsGym - 101002E
题目大意:
给出一个AB串S,长度为N
S[j] == ‘B’, S[j] == ‘A’, k == j - i ,k在[1, N-1]的每种的个数。
思路
看到 j - i == k是不是很容易想到卷积的形式,可以看成j+(-i)== k但是坐标不能是负的,简单,两边加上n-1那就成卷积f(k+n-1) =g(n-1-i)h(j) 的形式,然后找到需要输出的那个区间,闭眼输出就行。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// ---------------------------- FFT ------------------------------- /
// ---------------------------- FFT ------------------------------- /
const int maxn = 9e6 + 100;
char s[maxn/4], p[maxn/2];
Complex x1[maxn], x2[maxn];
int n;
int main()
{
// freopen("/Users/maoxiangsun/MyRepertory/acm/i.txt", "r", stdin);
scanf("%s", s);
n = strlen(s);
memset(p, 0, sizeof(p));
for(int i = 0; i < n; i++) p[n-i-1] = s[i];
int len0 = n*2, len = 1;
while(len < len0 * 2) len<<=1;
for(int i = 0; i < n; i++) {
if(s[i] == 'B') x1[i] = Complex(1.0, 0);
else x1[i] = Complex(0.0, 0.0);
}
for(int i = n; i < len; i++) {
x1[i] = Complex(0.0, 0.0);
}
for(int i = 0; i < n; i++) {
if(p[i] == 'A') x2[i] = Complex(1.0, 0.0);
else x2[i] = Complex(0.0, 0.0);
}
for(int i = n; i < len; i++) {
x2[i] = Complex(0.0,0.0);
}
fft(x1, len, 1);fft(x2, len, 1);
for(int i = 0; i < len; i++) {
x1[i] = x1[i] * x2[i];
}
fft(x1, len, -1);
for(int i = 0; i < n-1; i++) {
printf("%d\n", (int)(x1[n-i-2].x + 0.5));
}
return 0;
}
例题三
含有通配符的匹配。
https://www.luogu.org/problemnew/solution/P4173 这篇文章写得不错,也是用到卷积。
C ( x , y ) = [ A ( x ) − B ( y ) ] 2 A ( x ) B ( y ) C(x,y)=[A(x)−B(y)]^2A(x)B(y) C(x,y)=[A(x)−B(y)]2A(x)B(y)
核心就是把上面那个式子暴力展开,把带通配符的标记为0,如果匹配的话卷积为0。
总结
在字符串匹配的任务中,用FFT往往可以搞定一些用KMP无法做到的任务,比方说字符串模糊匹配,字符串中带有卷积形式的问题求解,含有通配符匹配等。