字符串简单题目 1

luogu4391 无线传输

分析

题目说的很别扭,实际上是求一个最短的 \(S\) 的子串,满足这个子串自我拼接之后,\(S\) 是这个拼接串的子串。也就是 \(S\) 的最小周期。

\(|S|=n\),则答案为 \(n-next_n\)。证明如下:

如下图表示 \(S\) 的最大 Border 相交的情况,黑色部分是字符串 \(S\),蓝色部分是和红色部分是 \(next_n\),绿色部分 \(T=S[1,n-next_n]\)

显然 \(T\)\(S[1,next_n]\) 的一个前缀,又因为 \(S[1,next_n] = S[n-next_n+1,n]\),所以 \(T\) 后面又能再拼接上一块 \(T\)。而前面的黄色部分 \(S[n-next_n+1,next_n]\) 是前缀 Border 的最后 $next_n - (n-next_n+1) + 1 = 2 next_n - n $ 个字符,后面的黄色部分是后缀 Border 的最后 \(n - 2(n-next_n) = 2next_n - n\) 个字符,所以黄色这这两块是等价的。由于左边的黄色部分是 \(T\) 的一个前缀,所有右边的黄色部分也是 \(T\) 的前缀,于是在第二个 \(T\) 后面再接一个 \(T\),此时 \(S\) 必然为它的子串。

那么最大 Border 不重叠呢?看下图,颜色所代表的信息与上图相同。不难发现红色部分就是等价于蓝色部分,蓝色部分是 \(T\) 的前缀。所以再接一个 \(T\) 就能覆盖整个串了。

综上,\(n-next_n\) 一定是 \(S\) 的一个周期。

\(next_n\)\(S\) 的最大 Border,对应的 \(n-next_n\) 就是 \(S\) 的最小周期。

luogu3435 Periods of Words

上题证明了,\(n-next_n\)\(S\) 的最小周期,同理 \(i-next_i\) 是前缀 \(S[1,i]\) 的最小周期。由于 \(next_{next_i}\)\(S[1,i]\) 的次小 Border,所以只要对于一个前缀 \(S[1,i]\),只要找到最小的非 \(0\) border \(t\),最大周期即为 \(i-t\)。方法是令 \(t=i\),如果 \(next_t > 0\),那么就不断让 \(t=next_t\)。这个过程也是可以记忆化的。

luogu4824 Censoring

套路性的维护一个栈,然后如果匹配到一个 \(T\) 串,那么那么就让栈顶减小 \(|T|\) 即可。最后留在栈里的即为答案。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n, m, top, nxt[N], f[N], stk[N];
char a[N], b[N];
void pre() {
	nxt[1]=0;
	for(int i=2,j=0;i<=n;++i) {
		while(j&&a[i]!=a[j+1]) j=nxt[j];
		j+=a[i]==a[j+1];
		nxt[i]=j;
	}
}
signed main() {
	scanf("%s%s",b+1,a+1);
	n=strlen(a+1), m=strlen(b+1);
	pre();
	for(int i=1,j=0;i<=m;++i) {
		while(j&&b[i]!=a[j+1]) j=nxt[j];
		j+=b[i]==a[j+1];
		f[i]=j;
		stk[++top]=i;
		if(f[i]==n) top-=n, j=f[stk[top]];
	}
	for(int i=1;i<=top;++i) printf("%c",b[stk[i]]);
	puts("");
}

luogu2375 动物园

要求前缀和后缀不重叠还要计数。

\(f_i\) 表示 \(S[1,i]\) 的 Border 的个数,也就是如果 \(next_i = j\),那么有 \(f_i = f_j +1\)

