ACDoubleArrayTrie



2023年02月04日    Author:Guofei

文章归类: 0x80_数据结构与算法    文章编号: 591

版权声明:本文作者是郭飞。转载随意,但需要标明原文链接,并通知本人
原文链接:https://www.guofei.site/2023/02/04/ac.html


是什么

ACDoubleArrayTrie = KMP + DoubleArrayTrie

KMP

  • 空间换时间
  • 根据模式串 pattern 构建一个跳转指示索引,从而使遍历串的时候“不走回头路”
  • 只能匹配单个模式串

Trie:不多说

DoubleArray:把 Trie 以 Array 的形式表达,从而提升性能、降低索引成本。

DoubleArrayTrie

KMP

三个作者(D.E.Knuth、J.H.Morris和V.R.Pratt)

是一种深度优化过的字符串匹配算法

  • 时间复杂度为 O(M+N)
  • 关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数

两种类型

  • 精确匹配
  • 近似匹配

本文用 pat 表示模式串,长度为 M,txt 表示文本串,长度为 N。 KMP 算法的任务是在 txt 中查找子串 pat,如果存在,返回这个子串的起始索引,否则返回 -1。

dp[j][c] = next
0 <= j < M,代表当前的状态
0 <= c < 256,代表遇到的字符(ASCII 码)
0 <= next <= M,代表下一个状态

dp[4]['A'] = 3 表示:
当前是状态 4,如果遇到字符 A,
pat 应该转移到状态 3

dp[1]['B'] = 2 表示:
当前是状态 1,如果遇到字符 B,
pat 应该转移到状态 2

暴力匹配

特点

  • 时间复杂度为 O(MN),空间复杂度为 O(1)
  • 如果字符串中重复的字符比较多,该算法就显得很蠢。

fn brute_force_search(s: &[u8], p: &[u8]) -> Option<usize> {
    let n = s.len();
    let m = p.len();

    if m == 0 { return Some(0); }
    if m > n { return None; }

    for i in 0..=(n - m) {
        for j in 0..m {
            if s[i + j] != p[j] {
                break;
            }
            if j == m - 1 {
                return Some(i);
            }
        }
    }
    None
}

fn main() {
    let s = "hello world";
    let p = "world";
    let res = brute_force_search(s.as_bytes(), p.as_bytes());
    println!("{:?}", res);
}

KMP

KMP 会花费空间来记录一些信息,在以下两种情况下很聪明

KMP 算法永不回退 txt 的指针 i,不走回头路(不会重复扫描 txt),而是借助 dp 数组中储存的信息把 pat 移到正确的位置继续匹配,时间复杂度 O(N),用空间换时间。它还可以被看成一种动态规划算法。

  • 如何计算 dp 呢? 经过分析,这个 dp 数组,只和 pat 有关

KMP-dp版

正经的 KMP 用的是一维的 next 数组,这里为了代码可读性,用二维的 dp 数组

class KMP:
    def __init__(self, pat: str):
        # 构建 kmp
        # O(m)
        self.pat = pat
        m = len(pat)
        dp = [[0 for _ in range(256)] for _ in range(m)]

        # base case,遇到 pat[0] 才转移 0 -> 1,遇到其它字符停留在 0
        dp[0][ord(pat[0])] = 1

        # x 是影子状态
        x = 0
        for j in range(1, m):
            for c in range(256):
                if ord(pat[j]) == c:
                    dp[j][c] = j + 1
                else:
                    dp[j][c] = dp[x][c]

            x = dp[x][ord(pat[j])]

        self.dp = dp

    def search(self, txt):
        m, n = len(self.pat), len(txt)

        j = 0
        for i in range(n):
            j = self.dp[j][ord(txt[i])]
            if j == m:
                return i - m + 1

        return -1


kmp = KMP('aaab')

kmp.search('aaacaaab')

仿照 Python 版本,写出 Rust 版本:

pub struct KMP {
    dp: Vec<Vec<usize>>,
}

impl KMP {
    pub fn new(pat: &[u8]) -> Self {
        let m = pat.len();
        let mut dp = vec![vec![0; 256]; m];

        for (j, chr) in pat.iter().enumerate() {
            if j == 0 {
                dp[0][*chr as usize] = j + 1;
                continue;
            }
            for c in 0usize..256 {
                if *chr as usize == c {
                    dp[j][c] = j + 1;
                } else { dp[j][c] = dp[j - 1][c] }
            }
        }

        Self { dp }
    }
    pub fn search(&self, s: &[u8]) -> Option<usize> {
        let m = self.dp.len();
        let mut j = 0;

        for (i,chr) in s.iter().enumerate(){
            j = self.dp[j][*chr as usize];
            if j == m {
                return Some(i + 1 - m);
            }
        }

        return None;
    }
}


fn main() {
    let kmp = KMP::new("world".as_bytes());
    let res = kmp.search("hello world".as_bytes());
    let res = kmp.search("hello worll world".as_bytes());
    println!("{:?}", res);

    let kmp = KMP::new("abcac".as_bytes());

    assert_eq!(kmp.search("ababcabcacbab".as_bytes()), Some(5));
    assert_eq!(kmp.search("abcac".as_bytes()), Some(0));
    assert_eq!(kmp.search("abcac14123".as_bytes()), Some(0));
    assert_eq!(kmp.search("ababcabcac".as_bytes()), Some(5));
    assert_eq!(kmp.search("ababcab1cacbab".as_bytes()), None);
}

KMP-next数组

Rust 版本

pub struct KMP {
    pat: Vec<u8>,
    next: Vec<usize>,
}

impl KMP {
    pub fn new(pat: &[u8]) -> Self {
        let mut next = vec![0; pat.len() + 1];
        let (mut i, mut j) = (1, 0);
        while i < pat.len() {
            if j == 0 || pat[i - 1] == pat[j - 1] {
                i += 1;
                j += 1;
                next[i] = j;
            } else { j = next[j] }
        }
        Self { pat: pat.to_vec(), next }
    }
    pub fn search(&self, s: &[u8]) -> Option<usize> {
        let (mut i, mut j) = (1, 1);
        while i <= s.len() && j <= self.pat.len() {
            if j == 0 || s[i - 1] == self.pat[j - 1] {
                i += 1;
                j += 1;
            } else { j = self.next[j]; }
        }
        if j > self.pat.len() {
            return Some(i - j);
        }
        None
    }
}


fn main() {
    let kmp = KMP::new("world".as_bytes());
    let res = kmp.search("hello world".as_bytes());
    let res = kmp.search("hello worll world".as_bytes());
    println!("{:?}", res);

    let kmp = KMP::new("abcac".as_bytes());

    assert_eq!(kmp.search("ababcabcacbab".as_bytes()), Some(5));
    assert_eq!(kmp.search("abcac".as_bytes()), Some(0));
    assert_eq!(kmp.search("abcac14123".as_bytes()), Some(0));
    assert_eq!(kmp.search("ababcabcac".as_bytes()), Some(5));
    assert_eq!(kmp.search("ababcab1cacbab".as_bytes()), None);
}

另外,C/Java/Python 版本放到 gist 了:https://gist.github.com/guofei9987/704b6a946a649812718d008ddab0acfa

ACDoubleArrayTrie

C++/C/Python 实现放到了 gist 了:https://gist.github.com/guofei9987/7f84db7cce1922388a87b7539f578cad

参考文献

  • KMP详解:https://zhuanlan.zhihu.com/p/83334559
  • AC 自动机:https://www.bilibili.com/video/BV1uJ411Y7Eg

您的支持将鼓励我继续创作!