一些套路的字符串

字符串

CF547E Mike and Friends

题目大意

给定 \(n\) 个字符串 \(s_{1...n}\)\(q\) 次询问,每次询问给定 \(k, l, r\)\(s_k\)\(s_{l...r}\) 内出现了几次。

题解

不是很难,不知道为啥 \(CF\) 评分这么高。

首先建出 \(AC\) 自动机,根据 \(AC\) 自动机的性质,一个字符串在其它字符串种出现的次数等于 \(fail\) 树上当前节点子树内其它字符串的出现次数,不难想到把询问差分,用树状数组维护 \(fail\) 树的 \(dfs\) 序,依次将每个串插入树状数组回答询问即可。

不难写不放代码了。。。。

CF204E Little Elephant and Strings

题目大意

给定 \(n\) 个字符串 \(a_{1...n}\) ,对于每个字符串 \(a_i\) 求出 \(a_i\) 有多少二元组 \((l, r)\) 满足 \(a_i\) 的子串 \([l, r]\) 是至少 \(k\) 个字符串的子串。

题解

首先建出广义 \(SAM\) ,对于每个节点,求出有多少个字符串包含这个节点所代表的子串,可以通过数上差分求得,考虑枚举一个字符串的 \(r\) ,在 \(link\) 树上大力倍增即可找到最靠左的 \(l\) ,则以 \(r\) 结尾的子串有 \(r - l + 1\) 个。

不难写不放代码了。。。

gym103427 M.String Problem

题目大意

给定一个字符串,求对于字符串的每个前缀,字典序最大的子串的长度。

题解

貌似有 \(SA\) 做法和 \(lyndon\) 分解做法,但我不会。。。所以自己口胡了 \(SAM\) 。。。官方题解要么看不懂,要么及其敷衍。。。看来我还是太菜了。。。

首先不难发现,每个前缀内字典序最大的子串实际上是该前缀的一个后缀。考虑动态的求解这个问题,字典序最大的后缀实际上对应了从开始节点到结束节点的一条路径,可以根据 \(SAM\) 的性质,动态的维护这条路径。

考虑每次新加入一个字符,路径会发生什么变化,对于路径中的一个节点,如果它有了更大的出边,且之前是 \(SAM\) 上的结束节点(不一定是路径的结束节点),那么从它走向更大的出边可以形成一个字典序更大的后缀,且这个节点越短,新形成的串字典序越大。考虑到 \(SAM\) 所有的结束节点都在从 \(last\) 到根的一条链上,且没有这个出边的点是连续的一段,那么可以在跳父亲时完成这一工作,由于越往上跳代表的串越短,所以找到最靠上的路径中的节点即可。

记找到的点为 \(cur\) ,不难发现之前路径一定以 \(last\) 结尾,那么从 \(last\) 开始回溯,直到 \(cur\) ,再从 \(cur\) 走到最新插入的节点 \(now\) 便得到最大后缀。

注意在复制节点时,若遇到路径中的点,需要原封不动的将路径信息复制过去,这一部分细节较多。

code
#include <bits/stdc++.h>

const int N = 2e6 + 10;

int read(void){
	int f = 1, x = 0; char ch = getchar();
	while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
	while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
	return f * x;
}

struct SAm{
	int to[N][26], len[N], link[N], siz, lst, pos;
	int l[N], pre[N], nxt[N];
	bool vis[N];
	void init() { vis[siz = lst = pos = 1] = 1; }
	int insert(char ul){
		int now = ++siz, p = lst, cur = pos; len[now] = len[p] + 1;
		while(p && !to[p][ul - 'a']){
			if(vis[p] && ul - 'a' > nxt[p]) cur = p;
			to[p][ul - 'a'] = now, p = link[p];
		}
		if(!p) link[now] = 1;
		else{
			int q = to[p][ul - 'a'];
			if(len[q] == len[p] + 1) link[now] = q;
			else{
				int cl = ++siz;
				len[cl] = len[p] + 1, link[cl] = link[q];
				memcpy(to[cl], to[q], sizeof to[q]);
				while(p && to[p][ul - 'a'] == q){
					if(vis[q] && pre[q] == p){
						vis[q] = 0, vis[cl] = 1, pre[cl] = pre[q];
						nxt[cl] = nxt[q], l[cl] = l[q];
						if(q ^ pos) pre[to[q][nxt[q]]] = cl;
						if(q == cur) cur = cl;
						if(q == pos) pos = cl;
					}
					to[p][ul - 'a'] = cl, p = link[p];
				}
				link[q] = link[now] = cl;
			}
		}
		lst = now;
		while(pos ^ cur) vis[pos] = 0, pos = pre[pos];
		pre[now] = pos, nxt[pos] = ul - 'a';
		l[now] = l[pos] + 1, vis[pos = now] = 1;
		return l[now];
	}
}S;