对于每一个 \(i\),找到最大的满足 \(i \ge 2j\) 的 Border \(j\),此时 \(num_i = f_j\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5, mod=1e9+7;
int t, n, ans, nxt[N], f[N];
char s[N];
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
void pre() {
	nxt[1]=0, f[1]=1;
	for(int i=2,j=0;i<=n;++i) {
		while(j&&s[i]!=s[j+1]) j=nxt[j];
		if(s[i]==s[j+1]) ++j;
		nxt[i]=j;
		f[i]=f[j]+1;
	}
}
void solve() {
	ans=1;
	scanf("%s",s+1), n=strlen(s+1);
	pre();
	for(int i=2,j=0;i<=n;++i) {
		while(j&&s[i]!=s[j+1]) j=nxt[j];
		if(s[i]==s[j+1]) ++j;
		while(i<2*j) j=nxt[j];
		(ans*=f[j]+1)%=mod;
	}
	printf("%lld\n",ans);
}
signed main() {
	t=read();
	while(t--) solve();
}

luogu3426 Template

首先印章上的字符串一定是原串的一个 Border,但是原串的 Border 不一定合法。

而如果一个 Border 合法,那么所有 Border 为它的下标之间的间隔绝对小于它的长度。这样就可以完成整个串的覆盖。观察样例就能得到这两个结论。

所以答案就要从原串 \(S\) 的 Border 里面选择,优先考虑最小的。

\(ans_i\) 为候选答案中第 \(i\) 小的答案。考虑 \(ans_{i-1}\) 不合法,但是 \(ans_i\) 合法的情况。

由于 \(ans_{i-1}\) 不合法,所以要排除所有 Border 为 \(ans_{i-1}\) 且不为 \(ans_i\) 的下标(\(ans_{i-1}\) 一定是 \(ans_i\) 的 Border)。排除之后,又要再次统计 Border 为 \(ans_i\) 的下标的间隔。可以用链表来实现快速查询和删除。

如何快速实现排除下标呢?引入一种数据结构,名为——失配树。也叫 \(fail\) 树和 \(next\) 树。简而言之,就是把 \(next_i\)\(i\) 连一条边。

它的主要性质是:点 \(x\) 如果是点 \(y\) 的祖先,那么 \(S[1,x]\)\(S[1,y]\) 的一个 Border。如果 \(x\)\(y\) 之间没有祖孙关系,那么绝不满足上述性质。

也就是说,删去所有 Border 为 \(ans_{i-1}\) 且 Border 不为 \(ans_i\) 的下标,只要在失配树中删去 \(ans_{i-1}\) 子树内所有非 \(ans_i\) 的节点即可。

从小到大枚举答案,如果某个 \(ans_i\) 满足了,答案就是 \(ans_i\)

参考:[POI2005][luogu3462] SZA-Template [fail树]

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+5;
int n, m, gap, rec, fail[N], pre[N], suf[N], ans[N];
int tot, h[N], to[N], nxt[N];
char s[N];
void add(int x,int y) {
	to[++tot]=y, nxt[tot]=h[x], h[x]=tot;
}
void prepre() {
	fail[1]=0;
	for(int i=2,j=0;i<=n;++i) {
		while(j&&s[i]!=s[j+1]) j=fail[j];
		if(s[i]==s[j+1]) ++j;
		fail[i]=j;
	}
	for(int i=1;i<=n;++i) add(fail[i],i);
    // 每一个点都要加入失配树
	for(int i=n;i;i=fail[i]) ans[++m]=i;
    // 这里是从大到小存的
	// for(int i=1;i<=m;++i) printf("%lld\n",ans[i]);
	for(int i=1;i<=n;++i) pre[i]=i-1, suf[i]=i+1;
}
void del(int x) {
	suf[pre[x]]=suf[x], pre[suf[x]]=pre[x];
	gap=max(gap,suf[x]-pre[x]);
	pre[x]=suf[x]=0;
}
void dfs(int x,int zero) {
	del(x);
	for(int i=h[x];i;i=nxt[i]) {
		int y=to[i];
		if(y==zero) continue;
		del(y);
		dfs(y,zero);
	}
}
signed main() {
	scanf("%s",s+1);
	n=strlen(s+1);
	prepre();
	for(int i=m;i;--i) {
		dfs(ans[i+1],ans[i]);
        // 这个可以模拟一下,注意第一次先删去了所有根节点0所有Border不是ans[m]的节点
		if(gap<=ans[i]) { rec=ans[i]; break; }
	}
	printf("%lld\n",rec);
}

CF126B Password

既是前缀,又是后缀,又在中间出现过。

不难想到,既是前缀又是后缀,说明一定是 \(S\) 的一个 Border,所以答案最大为 \(next_n\)。在中间也出现过,假设出现的位置是 \(i_0\),那么答案也是 \(S[1,i_0]\) 的一个 Border。因此只要从大到小寻找满足

  1. \(S\) 的 Border。
  2. 存在至少一个 \(i \in [1,n-1]\),满足 \(next_j = i\)

的最大的 \(j\) 即可。

CODE

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n, nxt[N], v[N];
char s[N];
int main() {
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=2,j=0;i<=n;++i) {
		while(j&&s[i]!=s[j+1]) j=nxt[j];
		if(s[i]==s[j+1]) ++j;
		nxt[i]=j;
		if(i!=n) v[j]=1;
	}
	int ans=nxt[n], fg=0;
	while(ans) {
		if(v[ans]) { fg=1; break; }
		ans=nxt[ans];
	}
	if(!fg) { puts("Just a legend"); return 0; }
	for(int i=1;i<=ans;++i) printf("%c",s[i]);
	puts("");
}

