题解 POI 2006 OKR-Periods of Words

题目:LibreOJ / Luogu / 一本通

前排提醒:fail 函数 / 数组 和 next 函数 / 数组 是同一个东西,叫法存在差异,是用于解决失配情况的。个人在这篇题解里使用了 next 的说法,若有差异,敬请理解包涵。

题目大意

个人认为上述 OJ 上的题面足够简洁易懂,故不作解释。

大致思路

见下例,可见答案即为给定字符串所有前缀的最短公共前后缀长度之和。

假定给出的输入为 abababababa,首先我们找出它的其中一个前缀 ababababab 并处理。过程如下:

  1. ababababab 中揪出来一个前缀 abababab
  2. abababab 翻倍变成 abababababababab
  3. 合格性检验:将 abababab|abababababababab|ab 进行比对(因为方便观看所以加了竖线),发现后者较提取出的前缀多出来一个 ab,这个 ab 为揪出来的前缀 abababab最短公共前后缀

详细做法

此题重难点在「将题目转换成求取最短公共前后缀问题」、「求取最短公共前后缀」和「优化求取过程」这三方面。

· 问题转化

我们还对上述样例进行分析。

假定给出的输入为 abababababa,首先我们找出它的其中一个前缀 ababababab 并处理。处理使用手推,过程如下:

  1. ababababab 中揪出来一个前缀 abababab(因为是手推所以找一些方便找规律的);
  2. abababab 翻倍变成 abababababababab
  3. 合格性检验:将 abababab|abababababababab|ab 进行比对,发现后者较提取出的前缀多出来一个 ab,这个 ab 为揪出来的前缀 abababab 的前缀。

接下来一组对照:

  1. ababababab重新揪出来一个前缀 ababab(因为是手推所以再找一个,程序里不会这么做);
  2. ababab 翻倍变成 abababababab
  3. 合格性检验:将 ababab|abababababab|abab 进行比对,发现后者较提取出的前缀多出来一个 abab,这个 abab 为揪出来的前缀 ababab 的前缀。

诶,阅读题目,我们发现我们揪出来的两个前缀都是在处理的原串前缀的「周期」。而我们要的是最大周期,所以需要保留 abababab,而不保留 ababab
分析 abababab 的性质,多出来的 ab 为揪出来的前缀 abababab最短公共前后缀,周期最大就需要公共前后缀最短。而这些原串前缀所需要累加的长度为 最大周期长度 = 字符串长度 - 最短公共前后缀长度

这个题目就这样通过模拟一下转换完了。

· 求取答案

求取最短公共前后缀的过程不难,但是需要对 KMP 算法的 next 数组有一定的理解。
个人理解:next 数组的意义是,当跑到模式串的第 \(i\) 位时若失配,则跳转到第 \(next_i\) 位继续匹配,是以 \(i\)(下标从 \(1\) 开始)结尾的后缀与串的前缀的最长公共字符串长度,性质满足在长度为 \(i\) 的字符串前缀中,从第 \(1\) 位到第 \(next_i\) 位的字符串是其最长公共前后缀

等下,出现了最长公共前后缀,和最短公共前后缀只差一个字了,我们需要把它转换成最短公共前后缀。我们可以通过递归 next 数组的方式找到最短公共前后缀:一直递归直到 next[POINTER] = 0,最短公共前后缀则为从 \(1\)POINTER 的字符串。
用一个例子来解释会更清晰明了。

我们现在有一个字符串 abababcababxyzabababcabab,我们需要求它的最短公共前后缀。
首先跑出来 next 数组的结果为 0 0 1 2 3 4 0 1 2 3 4 0 0 0 1 2 3 4 5 6 7 8 9 10 11
然后设定变量 POINTER = 25,为需要处理的字符串长度。然后进行处理:

  1. 在这个字符串中,next[POINTER] = 11,指向字母 b,指向长度为 \(11\),指向字符串为 abababcabab,赋值:POINTER = next[POINTER]
  2. 在这个字符串(abababcabab)中,next[POINTER] = 4,指向字母 b,指向长度为 \(4\),指向字符串为 abab,赋值:POINTER = next[POINTER]
    特殊说明:无需再跑一遍 next 数组,因为整个字符串的 next 数组的前面 11 位即为此字符串的 next 数组。
  3. 在这个字符串(abab)中,next[POINTER] = 2,指向字母 b,指向长度为 \(2\),指向字符串为 ab,赋值:POINTER = next[POINTER]
  4. 在这个字符串(ab)中,next[POINTER] = 0,递归结束。从第一位到第 POINTER 位(字符串 ab)则为大字符串 abababcababxyzabababcabab 的最短公共前后缀,长度为 POINTER = 2

代码实现就很简单了,while (nxt[POINTER]) POINTER = nxt[POINTER]; 一句就可以完成。

所有的有最长公共前后缀的字符串都可以拆成 \(\rm ABA\) 的形式,其中 \(\rm A\) 长度不为 \(0\)。我们将 \(\rm A\) 拆到最小、不可拆的形式的时候,这个最终的 \(\rm A\) 就是最短公共前后缀。

· 优化过程

考虑一个极端情况:出题人嘿嘿一笑,「我将,扭转万象!」然后给了你一个数据。我将其缩小到了原来的 \(\frac{1}{40000}\),放到了第一行:

25 aaaaaaaaaaaaaaaaaaaaaaaaa
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

第二行,如你所见,是跑完 next 数组的结果。这意味着处理这个长字符串的时候你要递归 \(24\) 次,原数据 \(999999\) 次。而这是输入数据,是你要跑的最长的字符串,总共跑完要 \(499999500000\) 次,这个 \(O(n^2)\) 的算法直接会时间超限。

提供两种优化,第一种可以优化到 \(O(n\log n)\),第二种可以到 \(O(n)\)。第一种是倍增,不再赘述,可以去参考「倍增求 LCA」。

这里主要讲解记忆化搜索。很简单,马上就好。

当你跑完 while 之后,如果 POINTER 不等于你在处理的字符串长度,这证明 while 跑了至少一次。接下来覆盖原先的 next 数组。将处理顺序写为长度从短到长,写好 next 数组的覆盖,就可以了。

代码

Click to show code
#include <bits/stdc++.h>
using namespace std;

int nxt[1919810]; // next 数组
string p;         // 读入的字符串,但是因为要跑 getNext 所以写了 pattern 而非 string
int lp;           // 字符串的长度(length)

void getNext () { // 跑 next 数组,如果这里不清楚请重新学习 KMP
  int j = 0;
  nxt[1] = 0;
  for (int i = 1; i < lp; i++){
    while (j && p[j] != p[i]) j = nxt[j];
    if(p[j] == p[i]) j++;
    nxt[i + 1] = j;
  }
}

signed main () {
  cin >> lp >> p;
  getNext ();
  long long ans = 0;
  for (int i = 1; i <= lp; i++) {
    int j = i; // j 是指针(POINTER),意义见上文
    while (nxt[j]) j = nxt[j]; // 求最短公共前后缀
    if (j != i) nxt[i] = j;    // 记忆化搜索
    ans += i - j; // 处理字符串长度 - 最短公共前后缀长度
  }
  cout << ans;
}

关联题目

趁热打铁:2014 年湖北省队互测 Week2 似乎在梦中见过的样子

posted @ 2022-10-04 14:03  Reverist  阅读(40)  评论(0编辑  收藏  举报