在这一章中,老师教了我们四种数据结构:BF算法,kmp算法,三元组和十字链表;还给我们讲了2019年团体天体赛中T1-8的AI题
1、对于BF和kmp算法,老师除了在课堂上讲解算法的主要核心思想外,还给了我们一道作业题去巩固;
这道题如下:
给定一个主串S(长度<=10^6)和一个模式T(长度<=10^5),要求在主串S中找出与模式T相匹配的子串,返回相匹配的子串中的第一个字符在主串S中出现的位置。
输入格式:
输入有两行: 第一行是主串S; 第二行是模式T.
输出格式:
输出相匹配的子串中的第一个字符在主串S中出现的位置。若匹配失败,输出0.
输入样例:
在这里给出一组输入。例如:
aaaaaba
ba
输出样例:
在这里给出相应的输出。例如:
6
首先,可以用BF算法去实现,但是BF算法是不能通过所有测试点的;BF算法实际上是一种暴力算法,而题目给的测试样例会有一个卡住而造成超时;
比如主串为aaaaaaaaaaaaaaaab 模式串为aaaaab ,这样的话每次都得去比较到最后一个才发现不匹配,而题目给的最大数据是1000000,所以一定是超时的;
BF算法代码如下:
我这里写的跟课本不太一样,我的i,j下标是从0开始,而课本的i,j表示的是位置,从1开始,所以当不匹配是课本回溯到的是i-j+2;而我这里是i-j+1;
1 #include<iostream> 2 #include<string.h> 3 using namespace std; 4 5 string s , t; 6 int ssize ,tsize; 7 int i = 0 ; 8 int j = 0 ; 9 int main() 10 { 11 cin>>s; //输入主串 12 cin>>t; //输入模式串 13 ssize = s.size(); //主串的长度; 14 tsize = t.size(); //模式串的长度; 15 while(i<ssize&&j<tsize) 16 { 17 if(s[i]==t[j]) //如果匹配,则i++,j++;主串和模式串都向前移一位; 18 { 19 i++; 20 j++; 21 }else 22 { 23 i = i-j+1; 否则主串回溯到i-j+1; 24 j = 0; 模式串回溯到0; 25 } 26 } 27 if(j>=tsize) 28 { 29 cout<<i-tsize+1; 30 }else 31 cout<<0; 32 return 0; 33 }
这道题还能用kmp算法去写,kmp的核心是next数值;
解题思路:串的模式匹配有两种:一种是BF算法,一种是KMP算法;
基于这道题给的数据,若用BF算法便会超时,所以我们这道题用KMP算法;
那么问题来了,KMP算法到底怎么用的;简单来讲,就是有两个步骤:
1、求模式串的next数组;
2、进行主串与模式串的匹配;
假设主串和模式串分别为
第一个问题:如何求next数组
🔺next数组求的是模式串的!!!
下面就以上面给的模式串为例;
next数组便是前缀中的最长相同前后缀,说起来比较绕,什么意思呢,模拟一遍就清楚了;
所以对于模式串对应的next数组为
这样我们就求出了next数组;
接下来进行模式匹配,其实这样就会有个问题,所以实际上next数组这样是需要改进的;
假设我们不改进的话,进行匹配会出现什么问题呢;
进行模式匹配的大概代码如下:
1、即匹配,则i++;j++;
2、不匹配,根据刚刚求出的next数组,进行跳next数组;
下面代码中ssize为主串s的长度,tsize为模式串t的长度;
下面我们就根据代码模拟一遍;
上面我们求出来的next数组为:
现在我们把它们的下面也写上:
现在开始模拟一遍:
我们发现匹配到c的时候不匹配了,跳next数组,则跳到下标为0处,变成:
此时也不匹配,变成应该跳next数组,跳到下标为0处,但是这样就变成死循环了,所以我们应该退一步,将next数组的第0个赋值为-1,且将整个next数组向后移;就不会变成死循环了;
再模拟一次:
此时不匹配跳next数组;
变成:
发现a不匹配,跳next数组:
继续模拟:
发现不匹配,所以此时next应该跳到下标为-1处,但是这里没有下标为-1的,所以实际上就是整体向后移;变成:
发现完全匹配了;
那么基于上面的改进,next数组应该怎么写呢:
1 string s; 2 string t; 3 int ssize; 4 int tsize; 5 int next1[2000000]; 6 void nextsz(string t,int tsize) 7 { 8 next1[0] = -1; //防止进入死循环,而且到不能匹配时能整体后移 9 int k = -1; //是为了调节next数组; 10 int j = 0 ; 11 while(j < tsize-1) 12 { 13 if(k==-1||t[j]==t[k]) //k=-1进入这个循环是为了整体向后移; 14 { 15 ++k; k实际上也是记录了相同的个数; 16 ++j; 17 18 next1[j] = k; 找到next数组; 19 20 } 21 else 22 k = next1[k]; //不相同则更新k; 23 } 24 25 }
现在会了next数组,我们则可以进行模式匹配了;
利用上面求的next数组来进行模式匹配;过程原理和上面画的图是一模一样的;
代码如下:
1 int kmp(string s,string t,int sszie,int tsize) 2 { 3 int j = 0; 4 int i = 0; 5 while(i<ssize&&j<tsize) 6 { 7 8 if(j==-1||s[i]==t[j]) j=-1是为了调节到跳无可跳时,整体向后移; 9 { 10 i++; //匹配整体向前移; 11 j++; 12 13 } 14 else 15 { 16 j = next1[j]; 不断跳next数组; 17 } 18 19 20 } 21 22 23 if(j==tsize) 24 { 25 return i-j+1; //返回模式串在主串的第一个下标; 26 } 27 else return -1; //不匹配,则返回-1; 28 }
所以这道题的完整代码如下:
1 #include<iostream> 2 #include<string.h> 3 using namespace std ; 4 5 string s; 6 string t; 7 int ssize; 8 int tsize; 9 int next1[2000000]; 10 void nextsz(string t,int tsize) 11 { 12 next1[0] = -1; 13 int k = -1; 14 int j = 0 ; 15 while(j < tsize-1) 16 { 17 if(k==-1||t[j]==t[k]) 18 { 19 ++k; 20 ++j; 21 22 next1[j] = k; 23 24 } 25 else 26 k = next1[k]; 27 } 28 29 } 30 31 int kmp(string s,string t,int sszie,int tsize) 32 { 33 int j = 0; 34 int i = 0; 35 while(i<ssize&&j<tsize) 36 { 37 38 if(j==-1||s[i]==t[j]) 39 { 40 i++; 41 j++; 42 43 } 44 else 45 { 46 j = next1[j]; 47 } 48 49 50 } 51 52 53 if(j==tsize) 54 { 55 return i-j+1; 56 } 57 else return 0; 58 } 59 60 61 int main() 62 { 63 cin>>s; 64 cin>>t; 65 66 ssize = s.size(); 67 tsize = t.size(); 68 nextsz(t,tsize); 69 cout<<kmp(s,t,ssize,tsize)<<endl; 70 71 72 73 }
2、对于三元组和十字链表;
老师也是同样讲了核心思想加上给我们布置了稀疏矩阵的实践题;
如果一个矩阵中,0元素占据了矩阵的大部分,那么这个矩阵称为“稀疏矩阵”。对于稀疏矩阵,传统的二维数组存储方式,会使用大量的内存来存储0,从而浪费大量内存。为此,可以用三元组的方式来存放一个稀疏矩阵。
对于一个给定的稀疏矩阵,设第r行、第c列值为v,且v不等于0,则这个值可以表示为 <r,v,c>。这个表示方法就称为三元组。那么,对于一个包含N个非零元素的稀疏矩阵,就可以用一个由N个三元组组成的表来存储了。
如:{<1, 1, 9>, <2, 3, 5>, <10, 20, 3>}就表示这样一个矩阵A:A[1,1]=9,A[2,3]=5,A[10,20]=3。其余元素为0。
要求查找某个非零数据是否在稀疏矩阵中,如果存在则输出其所在的行列号,不存在则输出ERROR。
输入格式:
共有N+2行输入: 第一行是三个整数m, n, N(N<=500),分别表示稀疏矩阵的行数、列数和矩阵中非零元素的个数,数据之间用空格间隔; 随后N行,输入稀疏矩阵的非零元素所在的行、列号和非零元素的值; 最后一行输入要查询的非0数据k。
输出格式:
如果存在则输出其行列号,不存在则输出ERROR。
输入样例:
在这里给出一组输入。例如:
10 29 3
2 18 -10
7 1 98
8 10 2
2
输出样例:
在这里给出相应的输出。例如:
8 10
(1)利用三元组,实际上利用三元组是挺简单的,看如下代码:
1 #include<iostream> 2 using namespace std; 3 4 int yr , yc ,num; //定义原来矩阵的行数yr,原来矩阵的列数yc,非零元素num; 5 struct syz{ 6 int i ; 7 int j ; 8 int value; 9 }sanyz[505]; //定义一个三元组数组; 10 int index; //定义一个要查找的数字; 11 int flag = 0; //用来标记是否有要查找的数字; 12 int main() 13 { 14 cin>>yr>>yc>>num; 15 for(int k = 0 ; k < num ;k++) 16 { 17 cin>>sanyz[k].i>>sanyz[k].j>>sanyz[k].value; //输入三元组的行、列和数组; 18 } 19 cin>>index; //输入要检索的数字; 20 for(int k = 0 ; k < num ;k++) 21 { 22 if(index==sanyz[k].value) 23 { 24 flag = 1; //如果可以找到我们要的数字,将 flag置为1; 25 } 26 } 27 if(flag==0) //如果找不到; 28 { 29 cout<<"ERROR\n"; 30 }else 31 { 32 for(int k = 0 ; k < num ;k++) 33 { 34 if(index==sanyz[k].value) 35 { 36 cout<<sanyz[k].i<<" "<<sanyz[k].j<<endl; 37 } 38 } 39 } 40 }
(2)利用十字链表,这是一个很难的地方,实际上我也是理解了好久才似懂非懂;
代码如下:
1 #include<iostream> 2 #include<stdio.h> 3 using namespace std; 4 5 struct OLNod{ 6 int i ; //该非零元的行下标; 7 int j ; //该非零元 的列下标; 8 int value ; //该非零元的数值; 9 struct OLNod *right ,*down ;//该非零元所在的行表和列表的后继链域; 10 }; 11 struct CrossL{ 12 OLNod **rhead, **sead; //十字链表的行头指针和列头指针; 13 int row; //稀疏矩阵的行数; 14 int col; //稀疏矩阵的列数; 15 int num; //稀疏矩阵的非零个数; 16 }; 17 18 int InitSMatrix(CrossL *M) //初始化M(CrossList类型的变量必须初始化; 19 { 20 (*M).rhead = (*M).sead = NULL; 21 (*M).row = (*M).col = (*M).num = 0; 22 return 1; 23 } 24 25 int DestroysMatrix(CrossL *M) //销毁稀疏矩阵M; 26 { 27 int i ; 28 OLNod *p,*q; 29 for( i = 1 ; i <= (*M).row;i++) 30 { 31 p = *((*M).rhead+i); //p指针不断向右移; 32 while(p!=NULL) 33 { 34 q = p ; 35 p = p ->right; 36 delete q; //删除q; 37 } 38 } 39 delete((*M).rhead); //释放行指针空间; 40 delete((*M).sead); //释放列指针空间; 41 (*M).rhead = (*M).sead = NULL; //并将行、列头指针置为空; 42 (*M).num = (*M).row = (*M).col = 0; //将非零元素,行数和列数置为0; 43 return 1; 44 } 45 int CreatSMatrix(CrossL *M) 46 { 47 int i , j , m , n , t; 48 int value; 49 OLNod *p,*q; 50 if((*M).rhead!=NULL) 51 DestroysMatrix(M); 52 cin>>m>>n>>t; //输入稀疏矩阵的行数、列数和非零元个数; 53 (*M).row = m; 54 (*M).col = n ; 55 (*M).num = t; 56 //初始化行链表头; 57 (*M).rhead = new OLNod*[m+1];//为行头指针申请一个空间; 58 if(!(*M).rhead) //如果申请不成功,则退出程序; 59 exit(0); 60 //初始化列链表头; 61 (*M).sead = new OLNod*[n+1];//为列表头申请一个空间; 62 if(!(*M).sead) //如果申请不成功,则退出程序; 63 exit(0); 64 for(int k = 1 ; k <= m ; k++) 65 { 66 (*M).rhead[k] = NULL;//初始化行头指针向量;各行链表为空链表; 67 } 68 for(int k = 1 ; k <= n ;k++) 69 { 70 (*M).sead[k] = NULL;//初始化列头指针向量;各列链表为空链表; 71 } 72 for(int k = 0 ; k < t ;k++) //输入非零元素的信息; 73 { 74 cin>>i>>j>>value;//输入非零元的行、列、数值; 75 p = new OLNod();//为p指针申请一个空间; 76 if(!p) //e如果申请不成功; 77 exit(0); //退出程序; 78 p->i = i; 79 p->j = j; 80 p->value = value; 81 if((*M).rhead[i]==NULL) //如果行头指针指向的为空; 82 { 83 //p插在该行的第一个结点处; 84 p->right = (*M).rhead[i]; 85 (*M).rhead[i] = p; 86 }else //如果不指向空 87 { 88 for(q = (*M).rhead[i];q->right; q = q->right); 89 p->right = q->right; 90 q->right = p; 91 92 } 93 if((*M).sead[j]==NULL)//如果列头指针指向的为空; 94 { 95 //p插在该行的第一个结点处; 96 p->down = (*M).sead[j]; 97 (*M).sead[j] = p; 98 }else//如果不指向空 99 { 100 for(q = (*M).sead[j];q->down;q = q->down); 101 p->down = q->down; 102 q->down = p; 103 } 104 } 105 return 1; 106 } 107 int PrintSMatrix(CrossL *M) 108 { 109 int flag = 0; 110 int val ;//要查找的元素的值; 111 cin>>val; //输入要查找的s值; 112 OLNod *p; 113 for(int i = 1 ; i <= (*M).row ;i++) 114 { 115 for(p = (*M).rhead[i];p;p = p->right) //从行头指针开始找,不断向右找 116 { 117 if(p->value==val) //如果能找到 118 { 119 cout<<p->i<<" "<<p->j; //输出行下标和列下标 120 flag = 1; //标记找到该元素; 121 } 122 } 123 } 124 125 126 if(flag==0) //如果找不懂 127 { 128 cout<<"ERROR\n"; 129 } 130 131 } 132 int main() 133 { 134 CrossL A; //定义一个十字链表; 135 InitSMatrix(&A); //初始化; 136 CreatSMatrix(&A); //创建; 137 PrintSMatrix(&A); //输出; 138 DestroysMatrix(&A); //销毁; 139 return 0; 140 }
最后老师便是在周四的实验课中交了我们AI的代码;跟着老师的步伐觉得其实这道题也不是很难,主要是心要细而且静的下心来去写,它有太多要考虑的细节;这里我看了老师的博客,再去完善这道题;
代码如下:这其中我也知道了 tolower这个函数,是直接将大写字母转化成小写;
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 #include<string.h> 5 using namespace std; 6 7 //1、删除空格(删除前后空格,删除中间连续的空格保留一个,删除符号后的空格 8 //后面的条件依靠空格标准化 9 bool isIndepent(char ch) 10 { 11 ch = tolower(ch); 12 if(ch>='0'&&ch<='9'||ch>='a'&&ch<='z'||ch=='I') 13 { 14 return false; 15 }else 16 return true; 17 } 18 bool isPunctuation(char ch) 19 { 20 if(ch>='0'&&ch<='9'||ch>='a'&&ch<='z'||ch=='I'||ch==' ') 21 return false; 22 else 23 return true; 24 } 25 26 void go(string s) 27 {// 根据s输出AI的回答 28 //定义辅助变量t,来copy s的字符串 29 char t[3001]; 30 int i , j; //定义两个下表; i :定义到s的第一个非空; j : 定义t 31 for(i = 0 ; s[i]!= '\0' && s[i] == ' ' ; i++); //全为空格的时候会将s的最后一个字符‘\0’存储 32 //保证每次往后扫一次用for,往后扫的次数要跳跃无规律用while 33 j = 0; 34 while(s[i] != '\0'){ //把s串copy到t,但是连续的空格只copy一次 35 if(s[i] == ' ' && s[i-1] ==' ')//连续的空格第一个要,后面的都不要 36 { //一个一个copy到t字符串同时判断 37 i++; 38 continue; 39 } 40 41 if(s[i] == '?') 42 { 43 t[j] = '!'; 44 j++; 45 i++; 46 continue; //回到循环开头 47 } 48 if( s[i] != 'I') 49 { 50 t[j] = tolower(s[i]); 51 i++; 52 j++; 53 continue; 54 } 55 56 57 58 t[j] = s[i]; 59 i++; 60 j++; 61 62 //不能copy连续的空格 ,先读变量值,为下一轮循环做准备 63 //此时t没有处理'\0' 后面读出字符串会出错 64 } 65 t[j] = '\0';//给t补上结尾符; 66 67 j = 0 ; 68 while(t[j]!='\0') 69 { 70 if(t[j]=='I'&&(j==0||isIndepent(t[j-1]))&&isIndepent(t[j+1])) 71 { 72 cout<<"you"; 73 j++; 74 continue; 75 }else 76 if(t[j]=='m'&&t[j+1]=='e'&&(j==0||isIndepent(t[j-1]))&&isIndepent(t[j+2])) 77 { 78 cout<<"you"; 79 j += 2; 80 continue; 81 }else 82 if(t[j]==' '&&isPunctuation(t[j+1])) 83 { 84 j++; 85 }else 86 if(t[j]=='c'&&t[j+1]=='a'&&t[j+2]=='n'&&t[j+3]==' '&&t[j+4]=='y'&&t[j+5]=='o'&&t[j+6]=='u'&&(j==0||isIndepent(t[j-1])&&isIndepent(t[j+7]))) 87 { 88 cout<<"I can"; 89 j += 7; 90 } 91 else 92 { 93 cout<<t[j]; 94 j++; 95 96 } 97 98 } 99 cout<<endl; 100 } 101 102 //主函数 103 int main() 104 { 105 int n; 106 string s; 107 cin >>n; 108 getchar();// 吸收回车 《cstdio> 109 for ( int i=0; i<n ; i++) 110 { 111 getline(cin , s); 112 cout << s << endl ; 113 cout << "AI: "; 114 go(s); // 根据s输出AI的回答 115 } 116 117 return 0; 118 } 119
另外,我还把实践二给做了,实际上实践二便是kmp的应用;
题目如下:
给定两个由英文字母组成的字符串 String 和 Pattern,要求找到 Pattern 在 String 中第一次出现的位置,并将此位置后的 String 的子串输出。如果找不到,则输出“Not Found”。
本题旨在测试各种不同的匹配算法在各种数据情况下的表现。各组测试数据特点如下:
- 数据0:小规模字符串,测试基本正确性;
- 数据1:随机数据,String 长度为 10510^5105,Pattern 长度为 101010;
- 数据2:随机数据,String 长度为 10510^5105,Pattern 长度为 10210^2102;
- 数据3:随机数据,String 长度为 10510^5105,Pattern 长度为 10310^3103;
- 数据4:随机数据,String 长度为 10510^5105,Pattern 长度为 10410^4104;
- 数据5:String 长度为 10610^6106,Pattern 长度为 10510^5105;测试尾字符不匹配的情形;
- 数据6:String 长度为 10610^6106,Pattern 长度为 10510^5105;测试首字符不匹配的情形。
输入格式:
输入第一行给出 String,为由英文字母组成的、长度不超过 10610^6106 的字符串。第二行给出一个正整数 NNN(≤10\le 10≤10),为待匹配的模式串的个数。随后 NNN 行,每行给出一个 Pattern,为由英文字母组成的、长度不超过 10510^5105 的字符串。每个字符串都非空,以回车结束。
输出格式:
对每个 Pattern,按照题面要求输出匹配结果。
输入样例:
abcabcabcabcacabxy
3
abcabcacab
cabcabcd
abcabcabcabcacabxyz
输出样例:
abcabcacabxy
Not Found
Not Found
代码如下:实际上就是kmp,代码一模一样,这里便不赘述了,主要这里要多一个ans记录一下就好了;
1 #include<iostream> 2 #include<stdio.h> 3 using namespace std; 4 5 int next1[1000005]; 6 void getnext(string t ,int tsize) //求next数组 7 { 8 int k = -1 ; 9 int j = 0; 10 next1[0] = -1; 11 while(j<tsize-1) 12 { 13 if(k==-1||t[j] == t[k]) 14 { 15 ++j; 16 ++k; 17 next1[j] = k; 18 }else 19 k = next1[k]; 20 21 } 22 } 23 24 int kmp(string s ,string t ,int ssize,int tsize) //kmp匹配 25 { 26 int i = 0 ; 27 int j = 0 ; 28 while(i<ssize&&j<tsize) 29 { 30 if(j==-1||s[i]==t[j]) 31 { 32 i++; 33 j++; 34 }else 35 { 36 j = next1[j]; 37 } 38 } 39 if(j==tsize) 40 { 41 return i-j+1; 42 }else 43 return -1; 44 } 45 string s ; 46 string t ; 47 int n ; 48 int ssize; 49 int tsize; 50 int ans ; 51 int main() 52 { 53 cin>>s; 54 cin>>n; 55 ssize = s.size(); 56 while(n--) 57 { 58 cin>>t; 59 tsize = t.size(); 60 getnext(t,tsize); 61 ans = kmp(s,t,ssize,tsize); //记录ans,看是否能找到匹配 62 if(ans==-1) //无找到匹配 63 { 64 printf("Not Found\n"); 65 }else //找到匹配 66 { 67 for(int i = ans-1 ;i < ssize; i++) //输出匹配后面的所有字符; 68 { 69 cout<<s[i]; 70 } 71 cout<<endl; 72 } 73 74 } 75 return 0; 76 }
总结:其实这一章学的东西并不简单,还是需要花时间去琢磨的,但是收获还是非常大;
对于上一章定的目标,我觉得十字链表我是很生疏的,不是那么熟练,感觉比较吃力;
下一章的目标:能够吸收算法的主要核心思想,并加以运用,并且多打题,多锻炼思维;