2017清北学堂(提升组精英班)集训笔记——字符串算法

啊~个人速度真的是太慢了,学校又要提早开学!!还有两个月就要比赛了,垂死挣扎一下吧~html

继续更新笔记(眼含泪水)正则表达式

1、Trie树:算法

1.定义:经过字符串建成一棵树,这棵树的节点个数必定是最少的。例如:4个字符串"ab","abc","bd","dda"对应的trie树以下:数组

其中红色节点表示存在一个字符串是以这个点结尾的。oop

一个性质:在树上,两个点u,v知足u是v的祖先,那么u表明的字符串必定是v表明的字符串的前缀。网站

2.Trie树的插入:能够从根节点出发,每次沿着要走的字符串往下走,若没有则创建新节点。spa

假如全部字符串的长度之和为n,构建这棵trie树的时间复杂度为O(n)。指针

 1 int root=1;
 2 int cnt=1;
 3 int p[i][j];//表示从i这个节点沿着j这个字符走,能走到哪一个点,走不到就是0
 4 char s[];//存储字符串 
 5 for(int i=1;i<=n;i++)
 6 {
 7     scanf("%s",s);
 8     int len=strlen(s);
 9     int now=root;//now表示走到的当前节点,now的初始值为根节点
10     for(int j=0;j<len;j++)
11     {
12         if(p[now][s[j]]>0){
13             now=p[now][s[j]];//若是能沿着j节点往下走,直接往下走
14         }
15         else{
16             pow[now][s[j]]=++cnt;
17             now=p[now][s[j]];
18         } 
19     } 
20     v[now]++;//记录now这个节点被访问的次数 
21 } 

3.Trie树的查询:能够看出trie树中每一个节点表示其中一个字符串的前缀,在作题过程当中每每经过这个性质来获得较好的时间复杂度。code

4.一个例题:给定n个互不相同的串,求存在多少对数(i,j)(共n2对)知足第i个串是第j个串的前缀。htm

全部串的长度之和≤500000。

解题根据性质,“在树上,两个点u,v知足u是v的祖先,那么u表明的字符串必定是v表明的字符串的前缀”。

咱们要知足一个串是另外一个串的前缀,也就是说,在trie树上,这个串对应的位置是另外一个串对应的位置的祖先

构建这棵trie树,而后咱们枚举每一个红色点,它对答案的贡献是以它为根的子树中红色节点的个数之和。这个东西能够在一开始遍历这棵树预处理出来!

时间复杂度是线性的。

 1 void dfs(int x)
 2 {
 3     if(v[x]) sum[x]++;
 4     for(char i='a';i<='z';i++)
 5     {
 6         if(p[x][i])//从当前x沿着i这个字符走还能往下走 
 7         {
 8             dfs(p[x][i]);//往下走 
 9             sum[x]+=sum[p[x][i]];//累加贡献sum 
10         }
11     }
12 } 
13 dfs(1);//从根节点开始 

5.USACO的某题(这题真的乱)给定n个串,重排字符之间的大小关系,问哪些串有可能成为字典序最小的串。

全部字符串的长度之和<=100000。

例如,有一个字符串,里面只有'a'~'z'这些字符,默认地,'a'是最小的,'z'是最大的。可是咱们能够从新定义字符间的大小关系,好比这样:b<c<d<y<x<z,从而咱们对于一些字符串,就按照咱们新定义的大小关系来比较字典序大小。

举个栗子

三个字符串"aab","aba","baa",总共只出现了两个字符'a'和'b',因此字符间的大小关系要么是a<b要么是a>b,假设a<b,则第一个串"aab"就是字典序最小的串;假设b<a,则第三个串"baa"就是字典序最小的串,可是对于第二个串,不管咱们怎么定义字符间的大小关系,都不可能成为三个字符串中字典序最小的。

解题:首先对这n个串构建trie树,以后对每一个串,从根走向它,这个路程中遇到的全部兄弟在字典序下都比它大

对于每一个串,从前日后,直接肯定这个字符的大小关系,判断是不是字典序最小

如下两个串都是有可能成为字典序最小的串的:

abcd…xyza

abcd…xyzb

因此有:u是v的前缀,则v必定不是字典序最小的串。

如今问题来了:对于每一个串,在什么条件下,是字典序最小的??

来个栗子:有一些字符串("aabc","aac","aad","ac","adb","aabb"),构造trie树以下:

对于"aabc"这个串,往下遍历:

从深度为2的那一层,咱们能够获得:a<c,a<d;

从深度为3的那一层,咱们能够获得:b<c,b<d;

从深度为4的那一层,咱们还能够获得:c<b。

综上所述:咱们获得了一堆的关系:a<c,a<d;b<c,b<d;c<b,容易看出,这些关系是存在矛盾的,因此无解->"aabc"是不可能成为字典序最小的串。

因此要想有解,这一堆的关系必须知足两个条件:①不存在矛盾②不存在环

总的来讲,就是判断这一堆的关系是否存在拓扑序

最多有26×26个大小关系,而最多有100000个字符串,因此时间复杂度最大为:O(26×26×100000)