int n;
char ch[N];

signed main(void){
	S.init();
	scanf("%s", ch + 1); n = strlen(ch + 1);
	for(int i = 1, x; i <= n; ++i){
		x = S.insert(ch[i]);
		printf("%d %d\n", i - x + 1, i);
	}
	return 0;
}

CF700E Cool Slogans

题目大意

给定一个字符串 \(S\) ,求构造最长的字符串序列 \(t_{1...k}\) ,满足 \(\forall i, t_i\)\(S\) 的子串,且 \(\forall 2 \le i \le k, t_{i-1}\)\(t_i\) 中出现了两次,输出序列长度。

题解

首先不难发现,题中的 \(t_{i-1}\) 一定是 \(t_i\) 的一个后缀,如果不是,那么将 \(t_i\) 末尾不是 \(t_{i-1}\) 后缀的部分删去一定不劣。

先建出 \(SAM\) ,设 \(f_i\) 表示以节点 \(i\) 为结尾时序列的长度,考虑如何转移,\(i\) 号节点只能通过 \(i\) 在后缀链接树上的祖先转移过来,然而这样只能保证 \(t_{i-1}\)\(t_i\) 中出现一次,那么只要用线段树合并维护每个节点的 \(endpos\) 集合即可判断第二次出现。于是在后缀链接树上倍增找最深的满足条件的点即可。然而这样是两只 \(log\) 的,实际上只需要记录下每个点是通过那个点转移过来的便可以省去倍增。

code
#include <bits/stdc++.h>

const int N = 4e5 + 10;

int read(void){
	int f = 1, x = 0; char ch = getchar();
	while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
	while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
	return f * x;
}

int n;
char ch[N];
int rt[N], tot;
struct TRE { int ls, rs; } t[N << 5];
void add(int &p, int x, int l = 1, int r = n){
	p = ++tot;
	if(l == r) return; int mid = (l + r) >> 1;
	if(x <= mid) add(t[p].ls, x, l, mid);
	else add(t[p].rs, x, mid + 1, r);
}
void merge(int &l, int r){
	if(!l || !r) return l |= r, void();
	t[++tot] = t[l]; l = tot;
	merge(t[l].ls, t[r].ls);
	merge(t[l].rs, t[r].rs);
}
bool que(int p, int l, int r, int L = 1, int R = n){
	if(l > r || !p) return 0;
	if(l <= L && r >= R) return 1; int mid = (L + R) >> 1;
	if(l <= mid && que(t[p].ls, l, r, L, mid)) return 1;
	if(r > mid && que(t[p].rs, l, r, mid + 1, R)) return 1;
	return 0;
}

struct SAm{
	int to[N][26], len[N], link[N], pos[N], t[N], id[N], siz, lst;
	void init() { lst = siz = 1; }
	void insert(char ul, int id){
		int now = ++siz, p = lst; len[now] = len[p] + 1, pos[now] = id;
		while(p && !to[p][ul - 'a']) to[p][ul - 'a'] = now, p = link[p];
		if(!p) link[now] = 1;
		else{
			int q = to[p][ul - 'a'];
			if(len[q] == len[p] + 1) link[now] = q;
			else{
				int cl = ++siz; pos[cl] = id;
				len[cl] = len[p] + 1, link[cl] = link[q];
				memcpy(to[cl], to[q], sizeof to[cl]);
				while(p && to[p][ul - 'a'] == q) to[p][ul - 'a'] = cl, p = link[p];
				link[now] = link[q] = cl;
			}
		}
		lst = now; add(rt[now], id);
	}
	int fa[N], f[N];
	int solve(){
		int ans = 0;
		for(int i = 1; i <= siz; ++i) ++t[len[i]];
		for(int i = 1; i <= siz; ++i) t[i] += t[i - 1];
		for(int i = 1; i <= siz; ++i) id[t[len[i]]--] = i;
		for(int i = siz; i > 1; --i) merge(rt[link[id[i]]], rt[id[i]]);
		for(int i = 2; i <= siz; ++i){
			int u = id[i], v = link[u];
			if(v == 1) fa[u] = u, f[u] = 1;
			else if(que(rt[fa[v]], pos[u] - len[u] + len[fa[v]], pos[u] - 1))
				f[u] = f[v] + 1, fa[u] = u;
			else f[u] = f[v], fa[u] = fa[v];
			ans = std::max(ans, f[u]);
		}
		return ans;
	}
}S;

signed main(void){
	n = read(); S.init();
	scanf("%s", ch + 1);
	for(int i = 1; i <= n; ++i) S.insert(ch[i], i);
	printf("%d\n", S.solve());
	return 0;
}

CF1483F Exam

题目大意

