题解 2014 年湖北省队互测 Week2 似乎在梦中见过的样子

题目:LibreOJ / 一本通

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

题目大意

求长度为 \(n\) 的字符串 \(\rm S\) 中所有不同的形如 \(\rm ABA\) 的子串,满足 \(\operatorname{len}(A)\ge k\)\(\operatorname{len}(B)\ge 1\)
「不同的」的定义是位置不同、长度不同,拆分方式可以相同。

大致思路

由于其数据规模并不是很大,可以通过循环解决。
枚举子串的左端点,边跑 next 数组边计算答案(移除覆盖的情况、判断答案)。

详细做法

此题重难点在于「转化问题」和「移除覆盖情况」。

· 转化问题

求长度为 \(n\) 的字符串 \(\rm S\) 中所有不同的形如 \(\rm ABA\) 的子串,要满足一定条件。
如果你有做过一本通题目列表中此题的上一道题「POI 2006 OKR-Periods of Words」,这道题会相对好做一些。

上一道题是只移动右端点,跑一遍 next,右端点往右动,字符串从短到长跑,记忆化搜索,时间复杂度 \(O(n)\)

这道题我们可以参考一下,不仅仅移动右端点,还要移动左端点,字符串从短到长跑,因为数据范围并不大,时间复杂度可以接受 \(O(n^2)\)。可以使用边跑 next 边算答案。

· 移除覆盖情况

这道题不需要求最短公共前后缀,也不能用最长公共前后缀。这道题需要你判断子串是否有一个长度合适(合规)的公共前后缀。

详细的,我们用一个样例:aaaaa(下标从 \(1\) 开始),\(k\)\(1\)
跑完 next 的结果是 0 1 2 3 4

  1. 左端点指向 \(1\),右端点指向 \(1\),为了算 next 需要经历的过程,不满足基本条件,不通过
  2. 左端点指向 \(1\),右端点指向 \(2\),也不满足基本条件,不通过;
  3. 左端点指向 \(1\),右端点指向 \(3\),满足基本条件,但是存在问题:在 aaa 中最大公共前后缀为 aa,构成重叠,我们将指针 \(P = 3\) 赋值为 \(next_P = 2\),得到 aaa 的最大公共前后缀 aa,发现字符串 aa 的最大公共前后缀 a 是合法的。

这个时候我们需要用上一题求最短公共前后缀的思路,往前回溯(x = next[x])。

也并不是所有样例都像上述一般,有的时候存在回溯了就非法的情况:字符串 ababa\(k = 2\) 时,指针 \(P = 3\) 赋值为 \(next_P = 1\),不重叠了,但是长度为 \(1\),小于 \(2\)

最后将所有的左端点循环一遍就好了,从 \(1\)\(len - k * 2\)

代码

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

int nxt[111111];   // next 数组
int k, lp, lptmp;  // 给定 k,字符串长度(常量),字符串长度(变量)
long long ans = 0; // 答案

void exec (string s) {
  // 我命名为 exec 是因为它已经不是单纯的一个跑 next 了。
  // 边跑 next 边统计答案,你可以发现 for 里面前两句是纯纯的求 next
  // 第三句是融合一下 第四第五句就是统计答案
  nxt[0] = nxt[1] = 0;
  int j = 0;
  for (int i = 1; i < lptmp; i++){
    while (j && s[j] != s[i]) j = nxt[j]; // 求 next 第一步
    if(s[j] == s[i]) j++;                 // 求 next 第二步
    int P = nxt[i + 1] = j;       // 求 next 第三步,但是记录指针
    while (P > i / 2) P = nxt[w]; // 递归求满足最大长度要求的公共前后缀
    if (P >= k) ans++;            // 若满足最小长度要求 记录答案
  }
}

signed main () {
  string p;
  cin >> p >> k;
  lp = p.length (); // 之后就不能变了
  lptmp = lp;
  for (int i = 1; i <= lp - k * 2; i++) {
    exec (p);
    // 这里使用了一种很激进的方案,直接删字符串
    p.erase (0, 1); // 这里的 erase () 函数是从下标为 0 开始移除 1 个
    lptmp--;        // 字符串长度 - 1
  }
  cout << ans;
}
posted @ 2022-10-05 12:26  Reverist  阅读(25)  评论(0编辑  收藏  举报