最大子序列、最長遞增子序列、最長公共子串、最長公共子序列、字串編輯距離總結
一、最大子序列
即找出由陣列成的一維陣列中和最大的連續子序列。例如{5, -6, 4, 2}的最大子序列是{4, 2},它們的和是6。
思路:假設陣列為num,用dp[i]儲存當遍歷到num[i]時,num[0]~num[i]之間求得的最大子序列的和。
遍歷num,當遍歷到num[i]時,轉換方程如下:
如果dp[i-1]>0,則dp[i] = dp[i-1] + num[i],
否則dp[i] = num[i]。
這麼去想,站在num[i]的角度來看,如果dp[i-1]>0,說明前i-1個數的最大子序列和大於0,那麼num[i]加上一個正數肯定會比num[i]大,因此dp[i] = dp[i-1] + num[i]。如果dp[i-1]<=0,那麼num[i]加上一個非正數肯定比num[i] 小或者相等,所以dp[i] = num[i]
c++程式碼
#include<iostream> using namespace std; int main(){ int num[6]={5, -3, -4, 12, 9, -1}; int len=6; int* dp = new int[6]; int max_len=0; if(len>0){ dp[0] = num[0]; for(int i=1;i<len;i++){ if(dp[i-1]>0){ dp[i] = dp[i-1] + num[i]; }else{ dp[i] = num[i]; } if(dp[i]>max_len) max_len = dp[i]; } } cout<<"最大子序列和為:"<<max_len<<endl; return 0; }
輸出:
最大子序列和為:21
二、最長遞增子序列
給定一個長度為N的陣列,找出一個最長的單調自增子序列(不一定連續,但是順序不能亂)。例如:給定一個長度為6的陣列A{5, 6, 7, 1, 2, 8},則其最長的單調遞增子序列為{5,6,7,8},長度為4.程式碼:
#include<iostream> using namespace std; int maxLen(int arr[], int length){ int* dp = new int[length+1]; int max = 0; for(int i=0;i<length;i++){ dp[i] = 1; for(int j=0;j<i;j++){ if(arr[j]<arr[i] && (dp[j]+1)>dp[i]){ dp[i] = dp[j]+1; } }//for if(dp[i] > max){ max = dp[i]; } }//for return max; } int main(){ int arr[8] = {1, -1, 2, -3, 4, -5, 6, -7}; cout<<maxLen(arr, 8); return 0; }
測試用例:
在序列1,-1,2,-3,4,-5,6,-7中,其最長的遞增子序列為1,2,4,6
給定一個長度為6的陣列A{5, 6, 7, 1, 2, 8},則其最長的單調遞增子序列為{5,6,7,8},長度為4
三、最長公共子串和最長公共子序列
最長公共子串(Longest Common Substring)是串的一個連續的部分。最長公共子序列(Longest Common Subsequence,LCS)不一定是連續的,但是不改變序列中元素在原串中的相對順序,而是從序列中去掉任意的元素獲得新的序列。也就是說,子串中字元的位置必須是連續的,子序列則可以不連續。
(1)最長公共子序列
思想
採用動態規劃的思想,假設比較字串s1和s2的最長公共子序列,len1,len2分別為s1和s2的長度,L[m][n]表示當s1[m]和s2[n]比較完之後得到的最長公共子序列長度。有如下轉換方程:
如果s1[i] == s2[j],那麼L[m][n] = L[m-1][n-1] + 1;
如果s1[i] != s2[j],那麼L[m][n] = max(L[m-1][n], L[m][n-1]);
例子
以騰訊2017年實習生招聘的這道題為例
題目:
構造迴文
給定一個字串s,你可以從中刪除一些字元,使得剩下的串是一個迴文串。如何刪除才能使得迴文串最長呢?
輸出需要刪除的字元個數。
輸入描述:
輸入資料有多組,每組包含一個字串s,且保證:1<=s.length<=1000.
輸出描述:
對於每組資料,輸出一個整數,代表最少需要刪除的字元個數。
輸入例子:
abcda
google
輸出例子:
2
2
思路:
根據迴文串的特點,如果一個字串是迴文串,那麼這個字串和它的逆序串是相等的。所以上述問題轉化為求字串和逆序字串的最長公共子序列長度。
c++程式碼
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
int main(){
string s;
while(cin>>s){
string s1=s;
string s2=s;
reverse(s2.begin(),s2.end());
int len1=s1.length();
int len2=s2.length();
//動態申請一個二維陣列
//因為題目給出了s.length<=1000,因此L的長度可以固定
//直接定義L[1001][1001]也可以,比動態申請陣列更方便
int **L = new int* [len1+1];
int i,j;
for(i=0; i<=len1;i++){
L[i] = new int[len2+1];
}
for(i=0;i<=len1;i++){
for(j=0;j<=len2;j++)
L[i][j]=0; //對陣列進行初始化
}//for
for(i=1;i<=len1;i++){
for(j=1;j<=len2;j++){
if(s1[i-1] == s2[j-1]){
L[i][j] = L[i-1][j-1] + 1;
}else{
L[i][j] = max(L[i-1][j], L[i][j-1]);
}
}
}
cout<<len1-L[len1][len2]<<endl;
}
return 0;
}
例2:
題目描述:怎麼判斷一個字串是對稱的?如果不對稱,求最少需要新增幾個元素讓其變為對稱的?(2017年秋招CVTE面試題)
思路:對稱其實就是迴文,第一問簡單就不說了,第二問和上面例子其實差不多,對於一個不對稱的字串str,求它與它的逆序的最長公共子序列x,然後用長度len-x,即為要新增的元素數
(2)最長公共子串
思路和最長公共子串差不多,也是用動態規劃的方法。唯一要改變的一個地方是轉換方程那,改成:
如果s1[i] == s2[j],那麼L[m][n] = L[m-1][n-1] + 1;
如果s1[i] != s2[j],那麼L[m][n] = 0;
因為要求是連續的,因此只要有一個不相等的出現,那麼就把L[m][n]置為0。
例子
來源:2017年校招全國統一模擬筆試(第二場)程式設計題集合-牛客網
連結:https://www.nowcoder.com/questionTerminal/276712b113c6456c8cf31c5073a4f9d7
時間限制:1秒空間限制:32768K
題目描述:
牛牛有兩個字串(可能包含空格),牛牛想找出其中最長的公共連續子串,希望你能幫助他,並輸出其長度。
輸入描述:
輸入為兩行字串(可能包含空格),長度均小於等於50.
輸出描述:
輸出為一個整數,表示最長公共連續子串的長度
例項1:
輸入
abcde
abgde
Sit it out G
Sit down and shut up
輸出
2
4
程式碼:
#include<iostream>
#include<stack>
#include<cmath>
#include<cstring>
#define MAX 55
using namespace std;
int dp[MAX][MAX];
int main() {
char str1[MAX], str2[MAX];
int len1, len2;
while(cin.getline(str1, MAX)){
cin.getline(str2, MAX);
memset(dp, 0, sizeof(dp));
len1 = strlen(str1);
len2 = strlen(str2);
if(len1 == 0 || len2 == 0){
return 0;
}//if
int i,j,max_len=0;
for(i=1;i<=len1;++i){
for(j=1;j<=len2;++j){
if(str1[i-1] == str2[j-1]){
dp[i][j] = dp[i-1][j-1] + 1;
}
if(dp[i][j] > max_len){
max_len = dp[i][j];
}
}//for
}
cout<<max_len<<endl;
}
return 0;
}
這道題要注意的是輸入的字串可能包含空格!!!因此不能用string了,要用char陣列結合cin.getline函式。
編輯距離,又稱Levenshtein距離,是指兩個字串之間,由一個轉成另一個所需的最少編輯操作次數。
允許的操作包括(1)刪除一個字元(2)插入一個字元(3)將一個字元改為另一個字元
利用動態規劃的思想。用一個二維陣列edit[i][j],表示第一個字串s1的長度為i的子串和第二個字串s2的長度為j的子串的編輯距離。那麼容易得到如下的轉化方程:
if i==0 且 j==0, edit(i,j) = 0
if i==0 且 j>0, edit(i, j) = j
if i>0 且 j==0, edit(i,j) = i
if i>0 且 j>0, edit(i,j) = min{ edit(i-1,j)+1, edit(i, j-1)+1, edit(i-1,j-1)+diff(i,j)}
其中diff(i,j)表示的是s1[i] 和s2[j] 是否相等,如果s1[i] == s2[j],那麼diff(i,j)=0,否則diff(i,j)=1
舉個栗子:
假設s1 = "sailn",s2="failing",len1,len2分別為s1和s2的長度,為了處理邊界的情況,一般在動態申請二維陣列edit的時候多申請一行和一列,變為edit[len1+1][len2+1]。
那麼初始化情況如下:
edit從[1][1]開始算,但是注意比較的s1和s2是從下標0開始。明白了思想後,寫出程式碼不難。
c++程式碼如下:
#include<iostream>
#include<cmath>
using namespace std;
int main(){
string s1,s2;
int i,j;
//比較s1和s2的編輯距離
while(cin>>s1>>s2){
int len1=s1.length();
int len2=s2.length();
if(len1==0 && len2==0){
cout<<0<<endl;
}else if(len1==0 && len2>0){
cout<<len2<<endl;
}else if(len1>0 && len2==0){
cout<<len1<<endl;
}else{
//動態申請一個edit[len1+1][len2+1]的二維陣列
int **edit = new int*[len1+1];
for(i=0;i<=len1;i++){
edit[i] = new int[len2+1];
}
//對第一行和第一列進行初始化
edit[0][0] = 0;
for(i=1;i<=len1;i++){
edit[i][0] = i;
}
for(i=1;i<=len2;i++){
edit[0][i] = i;
}
for(i=1;i<=len1;i++){
for(j=1;j<=len2;j++){
edit[i][j] = min(edit[i-1][j]+1, edit[i][j-1]+1);
if(s1[i-1] == s2[j-1]){
edit[i][j] = min(edit[i][j], edit[i-1][j-1]);
}else{
edit[i][j] = min(edit[i][j], edit[i-1][j-1]+1);
}
}
}
for(i=0;i<=len1;i++){
for(j=1;j<=len2;j++)
cout<<edit[i][j]<<" ";
cout<<endl;
}
cout<<"編輯距離為:"<<edit[len1][len2]<<endl;
}
} //while
return 0;
}
輸出如下: