这里把最大子序列和放在第一个位置,它并不是字符串相关的问题,事实上它的目的是要找出由数组成的一维数组中和最大的连续子序列。比如[0,-2,3,5,-1,2]应返回9,[-9,-2,-3,-5,-3]应返回-2。
1、动态规划法
设状态为f[j],表示以S[j]结尾的最大连续子序列和(即最大连续子序列的最后一项为S[j],但开头一项没有规定,不一定是S[1]),状态转移方程如下:
f[j] = max(f[j-1]+S[j],S[j]) 1<=j<=n
target = max(f[j]) 1<=j<=n
int MaxSubarray(int data[],int length) { vector<int> f(length+1,0); int result=INT_MIN; for (int i=0;i<length;i++) { f[i+1] = max(f[i]+data[i],data[i]); result= max(f[i+1],result); } return result; }
void MaxSubarray(int data[],int length, int &start, int &end, int &result) { vector<int> f(length+1,0); result=INT_MIN; int curstart =0; for (int i=0;i<length;i++) { if (f[i]+data[i]>data[i]) { f[i+1] = f[i]+data[i]; } else { f[i+1] = data[i]; curstart =i; } if (f[i+1]>result) { result = f[i+1]; start = curstart; end =i; } } }
其实数组的问题,最好留点心,有一大部分题目是可以用分治的办法完成的,比如说这道题里面:最大子序列和可能出现在三个地方,1)整个出现在输入数据的左半部分,2)整个出现在输入数据的右半部分,3)或者跨越输入数据的中部从而占据左右两个半部分。可以有以下代码:
int MaxSumRec( const vector<int> & a, int left, int right ) { if( left == right ) // Base case if( a[ left ] > 0 ) return a[ left ]; else return 0; int center = ( left + right ) / 2; int maxLeftSum = maxSumRec( a, left, center ); int maxRightSum = maxSumRec( a, center + 1, right ); int maxLeftBorderSum = 0, leftBorderSum = 0; for( int i = center; i >= left; i-- ) { leftBorderSum += a[ i ]; if( leftBorderSum > maxLeftBorderSum ) maxLeftBorderSum = leftBorderSum; } int maxRightBorderSum = 0, rightBorderSum = 0; for( int j = center + 1; j <= right; j++ ) { rightBorderSum += a[ j ]; if( rightBorderSum > maxRightBorderSum ) maxRightBorderSum = rightBorderSum; } return max3( maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum ); }
和上一问题一样,这是数组序列中的问题,比如arr={1,5,8,2,3,4}的最长递增子序列是1,2,3,4
结合上一题的思路,在数组的这类问题里面使用动态规划还是很常见的,从后向前分析,很容易想到,第i个元素之前的最长递增子序列的长度要么是1(比如说递减的数列),要么就是第i-1个元素之前的最长递增子序列加1。
假设在目标数组array的前i个元素中,最长递增子序列的长度为LIS[i],那么
LIS[i] = max{1,LIS[k]+1},其中,对于任意的k<=i-1,array[i] > arr[k]
即如果array[i+1]大于array[k],那么第i+1个元素可以接在LIS[k]长的子序列后面构成一个更长的子序列,或者array[i+1]自己构成一个长度为1的子序列。
int LongestAscendSubarray(int data[],int length) { vector<int> LIS(length,1); for (int i=1;i<length;i++) for (int j=0;j<i;j++) { if (data[i]>data[j] && LIS[j]+1>LIS[i]) { LIS[i] = LIS[j]+1; } } int maxValue =0; for (int i=0;i<LIS.size();i++) { if (LIS[i]>maxValue) { maxValue = LIS[i]; } } return maxValue; }
2.上面是一个比较基本的解法,我们可以转换一下思路。现在我们想找到前i个元素中的一个递增子序列,是的这个递增子序列的最大值比array[i+1]小,且长度尽可能的长。这样将array[i+1]加载递增子序列后,便可以找到以array[i+1]为最大元素的最长递增子序列。
假设在数组的前i个元素中,以array[i]为最大元素的最长递增子序列的长度为LIS[i],同时利用MaxV[i]保存长度为i的的递增序列最大元素的最小值,这样就可以减少判断的次数
int LongestAscendSubarray(int data[],int length) { vector<int> LIS(length,1); vector<int> MaxV(length+1,0); MaxV[1] = data[0]; int nMaxLen=1; for (int i=0;i<length;i++) { for (int j=nMaxLen;j>0;j--) { if (data[i]>MaxV[j]) { LIS[i]=j+1; break; } } if (LIS[i]>nMaxLen) { nMaxLen = LIS[i]; MaxV[nMaxLen] = data[i]; } if (MaxV[LIS[i]>data[i]]) { MaxV[LIS[i]] = data[i]; } } return nMaxLen; }
在递增序列中,如果i<j,那么MaxV[i]<MaxV[j]。因此上面的代码
for (int j=nMaxLen;j>0;j--) { if (data[i]>MaxV[j]) { LIS[i]=j+1; break; } }可以利用二分搜索法进行加速,这样就可以吧时间复杂度降到O(nlogn)。
int LongestSubarray(int data[],int length) { vector<int> LIS(length,1); vector<int> MaxV(length,0); MaxV[0] = data[0]; int nMaxLen=1; for (int i=1;i<length;i++) { if (data[i]>MaxV[nMaxLen-1]) { ++nMaxLen; MaxV[nMaxLen] = data[i]; } else { int pos=BinarySearch(MaxV,1,nMaxLen,data[i]); MaxV[pos] = data[i]; } } return nMaxLen; }
二分搜索代码如下:
int BinarySearch(vector<int>& data,int start, int end, int target) { int mid; while (start<=end) { mid=(start+end)>>1; if (data[mid]==target) { return mid; } else if (data[mid]<target) start = mid+1; else end = mid-1; } return end; }
f[i][j] = f[i-1][j-1]+1 if s1[i]==s2[j]
b a b
c 0 0 0
a 0 1 0
b 1 0 2
a 0 2 0
计算完f之后,找到f的最大值,然后按照斜对角线读取,从而得到最长公共子串。
string LCS(string &s1,string &s2) { vector<vector<int>> f(s1.size()+1,vector<int>(s2.size()+1,0)); int result=0; int row=0; //记录最大LCS最后一个字符的行位置 for (int i=0;i<s1.size();i++) for (int j=0;j<s2.size();j++) { if (s1[i]==s2[j]) { f[i+1][j+1]=f[i][j]+1; } if (f[i+1][j+1]>result) { result = f[i+1][j+1]; row = i; } } string lcs = s1.substr(row-result+1,result); return lcs; }
这才是笔试面试中出现频度最高的问题,前面提到了一个最长公共子串,这里的最长公共子序列与它的区别在于最长公共子序列不要求在原字符串中是连续的,比如ADEFG和ABCDEG的最长公共子序列是ADEG。
设c[i][j]表示s1[1..i]与s2[1..j]的最长公共子序列,则状态转移方程为:
1.递归方法
int LCS_Recursive(string &s1,string& s2,int m,int n) { if (m<0 || n<0) return 0; if (s1[m] == s2[n]) { return LCS_Recursive(s1,s2,m-1,n-1)+1; } else { return LCS_Recursive(s1,s2,m-1,n)>LCS_Recursive(s1,s2,m,n-1)?LCS_Recursive(s1,s2,m-1,n):LCS_Recursive(s1,s2,m,n-1); } }
2. 动态规划方法
void LCS(string &s1, string &s2,int &lcsLen,vector<vector<int>>& flag) { vector<vector<int>> f(s1.size()+1,vector<int>(s2.size()+1,0)); for (int i=0;i<s1.size();i++) for (int j=0;j<s2.size();j++) { if (s1[i]==s2[j]) { f[i+1][j+1] = f[i][j]+1; flag[i][j] = 3; } else { if (f[i][j+1]>f[i+1][j]) { f[i+1][j+1] = f[i][j+1]; flag[i][j] = 2; } else { f[i+1][j+1] = f[i+1][j]; flag[i][j] = 1; } } } lcsLen = f[s1.size()][s2.size()]; }
void PrintLCS(string& s1,vector<vector<int>>& flag) //迭代 { int i = flag.size()-1; int j = flag[0].size()-1; vector<char> result; while (i>=0 && j>=0) { if (flag[i][j]==1) { j--; } else if (flag[i][j]==2) { i--; } else { result.push_back(s1[i]); i--; j--; } } for (int k=result.size()-1;k>=0;k--) { cout<<result[k]<<" "; } cout<<endl; } // 递归 void PrintLCS2(string& s1,vector<vector<int>>& flag,int m,int n) { if (m<0 || n<0) return; if (flag[m][n]==3) { PrintLCS2(s1,flag,m-1,n-1); cout<<s1[m]<<endl; } if (flag[m][n]==1) PrintLCS2(s1,flag,m,n-1); if (flag[m][n]==2) PrintLCS2(s1,flag,m-1,n); }
要求子串中的字符不能重复,判重问题首先想到的就是hash,寻找满足要求的子串,最直接的方法就是遍历每个字符起始的子串,辅助hash,寻求最长的不重复子串,由于要遍历每个子串故复杂度为O(n^2),n为字符串的长度,辅助的空间为常数hash[256]。代码如下:
/* 最长不重复子串 我们记为 LNRS */ int maxlen; int maxindex; void output(char * arr); /* LNRS 基本算法 hash */ char visit[256]; void LNRS_hash(char * arr, int size) { for(int i = 0; i < size; ++i) { memset(visit,0,sizeof(visit)); visit[arr[i]] = 1; for(int j = i+1; j < size; ++j) { if(visit[arr[j]] == 0) { visit[arr[j]] = 1; } else { if(j-i > maxlen) { maxlen = j - i; maxindex = i; } break; } } } output(arr); }
字符串的问题,很多都可以用动态规划处理,比如这里求解最长不重复子串,和前面讨论过的最长递增子序列问题就有些类似,在LIS(最长递增子序列)问题中,对于当前的元素,要么是与前面的LIS构成新的最长递增子序列,要么就是与前面稍短的子序列构成新的子序列或单独构成新子序列。
这里我们采用类似的思路:某个当前的字符,如果它与前面的最长不重复子串中的字符没有重复,那么就可以以它为结尾构成新的最长子串;如果有重复,那么就与某个稍短的子串构成新的子串或者单独成一个新子串。
我们来看看下面两个例子:
1)字符串“abcdeab”,第二个a之前的最长不重复子串是“abcde”,a与最长子串中的字符有重复,但是它与稍短的“bcde”串没有重复,于是它可以与其构成一个新的子串,之前的最长不重复子串“abcde”结束;
2)字符串“abcb”,跟前面类似,最长串“abc”结束,第二个字符b与稍短的串“c”构成新的串;
我们貌似可以总结出一些东西:当一个最长子串结束时(即遇到重复的字符),新的子串的长度是与(第一个重复的字符)的下标有关的。
于是类似LIS,对于每个当前的元素,我们“回头”去查询是否有与之重复的,如没有,则最长不重复子串长度+1,如有,则是与第一个重复的字符之后的串构成新的最长不重复子串,新串的长度便是当前元素下标与重复元素下标之差。
可以看出这里的动态规划方法时间复杂度为O(N^2),我们可以与最长递增子序列的动态规划方案进行对比,是一个道理的。代码如下:
/* LNRS 动态规划求解 */ int dp[100]; void LNRS_dp(char * arr, int size) { int i, j; maxlen = maxindex = 0; dp[0] = 1; for(i = 1; i < size; ++i) { for(j = i-1; j >= 0; --j) { if(arr[j] == arr[i]) { dp[i] = i - j; break; } } if(j == -1) { dp[i] = dp[i-1] + 1; } if(dp[i] > maxlen) { maxlen = dp[i]; maxindex = i + 1 - maxlen; } } output(arr); }
char* GetMaxSubStr( char* str ) { int hash[256]; //hash记录每个字符的出现位置 memset(hash,-1,sizeof(hash)); int strLen=strlen(str); int curStart=0; int maxStart=0; int maxEnd =0; int curLen=0; for (int i=0;i<strLen;i++) { if (hash[str[i]]==-1) //如果没有重复 { hash[str[i]] = i; cout<<hash[str[i]]<<endl; } else { curLen = i-curStart; //当前长度 if (curLen>maxEnd-maxStart+1) //如果当前长度最长 { maxStart=curStart; maxEnd = i-1; } curStart = hash[str[i]]+1; //更新当前最长的起点 hash[str[i]] = i; //更新字符出现的位置 } } if (maxEnd == 0)//没有重复字符,返回源串 { char* reStr = new char[strLen + 1]; strcpy(reStr, str); return reStr; } curLen = strLen-curStart; //当前长度 if (curLen>maxEnd-maxStart+1) //如果当前长度最长 { maxStart=curStart; maxEnd = strLen-1; } int MaxLength = maxEnd-maxStart+1; char* res=new char[MaxLength+1]; memset(res,0,MaxLength+1); strncpy(res,str+maxStart,MaxLength); return res; }
回文串就是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。回文子串,顾名思义,即字符串中满足回文性质的子串。比如输入字符串 "google”,由于该字符串里最长的对称子字符串是 "goog”,因此输出4。
1.问题解决的基本方法
分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是该函数的加强版。
要判断一个字符串是不是对称的,不是一件很难的事情。我们可以先得到字符串首尾两个字符,判断是不是相等。如果不相等,那该字符串肯定不是对称的。否则我们接着判断里面的两个字符是不是相等,以此类推。
<span style="font-family:SimSun;font-size:14px;">#include<iostream> using namespace std; //字符串是否对称 bool isAym(char *cbegin, char *cend) { if(cbegin == NULL || cend ==NULL || cbegin > cend) { return false; } while(cbegin<cend) { if(*cbegin!=*cend) { return false; } cbegin++; cend--; } return true; }</span>
现在我们试着来得到对称子字符串的最大长度。最直观的做法就是得到输入字符串的所有子字符串,并逐个判断是不是对称的。如果一个子字符串是对称的,我们就得到它的长度,最后经过比较,就能得到最长的对称子字符串的长度了。
<span style="font-family:SimSun;font-size:14px;">//O(n*n*n)复杂度的子字符串 int getMaxSym(char * str) { if(str == NULL) return 0; int maxlength = 0, strlength = 0; char *pFirst = str; char *strEnd = str + strlen(str); while(pFirst < strEnd) { char *pLast = strEnd; while(pLast > pFirst) { if(isAym(pFirst, pLast)) { strlength = pLast - pFirst + 1; if(strlength > maxlength) { maxlength = strlength; } } pLast --; } pFirst ++; } return maxlength; }</span>
上述方法的时间效率:由于需要两重while循环,每重循环需要O(n)的时间。另外,我们在循环中调用了IsSym,每次调用也需要O(n)的时间。因此整个函数的时间效率是O(n^3)。
假设输入:abcddcba,按照上述程序,要分割成 'abcddcba’, 'bcddcb’, 'cddc’, 'dd’…等字符串,并对这些字符串分别进行判断。不难发现,很多短子字符串在长些的子字符串中比较过,这导致了大量的冗余判断,根本原因是:对字符串对称的判断是由外向里进行的。
换一种思路,从里向外来判断。也就是先判断子字符串(如dd)是不是对称的。如果它(dd)不是对称的,那么向该子字符串两端各延长一个字符得到的字符串肯定不是对称的。如果它(dd)对称,那么只需要判断它(dd)两端延长的一个字符是不是相等的,如果相等,则延长后的字符串是对称的。
2.改进的解决方案
根据从里向外比较的思路写出如下代码:
<span style="font-family:SimSun;font-size:14px;">//改进后的程序 int getMaxSym2(char * str) { if(str == NULL) return 0; int maxlength = 0; char *ptag = str; while(*ptag !='\0') { //奇数子字符串 char *left = ptag - 1; char *right = ptag + 1; int oddlenght = 1; while(left >= str && *right != '\0' && *left == *right) { left--; right++; oddlenght += 2; } if(oddlenght > maxlength) { maxlength = oddlenght; } //偶数子字符串 left = ptag; right = ptag + 1; int evenlength = 0; while(left >= str && *right != '\0' && *left == *right) { left--; right++; evenlength += 2; } if(evenlength > maxlength) { maxlength = evenlength; } ptag++; } return maxlength; }</span>
由于子字符串的长度可能是奇数也可能是偶数。长度是奇数的字符串是从只有一个字符的中心向两端延长出来,而长度为偶数的字符串是从一个有两个字符的中心向两端延长出来。因此程序中要把这两种情况都考虑进去。
由于总共有O(n)个字符,每个字符可能延长O(n)次,每次延长时只需要O(1)就能判断出是不是对称的,因此整个函数的时间效率是O(n^2)。
上述方法称为朴素算法,关于字符串的题目常用的算法有KMP、后缀数组、AC自动机,这道题目利用扩展KMP可以解答,其时间复杂度也很快O(N*logN)。但是,这里介绍一个专门针对回文子串的算法,其时间复杂度为O(n),这就是manacher算法。
3.manacher算法
算法的基本思路是这样的:把原串每个字符中间用一个串中没出现过的字符分隔#开来(统一奇偶),同时为了防止越界,在字符串的首部也加入一个特殊符$,但是与分隔符不同。同时字符串的末尾也加入'\0'。算法的核心:用辅助数组p记录以每个字符为核心的最长回文字符串半径。也就是p[i]记录了以str[i]为中心的最长回文字符串半径。p[i]最小为1,此时回文字符串就是字符串本身。
示例:原字符串 'abba’,处理后的新串 ' $#a#b#b#a#\0’,得到对应的辅助数组p=[0,1,2,1,2,5,2,1,2,1]。
程序如下,对应的变量解释在后面
<span style="font-family:SimSun;font-size:14px;">char * pre(char *str) { int length = strlen(str); char *prestr = new char[2*length + 3]; prestr[0] = '$'; for(int i=0;i<length;i++) { prestr[2*i+1] = '#'; prestr[2*i+2] = str[i]; } prestr[2*length+1]='#'; prestr[2*length+2]='\0'; return prestr; } int getMaxSym3(char *str) { char *prestr = pre(str); int mx =0, pi=1;//边界和对称中心 int len = strlen(prestr); //辅助数组 int *p = new int[len]; p[0] = 0; for(int i=1;i<len;i++) { if(mx>i) { p[i]=min(mx-i,p[2*pi-i]);//核心 } else { p[i]=1; } while(prestr[i-p[i]]==prestr[i+p[i]]&&i-p[i]>0&&i+p[i]<len) { p[i]++; } if(i+p[i] > mx) { mx = p[i] + i; pi = i; } } //最大回文字符串长度 int maxlen = 0; for(int i=0;i<len;i++) { if(p[i]>maxlen) { maxlen = p[i]; } printf("%d ",p[i]); } delete []prestr; delete []p; return maxlen - 1; } </span>
上面几个变量说明:pi记录具有遍历过程中最长半径的回文字符串中心字符串。mx记录了具有最长回文字符串的右边界的下一个字符。
pi是最长回文字符串(淡蓝色)的中心,如果以j为中心的最大回文串如上如所示,那么i处的情况与j处相同(关于pi的两侧是对称的)。这样便减少了运算量,i的对称位置是2*pi-i。
但是有另外一种情况,就是j的一部分超出蓝色部分,这时p[i]=p[j]就不一定对了,如下图
这就为什么有取最小值这个操作:
if(mx>i) { p[i]=min(mx-i,p[2*pi-i]);//核心
}
剩下的代码就很容易看懂了。
最后遍历一边p数组,找出最大的p[i]-1就是所求的最长回文字符串长度,说明如下:
(1)因为p[i]记录插入分隔符之后的回文字符串半径,所以以i为中心的回文字符串长度为2*p[i]-1。例如:bb=>#b#b#,中间#的半径为3,回文字符串长度为2*3-1;
(2)注意上面两个串的关系。 #b#b#减去一个#号的长度就是原来的2倍。即((2*p[i]-1)-1)/2 = p[i]-1,得证。