2、KMP算法(“看mao片算法”~咳咳):

给定两个字符串A,B,判断T是否为S的子串(变式:寻找子串B在串A中的位置)。

要求一个O(|A|+|B|)的作法。

一般称A为目标串(或主串),B为模式串。

算法过程

咱们假设串A的长度为n,串B的长度为m,每一个字符串的开头下标默认为1。

定义两个变量i和j,这两个变量共同表示:A[i-j+1~i]与B[1~j]均匹配,即:A中以第i个字符结尾的、长度为j的字符串,和B从头开始长度为j的字符串彻底匹配

继续往下匹配:若是i+1和j+1不匹配。

如今,就是用到了KMP算法的核心:它对这一状况的处理方式是减小j,就至关于将子串向右平移

平移的目的是为了让“A[i-j+1~i]与B[1~j]均匹配”这个条件从新知足。

在上图中,j一直减少到了0,由于向右平移的过程当中,始终不能让这个条件知足(最右边"?"部分已经越界)

但有时候,将j减小一点点以后,是能够从新知足条件的,例如:

那么咱们将j从7减少到4时,有:

这样就能够彻底匹配啦!可是后面还有没有匹配的机会咱们就无论了,至少咱们已经保证A[4~7]和B[1~4]彻底匹配上了。

如今考虑一个问题:咱们每次把j减少1(一位一位地平移B字符串),这样太慢了,咱们在这里预处理一个next[]数组,表示当j匹配不下去的时候,咱们能够把j减小到next[j],继续尝试匹配

预处理过程:让j本身和本身匹配一下,一旦匹配发现B[k-m+1~k] 和 B[1~m] 匹配,则说明在A与B匹配过程当中,j等于k匹配不下去时,j能够尝试减少到m。

过程以下:


/**************************************///靓丽的分界线

一些代码:

 1 /*核心内容*/
 2 for(int i=1,j=0;i<=n;i++)
 3 {
 4     while(j&&B[j+1]!= A[i]) j=next[j];
 5     if(B[j+1]==A[i]) j++;
 6     if(j==m)
 7     {
 8         printf("%d\n", i-j+1);//输出找到的"B字串在 A中位置"
 9         //若是要求的是出现次数,这里也有多是ans++什么的
10         j=next[j];//让循环进行下去
11     }
12 }
1 for(int i=2,j=0;i<=m;i++)/*预处理next[]数组*/ 2 {
3     while(j&&B[j+1]!=B[i]) j=next[j];
4     if(B[j+1]==B[i]) j++;
5     next[i]=j;
6 }

经典例题:Blue jeans(POJ 3080)

给定m个串,求字典序最小的公共子串。找一个串,使得这个串是全部串的子串,而且字典序最小。

m≤10,每一个串的长度≤60.

解题思路:一个串,若是是全部串的子串,那么确定是第一个串的子串。

枚举全部子串,复杂度为60*60。
验证其它串是否包含这个子串,复杂度为10*60。
每次更新答案便可。

时间复杂度为:O(603×10)

经典例题:Seek the Name,Seek the Fame(POJ 2752)

给定一个字符串S,求全部既是S的前缀又是S的后缀的子串,从小到大输出这些串的长度。
|S|<=500000。

N为字符串S的长度。

解题思路

回到KMP算法,咱们令P[j]表示找最大的数x,使得B中位置是1~x的字符与j-x+1~j的字符彻底相同,也就是上面讲的KMP算法中的next[]。

考虑P[|S|]的意义,也就是最大的前缀等于后缀的长度(不包括其自己)。

那P[P[|S|]]就是次大的。
所以全部P[P[…P[|S|]]]就是答案了,一直这样递归下去就能够找到答案。

或者用Hash来作,这样更容易想到也比较方便,但效率没有KMP高。

3、AC自动机:

有n个模式串,长度之和是|T|,有一个主串,长度是|S|,问哪些模式串是这个主串的子串(或者有多少个模式串在主串中出现过)?

解法一:直接跑n次KMP算法,时间复杂度:O(n×|S|)。

解法二:AC自动机,时间复杂度:O(|T|+|S|),对于n个串,构建trie树,在trie树上作KMP

在这里我来详解一下AC自动机啊~

首先咱们定义一个指针,叫作“失配指针”或者“失败指针”,在KMP算法中,这个失配指针就是next[]数组,一样地在AC自动机中,在trie树上也定义一个失配指针与此相似但不彻底相同。

失配指针:假设一个节点k的失配指针指向j,那么k、j知足性质:设根节点root到j的距离为n,则从k以上的第n个节点到k这个节点所组成的长度为n的单词,与根节点root到j所组成的单词彻底相同。

以下图:

单词"she"中的'e'的失配指针指向的是单词"her"中的'e',由于红框中的部分是彻底同样的。

而后,问题来了,咱们该怎样处理这个失配指针呢?其实咱们能够用BFS就很方便地解决了。

处理过程:让和根节点直接相连的节点的失配指针指向根节点,对于其余节点(假设为a),设这个节点上的字母为ch,沿着a的父亲b的失配指针走,一直走到一个节点c,c的儿子中也有字母为ch的节点d,而后把a节点的失配指针指向c节点的儿子d(由于d的字母也为ch),若是一直走到了根节点都没找到,那就把失配指针指向根节点。

最开始,咱们把根节点加入队列(根节点的失败指针显然指向本身),这之后咱们每处理一个点,就把它的全部儿子加入队列,直到搞完。

这样咱们就获得了一棵带有失配指针的trie树了,接下来正式介绍AC自动机工做原理!

AC自动机原理:对于一棵trie树,咱们用黄色表示一个单词(某个模式串)的末尾,也就是说从根节点走到一个黄色的点,就组成一个“单词”,以下图:

一开始,trie树中有一个指针t1指向根节点root,将这n个模式串合并为一个模式主串,模式主串中有一个指针t2指向这个模式主串的串头。

接下来进行相似KMP算法的操做:若是t2指向的字母,是trie树中,t1指向的节点的儿子,那么把t2+1,t1改成那个儿子的编号,不然t1顺这当前节点的失配指针往上找,直到t2是t1的一个儿子,或者t1指向根为止。

若是t1通过了一个黄色的点,那么以这个点结尾的单词就算出现过了(这个模式串已经在主串中出现了),或者若是t1所在的点能够沿着失配指针走到一个黄色的点,那么以那个黄色的点为结尾的单词就算出现过了(这个模式串已经在主串中出现了),记录答案便可。

代码。。。我比较懒并且弱,之后有机会再发吧!

经典例题:伪装是字符串的题——正则表达式(ZHW YY出来的题)

给定一个字符串,判断其是否为合法的正则表达式。

一个正则表达式定义为:

①0是正则表达式,1也是正则表达式。

②P和Q都是正则表达式,则PQ也是正则表达式。

③P是正则表达式,则(P)是正则表达式

④P是正则表达式,则P*也是正则表达式

⑤P和Q都是正则表达式,则P|Q是正则表达式

举个栗子:

010101101*
(11|0*)*

以上都是都是正则表达式
|S|<=100

解题思路:令dp[i][j]表示第i个字符到第j个字符可否组成正则表达式,分5种状况进行转移就能够了。

四:对拍方法:

对拍:你给两个程序,和一个随机数据生成器,而后系统去用这个随机数据生成器的输出做为你这两个程序的输入,而后比较你这两个程序的输出,能够找到一组使这两个程序输出不同的数据(若是存在的话),这样就能够提升正确率。

通常的对拍是:对于一个题,咱们写一个暴力算法,管它什么时间空间效率呢?!而后写一个你对于这题的正确解法程序,拿这两个程序进行对拍。

对拍的实现过程

首先,在本地新建一个文件夹。

而后,在里面放入std.exe、mkdt.exe、a.exe这三个exe程序(名字能够本身乱取)。

std.exe:你暴力写的一个作法或者你从网上找的一份AC代码生成的程序,反正结果确定是对的。

a.exe:你的代码生成的程序,你不知道他对不对或者你知道他是WA的可是你不知道哪里WA了。

mkdt.exe:就是你的随机数据生成器,你能够用它去生成你认为的合法数据。

最后,在文件夹中新建一个txt记事本文档,在里面输入如下代码:

1 :loop
2     mkdt.exe
3     std.exe
4     a.exe
5     fc std.out a.out
6     if %errorlevel%==0 goto loop
7 pause

而后保存,把".txt"这个后缀名改成".bat",双击运行便可!

5、随机算法:

①随机生成一棵树:

1 for(int i=2;i<=n;i++)/*随机生成一棵树*/
2 {
3     cout<<rand()%(i-1)+1<<' '<<i<<endl;
4 }//深度为lgn 

②随机生成一棵长毛的链:

1 /*随机生成一棵长毛的链:1~n/2*/
2 for(int i=2;i<=n/2;i++) cout<<i-1<<' 'i<<endl;
3 for(int i=n/2+1;i<=n;i++) cout<<rand()%(i-1)+1<<' '<<endl; 

③给你一张图,生成一张图:

 1 /*给你一张图,生成一张图,10万个点,20万条边 */
 2 map<long long,int> mp;
 3 for(int i=1;i<=200000;i++)
 4 {
 5     int A=rand()%n+1;
 6     int B=rand()%n+1;
 7     while(A==B||mp[1ll*A*100005+B])
 8     {
 9         A=rand()%n+1;
10         B=rand()%n+1;
11     }
12     mp[1ll*A*10005+B]=1;
13     cout<<A<<' '<<B<<endl;
14 } 

④随机生成一个连通图:

先生成一棵树,这棵树上的边是必定存在的,在随机其余的边。

最近发现一些网站盗用个人blog,这实在不能忍(™把关于个人名字什么的所有删去只保留文本啥意思。。)!!但愿各位转载引用时请注明出处,谢谢配合噢~

原博客惟一地址:http://www.cnblogs.com/geek-007/p/7296453.html

相关文章
相关标签/搜索