字符串(kuangbin)题解

只做图论是会审美疲劳的,所以我来写写字符串的题换换心情>_

文章目录

  • KMP
      • 板子一
      • 板子二
      • 板子三
      • 最小循环节
      • 最长公共前缀后缀
      • 所有公共前缀后缀
      • 所有前缀在文本中出现的次数和
      • 所有文本的最长公共子串
      • 所有文本的最长公共子串
      • kmp+最小表示法
      • 在字符串可以旋转的情况下,有多少个不同的字符串
  • 马拉车
      • 简单说两句
      • 板子
  • AC自动机
      • 简单说两句
      • 板子
      • 板子2
      • AC自动机+有向图长度为n的路径的个数(矩阵快速幂)
      • AC自动机+有向图长度为n以内的路径的个数(矩阵快速幂

随时更新

KMP

KMP是个好东西,适用于单文本的字符串匹配,时间复杂度O(n+m)是AC自动机的灵魂,最关键的是,它简单啊!,代码又好看,还短,是普及选手的必备算法

板子一

没啥好说的,就是板子…
放个代码就过…

#include 
#include 
#include 
#define mem(a,b) memset(a,b,sizeof(a))
#define MAX 1000010
using namespace std;
int nxt[MAX];
int a[MAX];
int b[MAX];
int len_a;
int len_b;
void getnxt()
{
	mem(nxt,0);
	nxt[0]=-1;
	int i=0;
	int j=-1;
	while(i<len_b)
	{
		while(j!=-1&&b[i]!=b[j])
		{
			j=nxt[j];
		}
		j++;
		nxt[++i]=j;
	}
//	for(int i=0;i
//	cout<
//	cout<
}

int getans()
{
	int ans=-1;
	int i=0;
	int j=0;
	while(i<len_a&&j<len_b)
	{
		while(j!=-1&&a[i]!=b[j]) j=nxt[j];
		i++;
		j++;
	}
	if(j==len_b)
	return i-len_b+1;
	return -1;
}


int main()
{
	int t;
	scanf("%d",&t);

	while(t--)
	{
		scanf("%d%d",&len_a,&len_b);
		for(int i=0;i<len_a;i++)
			scanf("%d",&a[i]);
		for(int i=0;i<len_b;i++)
			scanf("%d",&b[i]);
		getnxt();
		printf("%d\n",getans());
	}
	return 0;
} 

板子二

和上一题有什么区别吗?
过…

板子三

和上一题唯一的区别就是不能重叠的计算字符了getans里稍微改一下就好了…

最小循环节

m的最小可能循环节的长度为: ∣ m ∣ − n x t [ ∣ m ∣ ] |m|-nxt[|m|] mnxt[m]仔细想一下其实不难理解(从那一位开始,前缀才开始和后缀相同嘛…),然后这个题就迎刃而解了,注意它不是nxt数组中最后一个0出现的位置反例:aaabaaaaaaa
ACCode:

#include 
#include 
#include 
#define mem(a,b) memset(a,b,sizeof(a))
#define MAX 1000010
using namespace std;
int nxt[MAX];
char a[MAX];
char b[MAX];
int len_a;
int len_b;
void getnxt()
{
	len_b=strlen(b);
	mem(nxt,0);
	nxt[0]=-1;
	int i=0;
	int j=-1;
	while(i<len_b)
	{
		while(j!=-1&&b[i]!=b[j])
		{
			j=nxt[j];
		}
		j++;
		nxt[++i]=j;
	}
//	for(int i=0;i<=len_b;i++)
//	cout<
//	cout<
}

int getans()
{
	len_a=strlen(a);
	int ans=0;
	int i=0;
	int j=0;
	while(i<len_a)
	{
		while(j!=-1&&a[i]!=b[j]) j=nxt[j];
		i++;
		j++;
		if(j==len_b)
		{
			j=0;
			ans++;
		}
	}
	return ans;
}


int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		scanf("%s",b);
		getnxt();
		int len=len_b-nxt[len_b];
		if(nxt[len_b]==0)//后缀完全不和前缀相同 
		{
			printf("%d\n",len);
			continue;
		}
		printf("%d\n",len_b%len==0? 0:len-len_b%len);
	}
	return 0;
}

最长公共前缀后缀

