月度归档: 2018 年 3 月

KMP算法原理与实现

KMP算法原理与实现

概述

KMP是一种字符串匹配算法,其复杂度已经达到了该类算法的下界,即O(|T|+|P|),其中T是文本串,P是模式串。下面介绍它的原理与实现。

MP算法(俗称)

下面我们介绍一种被称作MP算法的东西,这可以说是KMP算法的一个前身。
我们来尝试用P=”ABCD”来匹配T=”ABCABCABCD”,并观察它的过程。
第一步:P_0 \rightarrow T_0

ABCABCABCD
|||X
ABCD

第二步:P_0 \rightarrow T_1

ABCABCABCD
 X
 ABCD

第三步:P_0 \rightarrow T_2

ABCABCABCD
  X
  ABCD

第四步:P_0 \rightarrow T_3

ABCABCABCD
   |||X
   ABCD

……
第七步:P_0 \rightarrow T_6

ABCABCABCD
      ||||
      ABCD

这便是朴素地匹配字符串的过程,我们发现要匹配完成一共尝试了7次。这个算法的复杂度是O(|T||P|)的。
下面我们想,如果在第一步P_3失配后能够直接转移到第四步的位置,因为我们会发现用T_1, T_2去匹配P是完全没有用的。我们需要知道,如果一个字符失配了,应该将P后移到哪里才能避免中间若干失配的情况。我们把这个信息表示为fail[i]失配数组,具体而言,失配数组表示如果当前位置失配了,应该将P的哪一位移到这里来。
我们会发现,失配需要移动的时候实际上是从当前位置之前的子串中找到一个最长的后缀,使得其与某一前缀相等。这个过程可以用自我匹配来完成。

inline void calfail() {
    int i = 0, j = -1;
    fail[0] = -1;
    for(; pattern[i]; i++, j++) {
        while(j >= 0 && pattern[j] != pattern[i]) {
            j = fail[j];
        }
        fail[i + 1] = j + 1;
    }
}

从当前位置开始往前的某个后缀是原串的某个前缀,那么后一位如果失配应该移至这个前缀的后一位。
举个例子,如果P=”ABABC”,fail数组应该看起来像这样

P=    A B A B C
fail=-1 0 0 1 2

当我们发现没有这样的后缀时,我们会到达P的头,得到fail[0]=-1这个值,这意味着我们需要将P整体后移了。现在我们匹配的思路也就明确了,即失配→用失配数组中的上一个位置对齐继续匹配,直到匹配完成。
下面就是一个匹配的实现。

inline int match() {
    calfail();
    int i = 0, j = 0, m = strlen(pattern);
    for(; text[i]; i++, j++) {
        while(j >= 0 && pattern[j] != text[i]) {
            j = fail[j];
        }
        if(j >= m - 1) {
            return i - m + 1;
        }
    }
}

现在的算法就是O(|P|+|T|)的了。

KMP算法

我们来观察一组MP的过程。现在T=”ABCABCABABC”,P=”ABABC”。
第一步:P_0 \rightarrow T_0

ABCABCABABC
||X
ABABC

第二步:P_0 \rightarrow T_2

ABCABCABABC
  X
  ABABC

第三步:P_0 \rightarrow T_3

ABCABCABABC
   ||X
   ABABC

第四步:P_0 \rightarrow T_5

ABCABCABABC
     X
     ABABC

第四步:P_0 \rightarrow T_6

ABCABCABABC
      |||||
      ABABC

我们发现第二步、第四步的时候,我们老是在C这个位置失配,每次失配还要尝试2次,既然我们都知道了C和现在与fail指定的位置的A配不上,为什么不想办法把这段给跳过去呢?
接下来我们会更改fail的求法,使得中间重复的A被跳过去,实际上更改的方法也很简单。

inline void calfail() {
    int i = 0, j = -1;
    fail[0] = -1;
    for(; pattern[i]; i++, j++) {
        while(j >= 0 && pattern[j] != pattern[i]) {
            j = fail[j];
        }
        fail[i + 1] = pattern[j + 1] == pattern[i + 1] ? fail[j + 1] : j + 1;
        // 如果遇到了相同的字符,再往前跳一次
    }
}