给定 \(n\) 个互不相同的字符串 \(s_{1...n}\) ,求有多少对 \((i, j)\) 满足 \(s_j\)\(s_i\) 的子串,且不存在 \(k\) 满足 \(s_k\)\(s_i\) 的子串且 \(s_j\)\(s_k\) 的子串。

题解

考虑对于每个 \(s_i\) 寻找合法的 \(s_j\) ,若 \(s_j\) 每次在 \(s_i\) 种出现, \(s_j = s_i[l,r]\) ,都不存在 \(k \ne j, s_k = s_i[l1, r1], l1 \le l \ \& \ r1 \ge r\) 那么 \((i, j)\) 就是合法的。考虑倒叙枚举 \(r\) ,每次找出最长的,是 \(s_i[1,r]\) 后缀的字符串,若它未被上一个字符串包含,则计算一次次数,若对于一个字符串,它在 \(s_i\) 种出现的次数恰好等于它被计算的计数,就统计入答案。

code
#include <bits/stdc++.h>

const int N = 1e6 + 10;

int read(void) {
	int f = 1, x = 0; char ch = getchar();
	while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
	while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
	return f * x;
}

#define lb(x) (x & -x)
struct BIT {
	int a[N];
	void add(int p, int x) { for(; p < N; p += lb(p)) a[p] += x; }
	int sum(int p, int res = 0) { for(; p; p -= lb(p)) res += a[p]; return res; }
	int que(int l, int r) { return sum(r) - sum(l - 1); }
} t;
#undef lb

int n, to[N][26], num[N], siz[N], fail[N], dfn[N], ed[N], Ed[N], tot, ind;
std::string s[N];
std::vector<int> l[N];

void insert(std::string ch, int id, int p = 0) {
	for(auto ul : ch) {
		if(!to[p][ul - 'a']) to[p][ul - 'a'] = ++tot;
		p = to[p][ul - 'a'];
	} Ed[ed[p] = id] = p;
}
void built(void) {
	std::queue<int> q;
	for(int i = 0; i < 26; ++i)
		if(to[0][i]) q.push(to[0][i]);
	while(!q.empty()) { 
		int p = q.front(); q.pop();
		for(int i = 0; i < 26; ++i)
			if(to[p][i])
				fail[to[p][i]] = to[fail[p]][i], q.push(to[p][i]);
			else to[p][i] = to[fail[p]][i];
		ed[p] = ed[p] ? ed[p] : ed[fail[p]], l[fail[p]].push_back(p);
	}
}

void dfs(int u) {
	dfn[u] = ++ind, siz[u] = 1;
	for(int v : l[u]) dfs(v), siz[u] += siz[v];
}

signed main(void) {
	n = read(); int res = 0;
	for(int i = 1; i <= n; ++i)
		std::cin >> s[i], insert(s[i], i);
	built(), dfs(0);
	for(int i = 1; i <= n; ++i) {
		int p = 0; std::vector<int> pos, vis;
		for(auto ul : s[i]) {
			pos.push_back(p = to[p][ul - 'a']);
			t.add(dfn[p], 1);
		}
		for(int j = pos.size() - 1, pre = N; ~j; --j) {
			int p = pos[j], id = (j == pos.size() - 1) ? ed[fail[p]] : ed[p];
			if(!id) continue; int lft = j - s[id].size();
			if(lft < pre) {
				pre = lft, ++num[id];
				if(num[id] == 1) vis.push_back(id);
			}
		}
		for(int id : vis) {
			if(t.que(dfn[Ed[id]], dfn[Ed[id]] + siz[Ed[id]] - 1) == num[id]) ++res;
			num[id] = 0;
		}
		for(int p : pos) t.add(dfn[p], -1);
	} printf("%d\n", res);
	return 0;
}

CF1037H Security

CF1286E Fedya the Potter Strikes Back

题目大意

给定一个串 \(S\) 和序列 \(W\) ,每次往串后新加一个字符,序列后新加一个数,对于区间内所有子串 \(S_{l, r}\) ,若有 $S_{1, r - l + 1} = S_{l, r} $ ,则给答案贡献一个 \(min_{i = l}^r W_i\) ,求每次新加入后的答案,强制在线。

题解

实际上就是要求动态的维护对于每一个 \(r\) ,分别有哪些 \(border\) ,再计算其贡献。

考虑新加入一个 \(r\) 对于 \(border\) 会有那些改变:
若对于之前的一个 \(border\)\(S_{l, r - 1} = S_{1, r - l}\)\(S_{r - l + 1} \neq S_r\) ,那么需要删除这个 \(border\)
\(S_1 = S_r\) 那么会新加入一个长度为 \(1\)\(border\)
考虑如何快速找出所有需要删除的 \(border\) ,对于每个前缀 \(S_{1, i}\) ,可以记录它在 \(border\) 树上离他最近的祖先 \(x\) ,满足 \(S_{x + 1} \neq S_{i + 1}\) ,这样每次在 \(border\) 树上暴力跳祖先即可,而这个祖先也可以由父亲继承而来。这样每个 \(border\) 只会被加入删除各一次,加入和删除均是 \(O(1)\) 所以均摊复杂度也为 \(O(1)\)

