【loj - 3055】「HNOI2019」JOJO


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\n", res[id[i]]);
}

details

对于第二种方法,注意不能当循环节长度 < len/2 时就直接模循环节。
感性解释一下:因为我所要匹配的是 fail + 1 的位置,而循环节只能保证 [1...fail] 的有循环节。

posted @ 2020-06-11 17:43  Tiw_Air_OAO  阅读(310)  评论(0编辑  收藏  举报