下面的评论说可以用ex_KMP,然后百度了一下拓展KMP,没怎么看明白,先不管了,这个题也可以用KMP写,把两个串拼起来(中间最好加一个分隔符)跑KMP,输出nxt[len]就好…

所有公共前缀后缀

是上一道题的进阶版,求出最长的前缀后缀之后,在最长的前缀后缀上再求最长前缀后缀就好,这个题比较卡时间,要选好STL,多一个log就T,QWQ.

所有前缀在文本中出现的次数和

读错题了…读成所有子串了…然后就在想着不是直接输出不就好了?还要KMP?(做题千万条,读题第一条),嘛,反正也是一个水题,水水过去了…

所有文本的最长公共子串

一直在想似乎是后缀数组的板子…结果暴力可解…没什么好说的,莽就完了,不放心可以加二分优化(但长度就100,加不加差别不是很大吧…应该?)水题,不贴代码了

所有文本的最长公共子串

同上,依然不需要二分优化…

kmp+最小表示法

出现了几次就是一共有几个循环节,最小的循环节长度再上文已经总结过了,所以出现次数可以用kmp算法O(n)求出,剩下的就是最小表示法,其实挺简单的,就是在设计算法的时候优化力度不够…
优化1:我们只需要在循环节里求最小字典序的串就好(没什么卵用的优化)
优化2:不好解释,上图看一下就好
优化后:

int getf()
{
	int i=0;
	int j=1;
	while(i<len&&j<len)
	{
		for(int t=0;t<len_s;t++)
		{
			if(s[i+t]<s[j+t])
			{
				j=j+t+1;
				break;
			}
			else if(s[i+t]>s[j+t])
			{
				i=i+t+1;
				break;
			}
		}
		if(i==j)
		j++;
	}
	return min(i,j);
}

优化前:

int getf()
{
	int i=0;
	int j=1;
	while(j<len)
	{
		for(int t=0;t<len_s;t++)
		{
			if(s[i+t]<s[j+t])
			{
				j=j+t+1;
				break;
			}
			else if(s[i+t]>s[j+t])
			{
				i=j;
				j=j+1;
				break;
			}
		}
	}
	return i;
}

显然面对"ccccccccccccccccccccccccccccccccccccccccccb"这样的数据就炸了i和j交错进行还是会好很多的.

优化3:string改为char,常规优化,不解释.
注:最小表示法的代码中一旦发现了i开头的串和j开头的串重合了就要退出,因为没必要再往下跑了,已经开始循环了.

在字符串可以旋转的情况下,有多少个不同的字符串

很经典的问题,字符串变成数字啊,问题背景变成项链啊什么的,几个人围成环啊什么的,就是看看两个字符串的最小表示法下的字符串是否相同就好了.代码就不贴了,和上一题神似…

马拉车

简单说两句

之前觉得会了回文树,马拉车就可以抛弃了,现在想想还真的不行…有的也许只能用马拉车(在相当卡时间的时候)这个时候我们就要使劲优化算法,所以用string输入就是在作死.
马拉车的思路其实就是以每一个字符作为中心点往外扩展,直到发现两个端点不同位置,但正常1的暴力是O(n^2)的,太慢,所以,我们就再利用一下回文串的对称性,下面举个栗子:
字符串(kuangbin)题解_第1张图片

emmm,凑合着看吧…
从图上我们能得到的结论是当前我们要找的节点的最长回文串一定是从3到7的:
证明如下:
1号与5号一定相同,(蓝)
1号与7号一定相同,(黑)
5号与3号一定相同,(黑)
==> 7号与3号一定相同
同理4号与六号一定相同,

若2号与8号相同
2号与6号相同,(黑)
8号与0号相同,(黑)
则6号与0号相同,
==>与对称点的最长回文串矛盾

通过这种方法,我们就可以快速的求出当前节点的最长回文串,但是有三种情况我们直接得到的结果是不正确的,

  1. 对称点的左边界抵达了最左端(但是当前节点可能继续向右拓展)
  2. 对称点的回文串半径加当前节点超出了中心点回文串的最右端(通过上面的证明我们知道,我们只能保证在回文串内结论的正确性,所以这里我们要把当前节点的半径压缩到回文串里面,然后再向外拓展)
  3. 当前节点就是中心点回文串的最右端(直接拓展就ok了)