接下来考虑如何处理权值,每次新加入一个权值,肯定会把所有权值对它取 \(min\) ,这样可以用 \(std::map\) 开一个桶维护,每次暴力将所有大于它的暴力删掉,复杂度均摊 \(O(\log n)\) 。这样每次进行上述删除 \(border\) 的操作时还需要获取其最小值,相当于一个 \(rmq\) ,可以用各种方法解决,单调栈加二分简单又好写。

code
#include <bits/stdc++.h>

#define lll __int128
#define fi first
#define se second
const int N = 6e5 + 10;

int n, nxt[N], anc[N], w[N];
char ch[N];
lll res, sum;

void print(lll x) {
	static int stk[110], top = 0;
	if(!x) return void(puts("0"));
	while(x) stk[++top] = x % 10, x /= 10;
	while(top) putchar(stk[top--] + '0');
	return void(puts(""));
}

std::vector<int> stk;
std::map<int, int> map;
int que(int x)
	{ return *std::lower_bound(stk.begin(), stk.end(), x); }


signed main(void) {
	scanf("%d %s %d", &n, ch + 1, &w[1]);
	print(res = w[1]), stk.push_back(1);
	for(int i = 2, j = 0; i <= n; ++i) {
		scanf("%s %d", ch + i, &w[i]);
		w[i] ^= res & ((1 << 30) - 1);
		ch[i] = (ch[i] - 'a' + res % 26) % 26 + 'a';
		while(j && ch[j + 1] ^ ch[i]) j = nxt[j];
		nxt[i] = (j += ch[j + 1] == ch[i]);
		
		anc[i - 1] = ch[i] ^ ch[nxt[i - 1] + 1] ? nxt[i - 1] : anc[nxt[i - 1]];
		
		for(int p = i - 1; p; )
			if(ch[p + 1] ^ ch[i]) {
				int pos = que(i - p);
				sum -= w[pos], --map[w[pos]];
				if(!map[w[pos]]) map.erase(w[pos]);
				p = nxt[p];
			} else p = anc[p];
		if(ch[i] == ch[1]) sum += w[i], ++map[w[i]];
		while(!stk.empty() && w[stk.back()] >= w[i]) stk.pop_back();
		stk.push_back(i); int num = 0;
		for(auto it = map.upper_bound(w[i]); it != map.end(); ) {
			sum -= 1ll * (it -> fi - w[i]) * it -> se;
			num += it -> se, it = map.erase(it);
		}
		res += sum + w[stk[0]], map[w[i]] += num, print(res);
	}
	return 0;
}

ARC141 F Well-defined Abbreviation

题意

给定 \(n\) 个字符串 \(S_1 ... S_n\),定义一个串 \(T\) 为坏的当且仅当经过以下操作之后 \(T\) 不唯一。
\(\exist i, S_i\)\(T\) 的一个子串,则将 \(S_i\)\(T\) 中删除,直到不存在这样的 \(S_i\)
问是否存在这样的串 \(T\)

题解

这题还是很不错的。

乍一看不是很能做,于是先看样例,样例中给出 \(T = ABABA, S_1 = ABA\) 的情况,删除前一个和后一个 \(ABA\) 后分别剩余 \(BA, AB\)
于是受到启示,看 \(S\) 中是否存在两个字符串存在公共前后缀,若存在 \(S_i = A + B, S_j = B + C\) ,那么显然可以构造 \(T = A + B + C\) ,然而 \(A,C\) 都有可能被继续删除至最终相等,这样就不是很好处理了。于是可以考虑 \(S\) 中是否存在包含关系,若对于 \(i, \exist j\) 满足 \(S_i\) 包含 \(S_j\) ,那么考虑能否用除去 \(S_i\) 的串将 \(S_j\) 表示出来,若可以,则 \(S_i\) 是否存在对答案没有影响,不妨将其删去,否则 \(S_i\) 本身就是一个不合法的串,判断完这些之后,再去看 \(S\) 内是否存在 \(S_i = A + B, S_j = B + C\) 即可。

为表示在 \(ARC\) 中掉分的愤怒,代码先咕了。

CYH:ARC的出题人都是精神变态,出什么阴间结论题,四道题代码加起来20行,我 %¥!#¥@,下大分了。

posted @ 2022-05-19 20:11  Cyber_Tree  阅读(90)  评论(1编辑  收藏  举报