多模字符串匹配算法 -- 面试题 17.17. 多次搜索

面试题 17.17. 多次搜索

cpp 复制代码
/* 【多模字符串匹配算法】
N: len(big), 10e3
M: len(smalls), 10e6~10e3
S: Σlen(small), 10e3~10e6
L: len(small), 10e3
我们可以粗略的认为,MS=10e9,当然这是不严谨的

一、朴素做法
对于每个短字符串 small(smalls[i]),在长字符串 big 中做暴力匹配(KMP)
每次匹配的时间复杂度为 O(N + len(small))
总的时间复杂 O(N + len(smalls[1])) + O(N + len(smalls[2])) + ... + O(N + len(smalls[M]))
等于 O(NM + S)
但当 M=10e6,S=10e3 时,也即每个 small 都很短时,时间复杂度最坏,达到 10e9
也即对于每个非常短的字符串 small,我们都需要遍历一遍 big

二、Trie
注意到题目所说:smalls 中的每一个较短字符串
并且 len(smalls[i])<=1e3,且 Σlen(small)<=1e6
可能出现下面场景:每个 smalls[i] 的长度很短,可能 <= 10,但 len(smalls) 可能很大,为
    26 + 26^2 + 26^3 + ... + 26^10
显然,此时存在大量 smalls[i] 是 smalss[j] 的前缀,因此我们可以用 Trie 优化
首先针对 smalls 建立 Trie,然后以 i=[0,len(big)) 作为起始点遍历 trie
总的时间复杂度为 O(S + NL) = O(10e6)

三、AC 自动机
时间复杂度 O(S + N)
*/
class Solution {
    class Trie {
    private:
        struct Node {
            vector<unique_ptr<Node>> childrens;
            int idx;
            Node() : childrens(26), idx(-1) {}
        };

    public:
        Trie() : tr(new Node) {} 
        ~Trie() { delete tr; }

        void insert(const string &str, int idx) {
            Node *p = tr;
            for(auto &s : str) {
                int u = s - 'a';
                if(p->childrens[u] == nullptr) {
                    p->childrens[u] = std::make_unique<Node>();
                }
                p = p->childrens[u].get();
            }
            p->idx = idx;
        }

        void search(const string &s, int start, vector<vector<int>> &res) {
            Node *p = tr;
            for(int i = start; i < s.size(); i ++ ) {
                int u = s[i] - 'a';
                if(p->childrens[u] == nullptr)  return ;
                p = p->childrens[u].get();
                if(p->idx != -1) res[p->idx].emplace_back(start);
            }
        }

    private:
        Node *tr;
    };
public:
    vector<vector<int>> multiSearch(string big, vector<string>& smalls) {
        Trie tr;
        vector<vector<int>> res(smalls.size());
        for(int i = 0; i < smalls.size(); i ++ ) tr.insert(smalls[i], i);
        for(int i = 0; i < big.size(); i ++ ) tr.search(big, i, res);
        return res;
    }
};

对于多模字符串匹配算法,从简单到复杂,从低效到高效有三种方法:

  1. 暴力 KMP 匹配
  2. Trie 优化
  3. AC 自动机(了解)

时间复杂度分析

设:

  • N = ∣ b i g ∣ N = |big| N=∣big∣ (大字符串长度,约 1 0 3 ∼ 1 0 4 10^3 \sim 10^4 103∼104)
  • M = ∣ s m a l l s ∣ M = |smalls| M=∣smalls∣ (模式串数量,可达 1 0 6 10^6 106)
  • S = ∑ ∣ s m a l l s [ i ] ∣ S = \sum |smalls[i]| S=∑∣smalls[i]∣ (所有模式串总长度, 1 0 3 ∼ 1 0 6 10^3 \sim 10^6 103∼106)
  • L = max ⁡ ∣ s m a l l s [ i ] ∣ L = \max |smalls[i]| L=max∣smalls[i]∣ (最长模式串长度, ≤ 1 0 3 \le 10^3 ≤103)

在极端情况下, M M M 很大且每个模式串都很短,可粗略认为:

  • M ⋅ L ≈ 1 0 9 M \cdot L \approx 10^9 M⋅L≈109(仅用于复杂度规模估计)

