字符串合集

luqyou / 2023-05-13 / 原文

基本概念

有一个字符串 \(s\),那么它的长度记作 \(|s|\)

子串:由一个字符串 \(s\) 的一段区间 \([l,r]\) 中的字符按顺序构成的字符串称为这个字符串的子串。

前缀:由一个字符串 \(s\) 的一段区间 \([1,r]\) 中的字符按顺序构成的字符串称为这个字符串的前缀。特别地,当 \(r < |s|\) 时,这个前缀称为真前缀。

后缀:由一个字符串 \(s\) 的一段区间 \([l,|s|]\) 中的字符按顺序构成的字符串称为这个字符串的后缀。特别地,当 \(l > 1\) 时,这个前缀称为真后缀。

border:对于一个字符串 \(s\),既是 \(s\) 的前缀又是 \(s\) 的后缀的一个字符串称为 \(s\) 的 border。

字典树(Trie 树)

普通字典树

对于一堆字符串 \(s_1,s_2,s_3,\dots,s_n\),如果我们想快速查找一个字符串 \(t\) 在这一堆字符串中,有哪些是它的前缀,怎么做?

暴力扫显然是 \(O(n \times |t|)\) 的。

假如我们的字符串是 iintintegershwshw666interninternet,那么我们可以建一颗树:

image

对于每个字符串,构建方法为:

首先,\(now\) 指向根节点,然后遍历字符串:

若节点 \(now\) 没有儿子 \(s_i\),则创建一个节点 \(s_i\)\(cnt\) 加一并令 \(now=cnt\)\(cnt\) 为节点数量);

否则直接跳到该儿子。

同时,我们记录一下哪些节点是一个字符串的结尾,那么对于每个询问,我们只要爬一遍树,看看路径上有哪些节点是被标记以该节点结尾的字符串,统计一下就是答案。

例如我们查找字符串 shw114514,此时爬树时会遇到 \(10\) 号节点被标记,除此之外没有节点被标记,所以答案为 \(1\)

题:P8306 【模板】字典树

#include<bits/stdc++.h> 
using namespace std;
const int maxn=3e6+10;
int t,n,q,trie[maxn][63],cnt,e[maxn];
int id(char c){
	if('a'<=c&&c<='z') return c-'a';
	else if('A'<=c&&c<='Z') return 26+c-'A';
	else return 52+c-'0';
}
void insert(string s){
	int len=s.size(),now=0;
	s=" "+s;
	for(int i=1;i<=len;i++){
		if(!trie[now][id(s[i])]){
			trie[now][id(s[i])]=++cnt;
		}
		now=trie[now][id(s[i])];
		e[now]++;
	}
}
int query(string s){
	int len=s.size(),now=0;
	s=" "+s;
	for(int i=1;i<=len;i++){
		if(trie[now][id(s[i])]){
			now=trie[now][id(s[i])];
		}
		else{
			return 0;
		}
	}
	return e[now];
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>t;
	while(t--){
		for(int i=0;i<=cnt;i++){
			for(int j=0;j<=114;j++){
				trie[i][j]=0; 
			}
		}
		for(int i=0;i<=cnt;i++){
			e[i]=0;
		}
		cnt=0;
		cin>>n>>q;
		for(int i=1;i<=n;i++){
			string s;
			cin>>s;
			insert(s);
		}
		for(int i=1;i<=q;i++){
			string s;
			cin>>s;
			cout<<query(s)<<endl;
		}
	}
	return 0;
}

01Trie

P4551 最长异或路径

给定一棵 \(n\) 个点的带权树,结点下标从 \(1\) 开始到 \(n\)。寻找树中找两个结点,求最长的异或路径。

\(1\le n \le 100000;0 < u,v \le n;0 \le w < 2^{31}\)

首先是一个很简单的处理:我们与处理出每个节点到根节点的异或值,记为 \(s_i\),那么两个节点之间的异或路径就可以用 \(s_x \bigoplus x_y\),因为对于重复的路径,异或两次会消掉。

那么问题就变成了:计算 \(\max\limits_{1 \le i \le n,1 \le j \le n} s_i \bigoplus s_j\)

我们对于每个 \(s_i\),将它的二进制表示插入 Trie 中,然后对于每个 \(s_i\),我们树上贪心,若第 \(x\) 位为 \(1\),那么如果这一个点有儿子 \(0\),那么就往 \(0\) 方向走,否则只能往 \(1\)方向走(因为异或是要求不同才是 \(1\)),反之亦然,然后取最大值就是答案了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e6+10;
struct edge{
	int e,v;
};
int s[N],trie[N][2],n,cnt;
vector<edge> vec[N];
void dfs(int x,int fa){
	for(int i=0;i<vec[x].size();i++){
		int nx=vec[x][i].e;
		if(nx!=fa){
			s[nx]=s[x]^vec[x][i].v;
			dfs(nx,x);
		}
	}
}
void insert(int v){
	int now=0;
	for(int i=(1<<30);i;i>>=1){
		bool x=(v&i);
		if(!trie[now][x]){
			trie[now][x]=++cnt;
		}
		now=trie[now][x];
	}
}
int getval(int v){
	int ans=0,now=0;
	for(int i=(1<<30);i;i>>=1){
		bool x=(v&i);
		if(trie[now][!x]){
			ans+=i;
			now=trie[now][!x];
		}
		else{
			now=trie[now][x];
		}
	}
	return ans;
}
int main(){
	//freopen("data.txt","r",stdin);
	//freopen("my.txt","w",stdout); 
	std::ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v,w;
		cin>>u>>v>>w;
		vec[u].push_back((edge){v,w});
		vec[v].push_back((edge){u,w});
	}
	dfs(1,-1);
	for(int i=1;i<=n;i++){
		insert(s[i]);
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		ans=max(ans,getval(s[i]));
	}
	cout<<ans;
	return 0;
}