匹配的方法与上面MP的一样,只是fail有一点小区别而已。这个优化并没有影响整体复杂度,只是一个常数优化。
注意我们求出来的fail数组实际上比P串长一位,最后一位的值并没有用上。

代码

MP算法

本代码可以通过【P3375】【模板】KMP字符串匹配 – 洛谷,一题。

// Code by KSkun, 2018/3
#include <cstdio>
#include <cstring>

const int MAXN = 1000005;

int fail[MAXN];
char str1[MAXN], str2[MAXN];

inline void calfail() {
    int i = 0, j = -1;
    fail[0] = -1;
    for(; str2[i]; i++, j++) {
        while(j >= 0 && str2[j] != str2[i]) {
            j = fail[j];
        }
        fail[i + 1] = j + 1;
    }
}

inline void match() {
    calfail();
    int i = 0, j = 0, m = strlen(str2);
    for(; str1[i]; i++, j++) {
        while(j >= 0 && str2[j] != str1[i]) {
            j = fail[j];
        }
        if(j >= m - 1) {
            printf("%d\n", i - m + 2);
        }
    }
}

int main() {
    scanf("%s%s", str1, str2);
    match();
    for(int i = 1; str2[i - 1]; i++) {
        printf("%d ", fail[i]);
    }
    return 0;
}

KMP算法

// Code by KSkun, 2018/3
#include <cstdio>
#include <cstring>

const int MAXN = 1000005;

int fail[MAXN];
char str1[MAXN], str2[MAXN];

inline void calfail() {
    int i = 0, j = -1;
    fail[0] = -1;
    for(; str2[i]; i++, j++) {
        while(j >= 0 && str2[j] != str2[i]) {
            j = fail[j];
        }
        fail[i + 1] = str2[j + 1] == str2[i + 1] ? fail[j + 1] : j + 1;
    }
}

inline void match() {
    calfail();
    int i = 0, j = 0, m = strlen(str2);
    for(; str1[i]; i++, j++) {
        while(j >= 0 && str2[j] != str1[i]) {
            j = fail[j];
        }
        if(j >= m - 1) {
            printf("%d\n", i - m + 2);
        }
    }
}

int main() {
    scanf("%s%s", str1, str2);
    match();
    for(int i = 0; str2[i]; i++) {
        printf("%d ", fail[i]);
    }
    return 0;
}

参考资料

数学笔记:逆元

数学笔记:逆元

逆元

一整数a对同余n之模逆元是满足以下公式的整数b
a^{-1} \equiv b \pmod{n}
也可以写成
ab \equiv 1 \pmod{n}

求逆元的方法

扩展欧几里得算法

费马小定理

费马小定理可以转换成如下形式
若a是一个整数,p是一个质数,则
a^{p-2} \equiv a^{-1} \pmod{p}

线性递推

我们设p=ki+r, r < i, 1 < i < p,然后把左边的式子展开
ki + r \equiv 0 \pmod{p}
两边同乘 (ir)^{-1}
\begin{aligned} kr^{-1} + i^{-1} &\equiv 0 &\pmod{p} \\ i^{-1} &\equiv -kr^{-1} &\pmod{p} \\ i^{-1} &\equiv -\lfloor \frac{p}{i} \rfloor \cdot (p \bmod i)^{-1} &\pmod{p} \end{aligned}
于是我们就得到了逆元的线性递推式:

inv[i] = -(MO / i) * inv[MO % i];

阶乘的逆元

先处理 (n!)^{-1} ,然后 [(n-1)!]^{-1} \equiv n(n!)^{-1} \pmod{p}

参考资料

[洛谷4245]【模板】MTT 题解 & 任意模数卷积算法(MTT)原理与实现

[洛谷4245]【模板】MTT 题解 & 任意模数卷积算法(MTT)原理与实现

题目地址:洛谷:【P4245】【模板】MTT – 洛谷

题目描述

多项式乘法。

输入输出样例

输入样例#1:

5 8 28
19 32 0 182 99 95
77 54 15 3 98 66 21 20 38

输出样例#1:

7 18 25 19 5 13 12 2 9 22 5 27 6 26

说明

数据范围1e5。

题解

由于NTT对模数有要求,我们需要使用任意模数卷积算法,也就是俗称的MTT。具体做法是,把一个数拆成32768x+y的形式,对拆出来的四组数分别卷积后拼起来。这样可以避免计算过程中的越界问题,一般的数据能够满足精度。
单位根最好预处理出来,这样才不会有精度问题。

