在這一章中,老師教了我們四種數據結構: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 }
總結:其實這一章學的東西並不簡單,還是需要花時間去琢磨的,但是收獲還是非常大;
對於上一章定的目標,我覺得十字鏈表我是很生疏的,不是那么熟練,感覺比較吃力;
下一章的目標:能夠吸收算法的主要核心思想,並加以運用,並且多打題,多鍛煉思維;