[NOIP2020] 字符串匹配

[NOIP2020] 字符串匹配

去年的我题都看错了, 看对了之后也完全没有头绪, 今年我虽然还是菜的一批, 但是能自己做做了.

虽然一看到的时候仍然没有任何头绪, 然后就开始写, 写自己能想到的所有东西, 在纸上画, 画这个串, 手玩, 发现一些性质.

心路历程

首先, 我们忽视 \(F(A) \le F(C)\) 这个条件, 那么 \(C\) 的选择就有 \(n - 2\) 种, \(|(AB)^i| \in [2, n - 1]\) .

那么我们需要考虑的就是 \(AB\) 该怎么选.

我们设去掉 \(C\) 的串长为 \(m\) , \(|AB|\)\(l\) , 那么 \(l | m\) . 我们就只需要求 \(AB\) 是否是剩下的一段的循环节就行了, 而 \(m\) 的质因子最多有 \(\log m\) 个, 我们有 \(n\)\(m\) , 每个 \(m\)\(\log m\) 个质因子, 所以我们只需要判断 \(n \log m\)\(AB\) .

接下来考虑如何判断 \(AB\) 是不是循环节.

这里我们有一个结论, 这里定义三个串, \(A, B, C\) , 这里的 \(ABC\) 跟上面无关. \(|ABC| \mod\ |A| == 0\) , \(AB == BC\) , 那么 \(A\) 就是 \(ABC\) 的一个循环节.

由上面的结论, 我们可以预处理出原串的 \(Hash\) 然后 \(O(1)\) 判断, 如果 \((AB)^i\) 是合法的, 那么 \((aAB)^{i/a}\) 也合法. 所以 \(AB\) 的贡献就是 \(|AB| - 1 + 2|AB| - 1 + ... + i|AB| - 1\) , 也就是 \((1 + i)i / 2 * |AB| - i\) . (或者先打标记, 然后统一统计, 这样应该会好写很多我感觉, 有点类似线性筛的感觉) .

这样我们就用 \(n \log m\) 的时间把对于每个 \(C\) 的所有可以作为循环节的长度求出来了, 接下来考虑 \(F(A) \le F(C)\) 这个限制.

\(F(S)\)\(S\) 中出现奇数次的字符的数量, 考虑一下有没有什么性质.

由于 \(A\) 一定是前缀, \(C\) 一定是后缀, 那么我们就可以正反分别扫一遍, 记录每个前缀和后缀的出现奇数次的字符数, 复杂度 \(O(n)\) , 完全可行.

话不多说, 开干!

正文

其实我在写代码的时候发现我上面的思路其实是有问题的, 但是仍然提供了非常多的可行之处, 主要是各种性质, 使得我们可以在很短的时间内进行判断, 但是我没有找到优化枚举的手段.

我这里没有找到任何优化枚举的手段, 所以只能给 \(O(n \log n)\) 枚举, 枚举 \(AB\) 的长度以及重复的次数.

因为我没有优化枚举的手段, 那么我们就让 \(Judge\) 尽可能的快.

我先打了个暴力, 用 \(Hash\) \(O(1)\) 判断 \(AB\) 是否是循环节以及 \(F(A), F(C)\) .

\(code:\)

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

inline int read() {
  int x = 0, f = 1;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
  return x * f;
}

const int N = 1 << 21, bs = 27;

struct sovle {
  int n, cnt1[N], cnt2[N], tot[30];
  ull hs[N], pw[N];
  char s[N];

  void get_hash() {
    pw[0] = 1;
    for (int i = 1; i <= n; i++) {
      hs[i] = hs[i - 1] * bs + s[i] - 'a' + 1;
      pw[i] = pw[i - 1] * bs;
    }
  }

  void get_cnt() {
    memset(cnt1, 0, sizeof(cnt1));
    memset(cnt2, 0, sizeof(cnt1));
    memset(tot, 0, sizeof(tot));
    for (int i = 1; i <= n; i++) {
      int j = s[i] - 'a';
      tot[j]++;
      cnt1[i] = cnt1[i - 1] + (tot[j] & 1 ? 1 : -1);
    }
    memset(tot, 0, sizeof(tot));
    for (int i = n; i >= 1; i--) {
      int j = s[i] - 'a';
      tot[j]++;
      cnt2[i] = cnt2[i + 1] + (tot[j] & 1 ? 1 : -1);
    }
  }