对于中心点的更新:只要当前节点所代表的回文串超出了原先回文串的右段,我们就更新中心节点.

板子

#include 
#include 
#include 
#include 
#include 
#define MAX 30000000
using namespace std;
int p[MAX];
char a[MAX];
string getit()
{
	int len=strlen(a);
	if(len==0)
	return "@$";
	string ans="@";
	for(int i=0;i<len;i++)
	{
		ans+="#";
		ans+=a[i];
	}
	ans+="#$";
	return ans;
}

void getp(string a)
{
	int c=1;
	int r=1;
	for(int i=1;i<a.size();i++)
	{
		int _i=2*c-i;
		if(r>i)
			p[i]=min(p[_i],r-i);
		else
			p[i]=0;
		while(a[i+p[i]+1]==a[i-p[i]-1])p[i]++;
		if(i+p[i]>r)
		{
			c=i;
			r=i+p[i];
		}
	}
}


int main()
{
	scanf("%s",a);
	string b=getit();
	getp(b);
	int ans=0;
	for(int i=0;i<b.size();i++)
	ans=max(ans,p[i]);
	cout<<ans;
	return 0;
}

双倍经验

AC自动机

简单说两句

时间复杂度:
假设有N个模式串,平均长度为L;文章长度为M。 建立Trie树:O(NL) 建立fail指针:O(NL) 模式匹配:O(M*L) 所以,总时间复杂度为:O( (N+M)*L )

又称树上KMP,理解getfail()AC自动机就没有问题了.fail[i]表示的是i节点所表示字符串在树上的最长后缀的节点.

板子

中文题干…题意就不说了.
这个题其实不适合做板子…因为坑有点多…但是确实是一个裸体.
坑点:

  1. 字符集是全体ASCII码!!!
  2. 被PE支配? 在最后加一个回车试试?见过检测行末空格的没见过检测行末回车的…(现在见了)
  3. 被MLE支配?试试吧初始化去掉,因为memset过的数组就是使用过的空间,会被计入总空间,但是你只开空间而不使用的话,是不会算入内存的(原理不明…)删掉初始化之后由50M瞬间减到30M…

剩下的就是纯板子了…

#include 
#include 
#include 
#include 
#include 
#include 
#define mem(a,b) memset(a,b,sizeof(a))
#define MAX 100005
using namespace std;
char ss[10005];
queue<int> que;
struct node{
	bool vis[MAX];
	int tree[MAX][128];
	int fail[MAX];
	int book[MAX];
	int num;
	int l=1;
	void insert(char *a)
	{
		int len=strlen(a);
		int now=0;
		for(int i=0;i<len;i++)
		{
			int t=a[i];
			if(tree[now][t]==0)
				tree[now][t]=++num;
			now=tree[now][t];
		}
		book[now]=l++;
	}
	
	void getfail()
	{	
		for(int i=0;i<128;i++)
			if(tree[0][i]) que.push(tree[0][i]);
		while(que.size())
		{
			int t=que.front();
			que.pop();
			for(int i=0;i<128;i++)
			{
				if(tree[t][i])
				{
					que.push(tree[t][i]);
					fail[tree[t][i]]=tree[fail[t]][i];
				}
				else
				tree[t][i]=tree[fail[t]][i];
			}
		}
	}
	
	int getans(char *a)
	{
		mem(vis,0);
		int f=0;
		int len=strlen(a);
		int now=0;
		for(int i=0;i<len;i++)
		{
			int t=a[i];
			now=tree[now][t];
			for(int j=now;j!=0&&vis[book[j]]!=1;j=fail[j])
			{
				if(book[j]) 
				{
					vis[book[j]]=1;
					f=1;
				}
			}
		}
		return f;
	}
}AC;


int main()
{
	int n;
	scanf("%d",&n);
	getchar();
	for(int i=1;i<=n;i++)
	{
		gets(ss);
		AC.insert(ss);
	}
	AC.getfail();
	scanf("%d",&n);
	getchar();
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		gets(ss);
		if(AC.getans(ss))
		{
			ans++;
			printf("web %d:",i);
			for(int i=1;i<=500;i++)
			if(AC.vis[i])
			printf(" %d",i);
			printf("\n");
		}
	}
	printf("total: %d\n",ans);
	return 0;
}
/*
5
a a a
b b b
a b
a b c
c b a
10
a a a b b b
*/

