【板子】kmp

复制代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// 求模式串p的next数组(最长border)
vector<int> getNext(const string &p) {
    int n = p.size();
    vector<int> next(n + 1, 0); // next[1]~next[n] 对应长度1~n前缀
    int j = 0;
    for (int i = 2; i <= n; ++i) {
        while (j > 0 && p[i - 1] != p[j]) j = next[j];
        if (p[i - 1] == p[j]) j++;
        next[i] = j;
    }
    return next;
}

// KMP匹配,返回所有匹配起始位置(1开头)
vector<int> kmpSearch(const string &s, const string &p, const vector<int> &next) {
    vector<int> res;
    int n = s.size(), m = p.size();
    int j = 0;
    for (int i = 1; i <= n; ++i) {
        while (j > 0 && s[i - 1] != p[j]) j = next[j];
        if (s[i - 1] == p[j]) j++;
        // 完全匹配
        if (j == m) {
            res.push_back(i - m + 1); // 左端点l(1起始)
            j = next[j];
        }
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    string s1, s2;
    cin >> s1 >> s2;
    
    auto next = getNext(s2);
    auto pos = kmpSearch(s1, s2, next);
    
    // 输出所有匹配位置
    for (int x : pos) cout << x << '\n';
    // 输出next[1]~next[m],空格分隔
    int m = s2.size();
    for (int i = 1; i <= m; ++i) {
        cout << next[i] << ' ';
    }
    return 0;
}

next特性:len-nextlen是最小循环节

优化失配数组

复制代码
#include <iostream>
#include <string>
#include <vector>
using namespace std;

vector<int> getNext(const string &p) {
    int n = p.size();
    vector<int> next(n + 1, 0);
    int j = 0;
    for (int i = 2; i <= n; ++i) {
        while (j > 0 && p[i - 1] != p[j]) j = next[j];
        if (p[i - 1] == p[j]) j++;
        next[i] = j;
    }
    return next;
}

// 生成nextval优化数组
vector<int> getNextVal(const string &p, const vector<int> &next) {
    int n = p.size();
    vector<int> nextval(n + 1, 0);
    nextval[1] = 0;
    for (int i = 2; i <= n; ++i) {
        int k = next[i];
        while (k > 0 && p[i - 1] == p[k]) k = next[k];
        nextval[i] = k;
    }
    return nextval;
}

// KMP匹配改用nextval
vector<int> kmpSearchVal(const string &s, const string &p, const vector<int> &nextval) {
    vector<int> res;
    int n = s.size(), m = p.size();
    int j = 0;
    for (int i = 1; i <= n; ++i) {
        while (j > 0 && s[i - 1] != p[j]) j = nextval[j];
        if (s[i - 1] == p[j]) j++;
        if (j == m) {
            res.push_back(i - m + 1);
            j = nextval[j];
        }
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    string s1, s2;
    cin >> s1 >> s2;
    
    auto next = getNext(s2);
    auto nextval = getNextVal(s2, next);
    auto pos = kmpSearchVal(s1, s2, nextval);
    
    for (int x : pos) cout << x << '\n';
    int m = s2.size();
    for (int i = 1; i <= m; ++i) cout << nextval[i] << ' ';
    return 0;
}

KMP 完整原理,分三层讲:问题、暴力缺陷、next 数组、匹配流程、nextval 优化

一、原始问题:字符串模式匹配

给定文本串 S(长 n)、模式串 P(长 m),找出 S 中所有 P 出现的位置。

暴力匹配缺点

两个指针 i(文本)、j(模式):

  1. 匹配相等:\(i++,j++\)
  2. 失配:i 回退到 \(i-j+2\),j 归零 最坏复杂度 \(O(nm)\),如 S=aaaaaaaP=aaab,大量重复比较浪费时间。

KMP 核心目标:失配时,文本指针 i 不回退,只让模式指针 j 跳到最优位置,整体线性 \(O(n+m)\)。

二、核心概念:Border(前缀函数 /next 数组)

1. 定义

对模式串前 i 个字符组成的前缀 \(P1,i\):

  • 真前缀 :不包含最后一位;真后缀:不包含第一位;
  • \(nexti\) = 最长的长度 k,满足前 k 位 = 后 k 位(\(k<i\)),这个长度叫 Border

例:\(P=abcab\)(长度 5) 前缀:a ab abc abca abcab 后缀:b ab cab bcab abcab 最长相等真前后缀是 ab,长度 2 → \(next5=2\)。

2. next 数组求解逻辑(1-based,和你板子统一)

j 记录上一个前缀的最长 border 长度:

  1. \(next1=0\):单个字符无真前后缀;
  2. i 从 2 遍历到 m:
    • 若 \(Pi-1 \ne Pj\):\(j=nextj\) 回溯,直到 \(j=0\);
    • 匹配相等:\(j++\);
    • \(nexti=j\)。 全程只前进不回退,\(O(m)\) 求出 next。

3. Border 的关键作用(KMP 灵魂)

当匹配到 \(Pj\) 和文本失配时: \(P1,j-1\) 是已经匹配成功的一段,不需要全部重比。 利用 \(nextj-1=k\):说明 \(P1,k = Pj-k,j-1\),直接把 j 跳到 k,文本指针 i 不动。

举例: \(P=abcabc\),匹配到第 6 位失配,\(next6=3\),直接让 \(j=3\),拿 \(P3\) 继续和当前文本字符对比,省去从头匹配。

三、KMP 匹配主流程

设文本 S,模式 P,i 文本下标,j 模式下标:

  1. 循环遍历文本每个字符 \(Si\):
    • 失配且 \(j>0\):\(j=nextj\);
    • 若字符相等:\(i++,j++\);
  2. 当 \(j=m\)(模式完全匹配):
    • 记录匹配起点;
    • \(j=nextj\) 寻找重叠匹配(比如 aaaaaa 会多次匹配)。

全程 i 只增不减,总复杂度 \(O(n+m)\)。

四、nextval(优化失配数组)原理

问题:普通 next 存在无效跳转

例 \(P=aaaaa\),\(next4=3\),若 \(P4=P3='a'\): 失配时跳到 \(j=3\),字符还是 a,依然失配,多一次无用判断。

nextval 优化规则

对每个 i:

  1. \(k=nexti\);
  2. 若 \(Pi-1 == Pk\),继续令 \(k=nextk\),直到字符不等或 \(k=0\);
  3. \(nextvali=k\)。

作用:失配时一步跳到字符不同的位置,减少比较次数,卡常专用

五、拓展:Border 衍生知识点

1. 最小循环节

字符串总长 L,最长 border \(k=nextL\),循环节长度 \(d=L-k\)。

  • 若 \(L \% d ==0\):整个串由 \(L/d\) 个相同子串重复;
  • 否则最小循环节是自身。 例:abcabcabc,\(L=9,next9=6,d=3\),可由abc重复 3 次构成。

2. 所有 Border 数量(cnt 数组,动物园题)

\(cnti\) = 前缀 i 所有合法 border 的总个数 递推式:\(cnti = cntnext\[i] +1\) 含义:最长 border + 最长 border 的所有 border。

3. 不重叠 border(num 数组,NOI 动物园)

要求 \(2k \le i\)(前后缀不重叠),用双指针维护最大合法 k,\(numi=cntk\),最后累乘 \((numi+1)\) 得答案。

相关推荐
通信仿真爱好者2 小时前
第【60期】--大规模MIMO系统信号检测算法误码率比较 --matlab完整代码+参考文章
算法·matlab·mimo·信号检测
AI科技星2 小时前
全域谱分析:无穷维超复数信息场分形统一场论——自然、量子、金融多重分形第一性原理完整体系(中英双语终稿)
人工智能·python·算法·金融·乖乖数学·全域数学
维他奶糖613 小时前
基于 DrissionPage 实现小红书搜索笔记批量采集
笔记
先吃饱再说3 小时前
为什么堆能 O(log n) 插入?拆解完全二叉树的数组魔法
算法·排序算法
留白_4 小时前
【决策树】泰坦尼克号生存预测
算法·决策树·机器学习
hssfscv4 小时前
QT的学习笔记4——QMainWindow、资源文件以及主要控件的介绍
笔记·qt·学习
克里斯蒂亚诺更新4 小时前
电脑关闭密码的操作
笔记
hqzing4 小时前
鸿蒙 PC 底层开发技术详解(七):二进制自签名算法的实现
算法·华为·harmonyos
AI科技星4 小时前
超复数全域经济周期场与信息谱场——金融与密码学底层理论重构《0·1·∞三元一体全域超复数统一场论》系列全集(六一字不漏完整合订终版)
人工智能·算法·金融·密码学·拓扑学·乖乖数学·全域数学
小小的代码里面挖呀挖呀挖4 小时前
杰理蓝牙耳机开发--LE Audio与游戏耳机应用
笔记·单片机·物联网·学习