Bootstrap

字符串kmp-幸运字符串

问题描述

给定一个长度为 n 的字符串 S ,幸运字符串的定义如下:

  1. 该字符串为 S 的一个前缀字符串 。
  2. 该字符串在 S 中至少出现过 2 次 。

现在要你求出长度最大的幸运字符串 。

输入格式

输入第一行,包含一个整数 n ,表示字符串的长度 。

输入第二行,长度为 n 且由小写字母组成的字符串 。

输出格式

输出仅一行,包含一个整数,表示长度最大的幸运字符串的长度 。

输入案例

9
abcdaaaba

样例输出

2

说明

前缀 ab 在 S 中出现了两次,由此答案是 2 。

解题思想:

该题目的本质就是求一个出现在字符串中的最大的前缀字符串,可以用 KMP 解决这个问题,KMP 的 next 数组存的是这个位置前面的字符串中前缀与后缀相等的最大长度,所以我们可以预处理 S的 next 数组,枚举每一个位置的 next 的值,取一个最大值即为答案 。

解题代码:

#include <bits/stdc++.h> 
using namespace std;    
const int N = 2e5 + 10;  // 定义常量 N,表示字符串的最大长度,设置为 2e5 + 10 以应对题目中的输入范围

int nex[N]; // 定义 next 数组,用于存储 KMP 算法中的部分匹配表
            // nex[i] 表示字符串前 i 个字符中,最长的既是前缀又是后缀的子串的长度

int main() {
    int n; cin >> n; // 读取字符串的长度 n
    char p[n + 1];   // 定义字符数组 p,长度为 n + 1,用于存储字符串(下标从 1 开始)
    cin >> p + 1;    // 从下标 1 开始读取字符串 S,存储在字符数组 p 中

    // 构建 KMP 算法的 next 数组
    for (int i = 2, j = 0; i <= n; i++) { // 从字符串的第二个字符开始遍历
        while (j && p[i] != p[j + 1]) j = nex[j]; // 如果当前字符不匹配,则回退 j 到 nex[j],直到匹配或 j 为 0
        if (p[i] == p[j + 1]) j++; // 如果当前字符匹配,则 j 增加 1,表示匹配长度增加
        nex[i] = j; // 将当前匹配的长度 j 存入 nex[i],表示前 i 个字符中,最长的既是前缀又是后缀的子串的长度
    }

    int ans = 0; // 初始化 ans 为 0,用于存储最大的幸运字符串的长度
    for (int i = 1; i <= n; i++) ans = max(ans, nex[i]); // 遍历 next 数组,找到其中的最大值
    cout << ans << endl; // 输出结果,即最长的幸运字符串的长度

    return 0; 
}

代码说明:

1. 代码逻辑总结
  • 输入处理

    • 读取字符串长度 nn。

    • 定义字符数组 p,长度为 n+1n+1,并从下标 1 开始读取字符串 SS。

  • KMP 算法的 next 数组构建

    • 通过遍历字符串,计算每个位置 ii 的最长前缀后缀匹配长度,并存储在 nex[i] 中。

  • 求解最大幸运字符串

    • 遍历 nex 数组,找到最大值,即为最长的既是前缀又在字符串中至少出现过 2 次的子串的长度。

  • 输出结果

    • 输出最大值 ans

在 KMP 算法中,i 从 2 开始,j 从 0 开始的原因与算法的核心思想密切相关。以下是详细解释:


2. i 从 2 开始的原因
  • i 表示当前正在处理的字符位置

  • KMP 算法的 next 数组nex[i] 表示字符串的前 ii 个字符中,最长的既是前缀又是后缀的子串的长度。

  • 为什么从 2 开始

    • 对于第一个字符(i = 1),它没有前缀和后缀,因此 nex[1] 的值一定是 0。

    • 从 i = 2 开始,才能有意义地计算前缀和后缀的匹配情况。

    • 如果从 i = 1 开始,nex[1] 的值已经被默认设置为 0,无需再计算。


3. j 从 0 开始的原因
  • j 表示当前匹配的前缀长度

  • 初始状态

    • 当 i = 2 时,j 从 0 开始,表示还没有匹配到任何前缀。

    • j 的作用是记录当前匹配的前缀长度,并在匹配失败时回退到 nex[j]

  • 为什么从 0 开始

    • j = 0 表示当前没有匹配到任何前缀。

    • 如果 j 从 1 开始,会导致逻辑错误,因为 j 表示的是已经匹配的长度,而不是当前字符的位置。


4. KMP 算法的核心逻辑
  • 匹配过程

    • 当 p[i] == p[j + 1] 时,表示当前字符匹配成功,j 增加 1。

    • 当 p[i] != p[j + 1] 时,表示当前字符匹配失败,j 回退到 nex[j],继续尝试匹配。

  • nex 数组的作用

    • nex[j] 记录了当匹配失败时,j 应该回退到的位置。

    • 通过 nex 数组,KMP 算法可以跳过不必要的比较,提高匹配效率。


5. 为什么 i 从 2 开始,j 从 0 开始的示例

假设字符串为 ababab,构建 nex 数组的过程如下:

ip[i]jp[j + 1]操作nex[i]
2b0ap[2] != p[1]j 保持 00
3a0ap[3] == p[1]j++1
4b1bp[4] == p[2]j++2
5a2ap[5] == p[3]j++3
6b3bp[6] == p[4]j++4
  • 最终 nex 数组为 [0, 0, 1, 2, 3, 4]

  • 最长的既是前缀又是后缀的子串是 abab,长度为 4。

;