CF858D Polycarp's phone book

对于每个字符串 \(S_i\),找到一个最短的子串,满足这个子串是且只是 \(S_i\) 的子串。

一个想法把所有字符串的所有子串插入集合,对于每个字符串 \(S_i\),从小到大枚举所有子串,在不考虑 \(S_i\) 的所有子串的情况下查看是否存在这个子串。

这个显然是不行的,考虑优化。注意到,只要把所有后缀都插入集合,枚举的时候只枚举起点,就能找到所有 \(S_i\) 的子串。

既然是和字符串相关,那么这个集合可以用 Trie 来在一个比较优的复杂度内实现。

考虑什么时候可行。设 \(cnt_x\) 表示字符 \(x\) 在集合中的出现次数,如果 \(S_i\) 的某个子串的结尾字符的 \(cnt\)\(0\),由于之前已经删掉了 \(S_i\) 的所有后缀,所以此时这个子串一定是唯一的,输出即可。然后再把所有后缀插入回去。

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n, tot, d, cnt[N], trie[N][15];
char a[N][15], ans[15], t[15];
int read() {
	int a=0, f=1; char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) a=a*10+c-'0', c=getchar();
	return a*f;
}
void insert(char* s) {
	int x=0, len=strlen(s);
	for(int i=0;i<len;++i) {
		int a=s[i]-'0';
		if(!trie[x][a]) trie[x][a]=++tot;
		x=trie[x][a], ++cnt[x];
	}
}
void del(char* s) {
	int x=0, len=strlen(s);
	for(int i=0;i<len;++i) {
		int a=s[i]-'0';
		x=trie[x][a], --cnt[x];
	}
}
void find(char* s) {
	int x=0, len=strlen(s);
	for(int i=0;i<len;++i) {
		t[i]=s[i];
		int a=s[i]-'0';
		x=trie[x][a];
		if(cnt[x]==0) {
			if(i+1<d) {
				d=i+1;
                //此时答案长度为i+1
				t[i+1]='\0';
                // 截取t[0,i]这一段,因为puts()遇到'\0'会停止
				strcpy(ans,t);
				break;
			}
		}
	}
}
int main() {
	n=read();
	for(int i=1;i<=n;++i) {
		scanf("%s",a[i]);
		for(int j=0;j<9;++j) insert(a[i]+j);
	}
	for(int i=1;i<=n;++i) {
		d=10;
		for(int j=0;j<9;++j) del(a[i]+j);
		for(int j=0;j<9;++j) find(a[i]+j);
		for(int j=0;j<9;++j) insert(a[i]+j);
		puts(ans);
	}
}

字符串简单题目 1
https://yozora0908.github.io/2022/string-solution-1/
作者
yozora0908
发布于
2022年8月9日
许可协议