  inline ull get(int l, int r) {
    return hs[r] - hs[l - 1] * pw[r - l + 1];
  }

  void run() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    ll ans = 0;
    get_hash();
    get_cnt();
    for (int i = 2; i < n; i++) {
      for (int j = i; j < n; j += i) {
        if (get(1, i) == get(j - i + 1, j)) {
          for (int k = 1; k < i; k++) {
            if (cnt1[k] <= cnt2[j + 1]) ans++;
          }
        }
        else break;
      }
    }
    printf("%lld\n", ans);
  }
} S;

int main() {
  int n = read();
  for (int i = 1; i <= n; i++) S.run();
  return 0;
}

\(48pts\)

复杂度 \(O(TN^2 \log N)\) .

然后我们发现, 我们其实不必要枚举 \(A\) 的长度, 因为我们只要 \(F(A) \le F(C)\) 就合法了, 所以我们可以用一个前缀和优化, 记 \(cnt[i][k]\) 表示长度为 \(i\) 的前缀中 \(F(A) \le k\)\(A\) 的个数, 这样我们 \(O(26N)\) 预处理, \(O(1)\) 查询, 总复杂度 \(O(26TN + TN \log N)\)

\(code:\)

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

inline int read() {
  int x = 0, f = 1;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
  return x * f;
}

const int N = 1 << 21, bs = 27;

struct sovle {
  int n, cnt1[N], cnt2[N], cnt[N][27], tot[30];
  ull hs[N], pw[N];
  char s[N];

  void get_hash() {
    pw[0] = 1;
    for (int i = 1; i <= n; i++) {
      hs[i] = hs[i - 1] * bs + s[i] - 'a' + 1;
      pw[i] = pw[i - 1] * bs;
    }
  }

  void get_cnt() {
    memset(cnt1, 0, sizeof(cnt1));
    memset(cnt2, 0, sizeof(cnt1));
    memset(tot, 0, sizeof(tot));
    for (int i = 1; i <= n; i++) {
      int j = s[i] - 'a';
      tot[j]++;
      cnt1[i] = cnt1[i - 1] + (tot[j] & 1 ? 1 : -1);
      for (int k = 0; k < cnt1[i]; k++) cnt[i][k] = cnt[i - 1][k];
      for (int k = cnt1[i]; k < 27; k++) cnt[i][k] = cnt[i - 1][k] + 1;
    }
    memset(tot, 0, sizeof(tot));
    for (int i = n; i >= 1; i--) {
      int j = s[i] - 'a';
      tot[j]++;
      cnt2[i] = cnt2[i + 1] + (tot[j] & 1 ? 1 : -1);
    }
  }

  inline ull get(int l, int r) {
    return hs[r] - hs[l - 1] * pw[r - l + 1];
  }

  void run() {
    scanf("%s", s + 1);
    n = strlen(s + 1);
    ll ans = 0;
    get_hash();
    get_cnt();
    for (int i = 2; i < n; i++) {
      for (int j = i; j < n; j += i) {
        if (get(1, i) == get(j - i + 1, j)) {
          ans += cnt[i - 1][cnt2[j + 1]];
        }
        else break;
      }
    }
    printf("%lld\n", ans);
  }
} S;

int main() {
  int n = read();
  for (int i = 1; i <= n; i++) S.run();
  return 0;
}

\(84pts\)

然后 \(O2\) 一开, 谁也不爱(bushi

这样其实就差不多了 \(qwq\) , 比去年的我已经强太多了, 在基于我的能力下, \(84pts\) 所花费的时间与分数的比是最优解, 现在最重要的已经不是切题了, 而是在最短的时间内拿最多的分, 不过正解思路还是要有的.

posted @ 2021-10-08 10:59  sshadows  阅读(154)  评论(1编辑  收藏  举报