description
JOJO 的奇幻冒险是一部非常火的漫画。漫画中的男主角经常喜欢连续喊很多的「欧拉」或者「木大」。
为了防止字太多挡住漫画内容,现在打算在新的漫画中用 (x) 欧拉或者 (x) 木大表示有 (x) 个欧拉或者木大。
为了简化内容我们现在用字母表示喊出的话。
我们用数字和字母来表示一个串,例如:2 a 3 b
表示的串就是 aabbb
。
一开始漫画中什么话都没有,接下来你需要依次实现 (n) 个操作,总共只有 (2) 种操作:
- 第一种:
1 x c
:在当前漫画中加入 (x) 个 (c),表示在当前串末尾加入 (x) 个 (c) 字符。保证当前串是空串或者串尾字符不是 (c); - 第二种:
2 x
:觉得漫画没画好,将漫画还原到第 (x) 次操作以后的样子,表示将串复原到第 (x) 次操作后的样子,如果 (x = 0) 则是将串变成空串。如果当前串是bbaabbb
,第 (4) 次操作后串是bb
,则2 4
会使bbaabbb
变成bb
,保证 (x) 小于当前操作数。
众所周知空条承太郎十分聪明,现在迪奥已经被打败了,他开始考虑自己的漫画中的一些问题:
对于一个串的每个前缀 (A),都有一个最长的比它短的前缀 (B) 与前缀 (A) 的一个后缀匹配,设这个最长的前缀 (B) 的长度为 (L)。(L) 为 (0) 时意味着 (B) 是一个空串。
每一次操作后,你都需要将当前的串的所有前缀的 (L) 求和并对 (998244353) 取模输出告诉空条承太郎,好和他的白金之星算出的答案对比。比如 bbaaabba
的 (L) 分别是 (0,1,0,0,0,1,2,3),所以对于这个串的答案就是 (7)。
solution
假如存在 s[1...x] = s[n-x+1...n],则中间完整的块必须字符与个数一一对应相等。
由此,我们不妨把某次操作增加的 (x, c) 视作二元组,对二元组做 kmp 求最大 border。
不过这里要特判一下第一个块:后半部分的第一个块长度 ≥ 前半部分第一个块(就是整个串第一个块)长度,且它们的字符相同,则认为它们相等。
可以发现这样操作依然满足 border 的传递性,实现时只在需要判相等时这样搞一下。
至于求答案。如果是整个串第一个块特判;否则边跳 fail 边记录当前已经匹配的最大值(需要注意即使存在长度 > 新加入的串长度也不能停止跳 fail,必须严格等于)。
还要特判上文所述 “后半部分的第一个块长度 ≥ 前半部分第一个块” 情况。
不过题目中还有 2 操作,离线下来发现就是 trie 上求最大 border。
然而 kmp 的复杂度是均摊的(虽然这道题你暴力跳好像也能过),我们考虑改一改求最大 border 的方法。
一种方法是建 O(∑)(字符集大小)个可持久化线段树,维护加入某一类字符共 k 个所转移到的点以及贡献。每次加入新点只会修改 O(1) 棵线段树。
另一种是利用 border 与循环的性质(形成 O(log) 个等差数列),每次如果当前循环节大小与上一次循环节大小相等,就可以直接对循环节取模。
accepted code
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 100000;
const int MOD = 998244353;
inline int add(int x, int y) {x += y; return x >= MOD ? x - MOD : x;}
inline int sub(int x, int y) {x -= y; return x < 0 ? x + MOD : x;}
inline int mul(int x, int y) {return (int) (1LL * x * y % MOD);}
struct type{
int ch, cnt;
friend bool operator == (const type &a, const type &b) {
return (a.ch == b.ch) && (a.cnt == b.cnt);
}
}a[MAXN + 5];
vector<int>ch[MAXN + 5]; int ncnt;
int add(int x, type t) {
a[++ncnt] = t, ch[x].push_back(ncnt);
return ncnt;
}
type stk[MAXN + 5]; int tp;
void update(int &res, int &lens, int k, int lim) {
if( lim > lens ) {
res = add(res, mul(sub(lim, lens), k));
res = add(res, (int)(1LL*(lens + 1 + lim)*(lim - lens)/2%MOD));
lens = lim;
}
}
int res[MAXN + 5], len[MAXN + 5], fail[MAXN + 5];
void insert(int x, type t) {
int nw = fail[tp], lens = 0, lst = -1;
stk[++tp] = t, len[tp] = len[tp - 1] + t.cnt, res[x] = 0;
while( nw != -1 ) {
if( stk[nw + 1].ch == t.ch ) {
update(res[x], lens, len[nw], min(stk[nw + 1].cnt, t.cnt));
if( nw == 0 && stk[1].cnt < t.cnt ) {
res[x] = add(res[x], mul(t.cnt - lens, stk[1].cnt));
break;
} else if( stk[nw + 1].cnt == t.cnt )
break;
}
if( nw && nw - fail[nw] == lst && 2*lst < nw )
nw %= lst;
else nw = fail[nw];
lst = nw - fail[nw];
}
fail[tp] = nw + 1;
if( tp == 1 )
res[x] = add(res[x], (int)(1LL*(t.cnt - 1)*t.cnt/2%MOD));
}
void dfs(int x, int ans) {
insert(x, a[x]), res[x] = add(res[x], ans);
for(unsigned i=0;i<ch[x].size();i++)
dfs(ch[x][i], res[x]);
tp--;
}
int id[MAXN + 5];
int main() {
int n; scanf("%d", &n);
for(int i=1,op,x;i<=n;i++) {
scanf("%d%d", &op, &x);
if( op == 1 ) {
char str[2]; scanf("%s", str);
id[i] = add(id[i - 1], (type){str[0] - 'a', x});
} else id[i] = id[x];
}
fail[0] = -1;
for(unsigned i=0;i<ch[0].size();i++) dfs(ch[0][i], 0);
for(int i=1;i<=n;i++) printf("%d
", res[id[i]]);
}
details
对于第二种方法,注意不能当循环节长度 < len/2 时就直接模循环节。
感性解释一下:因为我所要匹配的是 fail + 1 的位置,而循环节只能保证 [1...fail] 的有循环节。