板子2

上一题的简单变形,注意一点细节就可以秒杀,但是这个细节题干却没有明确指出…–>多组样例!!!
由于太过板子就不贴代码了…

AC自动机+有向图长度为n的路径的个数(矩阵快速幂)

这是一道神题,第一次让我意识到AC自动机原来是一张图(太憨了…),然后就是离散数学讲过的,长度为k的路径可以通过矩阵的幂的形式求出来,因为幂比较大,就用矩阵快速幂就好了…
这个题要注意的一个事:
字符串(kuangbin)题解_第2张图片
能少取%就少取…取%实在是太慢了…(自带大常数)

ACCode:

#include 
#include 
#include 
#include 
#include 
#include 
#define mem(a,b) memset(a,b,sizeof(a))
#define ll long long
#define MAX 110
#define mod 100000
using namespace std;
char s[MAX];
int idx[MAX];
struct node{
	int book[MAX];
	int fail[MAX];
	int tree[MAX][5];
	int num;
	
	void insert(char *a)
	{
		int now=0;
		int len=strlen(a);
		for(int i=0;i<len;i++)
		{
			int t=idx[a[i]];
			if(tree[now][t]==0) tree[now][t]=++num;
			now=tree[now][t];
		}
		book[now]=1;
	}
	
	void getfail()
	{
		int now=0;
		queue<int> que;
		for(int i=0;i<4;i++)
			if(tree[0][i]) que.push(tree[0][i]);
		while(que.size())
		{
			int now=que.front();
			que.pop();
			for(int i=0;i<4;i++)
			{
				if(tree[now][i]) 
				{
					fail[tree[now][i]]=tree[fail[now]][i];
					que.push(tree[now][i]);
				}
				else
					tree[now][i]=tree[fail[now]][i];
				book[tree[now][i]] |= book[tree[fail[now]][i]];
			} 
		}
//		for(int i=1;i<=num;i++)
//		cout<
	}
	
	void init()
	{
		mem(book,0);
		mem(fail,0);
		mem(tree,0);
		num=0;
	}
	
}AC;

struct jz{
	ll data[MAX][MAX];
	jz()
	{
		mem(data,0);
	}
};

jz operator *(jz a,jz b)
{
	jz c;
	for(int i=0;i<=AC.num;i++)
	{
		for(int j=0;j<=AC.num;j++)
		{
			ll ans=0;
			for(int k=0;k<=AC.num;k++)
			ans=(ans+(a.data[i][k]*b.data[k][j]))%mod;
			c.data[i][j]=ans;
		}
	}
	return c;
}

int main()
{
	idx['A']=0;
	idx['C']=1;
	idx['T']=2;
	idx['G']=3;
	int m,n;
	while(scanf("%d%d",&m,&n)!=EOF)
	{
		AC.init();
		for(int i=0;i<m;i++)
		{
			scanf("%s",s);
			AC.insert(s);
		}
		AC.getfail();
		
		jz M,X;
		for(int i=0;i<=AC.num;i++) X.data[i][i]=1;
		for(int i=0;i<=AC.num;i++)
		{
			if(AC.book[i]) continue;
			for(int j=0;j<4;j++)
			{
				int to=AC.tree[i][j];
				if(AC.book[to]) continue;
				++M.data[i][to];
			}
		}
//		for(int i=0;i<=AC.num;i++)
//		{
//			for(int j=0;j<=AC.num;j++)
//			cout<
//			cout<
//		}
		while(n)
		{
			if(n&1) X=X*M;
			M=M*M;
			n>>=1;
		}
		ll ans=0;
		for(int i=0;i<=AC.num;i++)
		ans=(ans+X.data[0][i])%mod;
		printf("%lld\n",ans);
	}
	return 0;
}

AC自动机+有向图长度为n以内的路径的个数(矩阵快速幂

看标题都知道是上一个题的进阶版…那么我们该如何处理呢?就是在最后一列多出一行1就好了…(仅仅是用于求前缀和而已…)
代码就不贴了,和上一题神似.

随时更新…

你可能感兴趣的:(字符串(kuangbin)题解)