【ACWing】2572. 生成魔咒

题目地址:

https://www.acwing.com/problem/content/2574/

魔咒串由许多魔咒字符组成,魔咒字符可以用数字表示。例如可以将魔咒字符 1 , 2 1, 2 1,2拼凑起来形成一个魔咒串 [ 1 , 2 ] [1,2] [1,2]。一个魔咒串 S S S的非空子串被称为魔咒串 S S S的生成魔咒。例如 S = [ 1 , 2 , 1 ] S=[1,2,1] S=[1,2,1]时,它的生成魔咒有 [ 1 ] , [ 2 ] , [ 1 , 2 ] , [ 2 , 1 ] , [ 1 , 2 , 1 ] [1],[2],[1,2],[2,1],[1,2,1] [1],[2],[1,2],[2,1],[1,2,1]五种。 S = [ 1 , 1 , 1 ] S=[1,1,1] S=[1,1,1]时,它的生成魔咒有 [ 1 ] , [ 1 , 1 ] , [ 1 , 1 , 1 ] [1], [1,1],[1,1,1] [1],[1,1],[1,1,1]三种。最初 S S S为空串。共进行 n n n次操作,每次操作是在 S S S的结尾加入一个魔咒字符。每次操作后都需要求出,当前的魔咒串 S S S共有多少种生成魔咒。

输入格式:
第一行一个整数 n n n
第二行 n n n个数,第 i i i个数表示第 i i i次操作加入的魔咒字符。

输出格式:
输出 n n n行,每行一个数。第 i i i行的数表示第 i i i次操作后 S S S的生成魔咒数量。

数据范围:
1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105
用来表示魔咒字符的数字 x x x满足 1 ≤ x ≤ 1 0 9 1≤x≤10^9 1x109

相当于是动态求一个字符串有多少个本质不同的子串。求一个字符串有多少个本质不同子串可以用后缀数组来做,参考https://blog.csdn.net/qq_46105170/article/details/125131642,设字符串长 n n n,高度数组为 h h h,则本质不同子串个数为 n ( n + 1 ) 2 − ∑ i = 2 n h [ i ] \frac{n(n+1)}{2}-\sum_{i=2}^n h[i] 2n(n+1)i=2nh[i]。但是这道题是在后面添加字符,这样一来每个后缀都会发生变化,不好求,可以将整个字符串翻转,这样变成了每次从开头添加字符,之前的后缀都不变,只是添加了一个新后缀。但是,考虑添加一个新后缀仍然不方便更新 h h h,我们可以反过来做,直接将最后所得的字符串 s s s求出,然后每次删除首字母,这样就变成每次删除一个后缀,由于有 r k rk rk数组,这个后缀的排名是知道的,再用一个双链表维护每个后缀字典序小 1 1 1的后缀是哪一个,例如后缀 x x x的字典序的后一个后缀是 d [ x ] d[x] d[x],这样更新 h h h的时候,由 l c p lcp lcp的性质, h [ d [ r k [ x ] ] ] h[d[rk[x]]] h[d[rk[x]]]更新为 min ⁡ { h [ r k [ x ] ] , h [ d [ r k [ x ] ] ] } \min\{h[rk[x]],h[d[rk[x]]]\} min{h[rk[x]],h[d[rk[x]]]}即可,同时再维护子串个数表达式的第一项和第二项。代码如下:

#include <iostream>
#include <cstring>
#include <unordered_map>
using namespace std;

const int N = 1e5 + 10;
int n, m;
int s[N];
int sa[N], rk[N], y[N], c[N], he[N];
int u[N], d[N];
long res[N];

// 需要离散化
int get(int x) {
  static unordered_map<int, int> mp;
  if (!mp.count(x)) mp[x] = ++m;
  return mp[x];
}

void get_sa() {
  for (int i = 1; i <= n; i++) c[rk[i] = s[i]]++;
  for (int i = 2; i <= m; i++) c[i] += c[i - 1];
  for (int i = n; i; i--) sa[c[rk[i]]--] = i;

  for (int k = 1;; k <<= 1) {
    int num = 0;
    for (int i = n - k + 1; i <= n; i++) y[++num] = i;
    for (int i = 1; i <= n; i++) if (sa[i] > k) y[++num] = sa[i] - k;
    for (int i = 1; i <= m; i++) c[i] = 0;
    for (int i = 1; i <= n; i++) c[rk[i]]++;
    for (int i = 2; i <= m; i++) c[i] += c[i - 1];
    for (int i = n; i; i--) sa[c[rk[y[i]]]--] = y[i];
    swap(rk, y);
    rk[sa[1]] = num = 1;
    for (int i = 2; i <= n; i++)
      rk[sa[i]] = y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k] ? num : ++num;
    if (num == n) break;
    m = num;
  }
}

void get_height() {
  for (int i = 1, k = 0; i <= n; i++) {
    if (rk[i] == 1) continue;
    if (k) k--;
    int j = sa[rk[i] - 1];
    while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
    he[rk[i]] = k;
  }
}

int main() {
  scanf("%d", &n);
  for (int i = n; i; i--) scanf("%d", &s[i]), s[i] = get(s[i]);

  get_sa();
  get_height();
  
  long ans = 0;
  // 按排名枚举后缀
  for (int i = 1; i <= n; i++) {
  	// 右边是排名第i的后缀有多少个前缀之前没算过
    ans += n - sa[i] + 1 - he[i];
    // 构造出双链表
    u[i] = i - 1, d[i] = i + 1;
  }
  // 开两个哨兵
  d[0] = 1, u[n + 1] = n;

  for (int i = 1; i <= n; i++) {
    res[i] = ans;
    // 每次删掉第i个字符开始的后缀
    int k = rk[i], j = d[k];
    // 清空掉排名第k后缀和排名第j后缀的贡献
    ans -= n - sa[k] + 1 - he[k];
    ans -= n - sa[j] + 1 - he[j];
    // 更新高度数组
    he[j] = min(he[j], he[k]);
    // 将排名第j的后缀的贡献加回来
    ans += n - sa[j] + 1 - he[j];
    // 双链表中删掉第i个字符开始的后缀
    d[u[k]] = d[k], u[d[k]] = u[k];
  }
	
  // 要逆序输出答案
  for (int i = n; i; i--) printf("%ld\n", res[i]);
}

时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值