代码

// Code by KSkun, 2018/3
#include <cstdio>
#include <cmath>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 1 << 20;
const double PI = acos(-1);
int MO;

struct Complex {
    double real, imag;
    Complex(double real = 0, double imag = 0) : real(real), imag(imag) {}
    inline Complex conj() {
        return Complex(real, -imag);
    }
    inline Complex operator+(Complex rhs) const {
        return Complex(real + rhs.real, imag + rhs.imag);
    }
    inline Complex operator-(Complex rhs) const {
        return Complex(real - rhs.real, imag - rhs.imag);
    }
    inline Complex operator*(Complex rhs) const {
        return Complex(real * rhs.real - imag * rhs.imag, imag * rhs.real + real * rhs.imag);
    }
    inline Complex operator*=(Complex rhs) {
        return (*this) = (*this) * rhs;
    }
    friend inline Complex operator*(double x, Complex cp) {
        return Complex(x * cp.real, x * cp.imag);
    }
    inline Complex operator/(double x) const {
        return Complex(real / x, imag / x);
    }
    inline Complex operator/=(double x) {
        return (*this) = (*this) / x;
    }
    friend inline Complex operator/(double x, Complex cp) {
        return x * cp.conj() / (cp.real * cp.real - cp.imag * cp.imag);
    }
    inline Complex operator/(Complex rhs) const {
        return (*this) * rhs.conj() / (rhs.real * rhs.real - rhs.imag * rhs.imag);
    }
    inline Complex operator/=(Complex rhs) {
        return (*this) = (*this) / rhs;
    }
    inline double length() {
        return sqrt(real * real + imag * imag);
    }
};

int n, m, len, rev[MAXN];
LL x[MAXN], y[MAXN], z[MAXN];
Complex a[MAXN], b[MAXN], c[MAXN], d[MAXN], e[MAXN], f[MAXN], g[MAXN], h[MAXN], wn[MAXN];

inline void fft(Complex *arr, int f) {
    for(int i = 0; i < n; i++) {
        if(i < rev[i]) std::swap(arr[i], arr[rev[i]]);
    }
    for(int i = 1; i < n; i <<= 1) {
        for(int j = 0; j < n; j += i << 1) {
            for(int k = 0; k < i; k++) {
                Complex w = Complex(wn[n / i * k].real, f * wn[n / i * k].imag), 
                    x = arr[j + k], y = w * arr[j + k + i];
                arr[j + k] = x + y;
                arr[j + k + i] = x - y;
            }
        }
    }
    if(f == -1) {
        for(int i = 0; i < n; i++) {
            arr[i] /= n;
        }
    }
}

int main() {
    n = readint(); m = readint(); MO = readint();
    for(int i = 0; i <= n; i++) {
        x[i] = readint() % MO;
    }
    for(int i = 0; i <= m; i++) {
        y[i] = readint() % MO;
    }
    m += n;
    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));
    }
    for(int i = 1; i < n; i <<= 1) {
        for(int k = 0; k < i; k++) {
            wn[n / i * k] = Complex {cos(PI * k / i), sin(PI * k / i)};
        }
    }
    for(int i = 0; i < n; i++) {
        a[i].real = x[i] >> 15; b[i].real = x[i] & 0x7fff;
        c[i].real = y[i] >> 15; d[i].real = y[i] & 0x7fff;
    }
    fft(a, 1); fft(b, 1); fft(c, 1); fft(d, 1);
    for(int i = 0; i < n; i++) {
        e[i] = a[i] * c[i]; f[i] = b[i] * c[i];
        g[i] = a[i] * d[i]; h[i] = b[i] * d[i];
    }
    fft(e, -1); fft(f, -1); fft(g, -1); fft(h, -1);
    for(int i = 0; i < n; i++) {
        z[i] = (((LL(round(e[i].real)) % MO) << 30) % MO 
            + ((LL(round(f[i].real)) % MO) << 15) % MO
            + ((LL(round(g[i].real)) % MO) << 15) % MO 
            + LL(round(h[i].real)) % MO) % MO;
    }
    for(int i = 0; i <= m; i++) {
        printf("%lld ", z[i]);
    }
    return 0;
}