字符串匹配算法

给定一个文本串 \(s\) 和模式串 \(t\),问 \(t\)\(s\) 中第一次出现的位置。

单模式串匹配

暴力匹配

在每个位置都进行一次匹配即可。

string a,b;
cin>>a>>b;
int sa=a.size(),sb=b.size();
for(int i=0;i<sa;i++){
	if(a[i]==b[0]){
		int f=1;
		for(int j=0;j<sb;j++){
			if(a[i+j]!=b[j])f=0;
		}
		if(f==1){
			cout<<i;
			return 0;
		}
	}
}

KMP

想想暴力匹配为什么会慢?

因为每次失配之后我们都要从头匹配。

KMP 就很好地优化了这一点,失配时,可以不需要再次重新匹配,而是可以找到当前位置失配时模式串应该从何处开始匹配。举个例子:

s: abcacababcab
t: abcab

\(i\) 为文本串匹配到何处,\(j\) 为模式串匹配到何处。

此时,我们在 \(i=j=5\) 时失配了。暴力的做法是 \(i \to 2,j \to 1\) 然后继续匹配。而其实我们可以 \(j \to 1\) 继续匹配:

s:abcacababcab
t:   abcab

为什么可以这么做?

因为我们扫完了模式串的前四位,发现字符串 a 是其 \(4\) 位前缀 abca 的最长 border。那么就意味着我们在 \(2,3\) 位不可能匹配上,同时在 \(4\) 位一定可以匹配其最长 border 的长度。

那么现在问题落在了怎么求解前缀的最长 border 上。(一般前 \(i\) 位的最长 border 记为 \(next_i\)\(fail_i\))。

怎么计算 abacabab\(next\) 数组?

考虑递推:

首先令 \(next_1=0\)\(i=1,j=2\)\(i\) 是前缀最后一位,\(j\) 是后缀最后一位)。

对于位置 \(2\),由于 \(s_i \neq s_j\),所以 \(next_2=0\),然后 \(j\) 加一。

对于位置 \(3\),由于 \(s_i = s_j\),所以 \(next_3=next_2+1=1\),然后 \(i,j\) 加一。

对于位置 \(4\),由于 \(s_i \neq s_j\),所以 \(next_4=0\),令 \(i=0\),然后 \(j\) 加一。

同理可得 \(next_5=1,next_6=2,next_7=3\)

对于位置 \(8\),此时 \(i=4,j=8\),我们发现 \(s_i \neq s_j\)。难道 \(next_8=0\) 吗?

显然不是,既然现在这个前缀不能与 \(s_j\) 组成一个公共前后缀,那么我们就考虑更短的,查表 \(next_i=1\),即目前匹配上的 aba 的最长 border 为 a,于是这个 a 就有可能与 \(s_j\) 组成一个 border,此时检查得 \(next_8=2\)(要是还没匹配上就继续往后跳)。

因为匹配时候的 \(i\) 是不会减的,所以 KMP 匹配的时间复杂度为 \(O(|s|+|t|)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int nxt[maxn],lena,lenb,j; 
string a,b;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>a>>b;
	lena=a.size(),lenb=b.size();
	a=" "+a,b=" "+b; 
	for(int i=2;i<=lenb;i++){     
		while(j>0&&b[i]!=b[j+1]){
			j=nxt[j];  
		}  
		if(b[j+1]==b[i]){
			j++; 
		}   
		nxt[i]=j;
	}
	j=0;
	for(int i=1;i<=lena;i++){
		while(j>0&&b[j+1]!=a[i]){
			j=nxt[j];
		}
		if(b[j+1]==a[i]){
			j++;
		}
		if(j==lenb){
			cout<<i-lenb+1<<endl;
			j=nxt[j];
		}
	}
	for(int i=1;i<=lenb;i++){
		cout<<nxt[i]<<" ";
	}
	return 0;
}

STL 大法

众所周知,C++ 的 string 类是有 find 函数的。

#include<bits/stdc++.h>
using namespace std;
string a,b;
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0),cout.tie(0);
	cin>>a>>b;
	cout<<int(a.find(b));
	return 0;
}

这里同时贴上 HNSDFZOJ1028 的代码。(非正解)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
string s,ans="Just a legend";
int nxt[maxn];
bool f=1;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>s;
    int n=s.size();
    for(int i=1;i<=n-2;i++){
        if(clock()>=998000){//卡时,运行超过 998ms 输出当前最优解退出
            cout<<ans<<endl;
            return 0;
        }
        string a=s.substr(0,i);
        string b=s.substr(1,n-2);
        string c=s.substr(n-i,i);
        int check=b.find(a);
        if(a==c&&check!=-1){
            ans=a;
        }
    }
    cout<<ans;
    return 0;
}

多模式串匹配

有一个文本串 \(s\)\(n\) 个模式串 \(t_1,t_2,t_3,\dots,t_n\),问这些模式串中有几个在文本串中出现过。

AC 自动机

先咕,晚上再写 qwq