一、朴素做法(逐个 small 在 big 中匹配)

对于每个 smalls[i] 在 big 中进行一次暴力或 KMP 匹配:

  • 单次匹配复杂度:
    O ( N + ∣ s m a l l s [ i ] ∣ ) O(N + |smalls[i]|) O(N+∣smalls[i]∣)

  • 总复杂度:

∑ i = 1 M ( N + ∣ s m a l l s i ∣ ) = N M + S \sum_{i=1}^{M} (N + |smalls_i|) = NM + S i=1∑M(N+∣smallsi∣)=NM+S

当存在大量极短 small(例如: M = 1 0 6 , S = 1 0 3 M = 10^6, S = 10^3 M=106,S=103)时:

N M = 1 0 3 ⋅ 1 0 6 = 1 0 9 NM = 10^3 \cdot 10^6 = 10^9 NM=103⋅106=109

即:每个短 small 都完整扫描一次 big,效率极低。


二、Trie(字典树)优化

由于:

  • ∣ s m a l l s [ i ] ∣ ≤ 1 0 3 |smalls[i]| \le 10^3 ∣smalls[i]∣≤103
  • S ≤ 1 0 6 S \le 10^6 S≤106

可能出现大量前缀关系,如:
26 , 2 6 2 , 2 6 3 , ... , 2 6 10 26, 26^2, 26^3, ..., 26^{10} 26,262,263,...,2610

此时可用 Trie 来共享前缀结构,减少重复匹配。

  • 构建 Trie:O ( S ) O(S) O(S)
  • 从 big 的每个起点向右沿 Trie 匹配,最多前进 L L L 步:O ( L ) O(L) O(L)

总匹配复杂度:

O ( N L ) O(NL) O(NL)

整体复杂度为:

O ( S + N L ) O(S + NL) O(S+NL)

在题目规模下约为 1 0 6 10^6 106 ,比朴素方法的 1 0 9 10^9 109 快两个数量级。


三、Aho--Corasick 自动机(AC 自动机)

在 Trie 的基础上增加 fail 指针,使失配时可以跳至最长后缀前缀,从而避免重复回溯:

  • 构建 Trie:O ( S ) O(S) O(S)
  • 构建 fail 链:O ( S ) O(S) O(S)
  • 匹配 big:O ( N + matches ) ≈ O ( N ) O(N + \text{matches}) \approx O(N) O(N+matches)≈O(N)

整体复杂度:

O ( S + N ) \boxed{O(S + N)} O(S+N)

这是多模式匹配的最优级别,并且与模式串数量 M M M 无关。


总结

方法 时间复杂度 在 M = 1 0 6 M=10^6 M=106 时表现
朴素多次匹配 O ( N M + S ) O(NM + S) O(NM+S) 最差可达 1 0 9 10^9 109 次操作
Trie 匹配 O ( S + N L ) O(S + NL) O(S+NL) 稳定在 1 0 6 10^6 106 左右
AC 自动机 O ( S + N ) O(S + N) O(S+N) 接近线性时间,最优
相关推荐
da_vinci_x31 分钟前
Sampler AI + 滤波算法:解决 AIGC 贴图“噪点过剩”,构建风格化 PBR 工业管线
人工智能·算法·aigc·材质·贴图·技术美术·游戏美术
惊鸿.Jh33 分钟前
503. 下一个更大元素 II
数据结构·算法·leetcode
chao18984439 分钟前
MATLAB 实现声纹识别特征提取
人工智能·算法·matlab
zhishidi41 分钟前
推荐算法之:GBDT、GBDT LR、XGBoost详细解读与案例实现
人工智能·算法·推荐算法
货拉拉技术42 分钟前
货拉拉RAG优化实践:从原始数据到高质量知识库
数据库·算法
AKDreamer_HeXY1 小时前
ABC434E 题解
c++·算法·图论·atcoder
罗湖老棍子1 小时前
完全背包 vs 多重背包的优化逻辑
c++·算法·动态规划·背包
TL滕1 小时前
从0开始学算法——第四天(题目参考答案)
数据结构·笔记·python·学习·算法
potato_may1 小时前
C++ 发展简史与核心语法入门
开发语言·c++·算法