码迷,mamicode.com
首页 > 其他好文 > 详细

字符串问题(一)

时间:2015-08-20 06:48:58      阅读:385      评论:0      收藏:0      [点我收藏+]

标签:字符串   算法   

字符串问题

1.左旋问题

2.字符包含问题

3.字符匹配KMP

4.编辑距离

5.最大回文子串,公共子串

6.最大公共子序列,回文子序列,上升子序列

7.基本字符串函数实现

8.大整数的加,,,,

9.合法回文,数字串

10.正则匹配,最长公共前缀,简化路经


1) 左旋字符串

定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部,如把字符串abcdef左旋转2位得到字符串cdefab。请实现字符串左旋转的函数,要求对长度为n的字符串操作的时间复杂度为O(n),空间复杂度为O(1).


思路一、暴力移位法

voidleftshiftone(char *s,int n) {

char t = s[0];

//保存第一个字符

for (int i = 1; i <n; ++i) {

s[i - 1] = s[i];

}

s[n - 1] = t;

}

如此,左移m位的话,可以如下实现:

void leftshift(char*s,int n,int m) {

while (m--) {

leftshiftone(s,n);

}

}


思路二、指针翻转法

#include<iostream>

#include<string>

usingnamespacestd;

voidrotate(string&str,intm) {

if(str.length() == 0 || m <= 0)

return;

intn = str.length();

if(m % n <= 0)

return;

intp1 = 0, p2 = m;

intk = (n - m) - n % m;

//交换p1,p2指向的元素,然后移动p1,p2

while(k--) {

swap(str[p1],str[p2]);

p1++;

p2++;

}

//重点,都在下述几行。

//处理尾部,r为尾部左移次数

intr = n - p2;

while(r--) {

inti = p2;

while(i> p1)

{

swap(str[i],str[i - 1]);

i--;

}

p2++;

p1++;

}

//比如一个例子,abcdefghijk

// p1p2

//当执行到这里时,defghia b c j k

//p2+m出界了,

//r=n-p2=2,所以以下过程,要执行循环俩次。

//第一次:j步步前移,abcjk->abjck->ajbck->jabck

//然后,p1++,p2++,p1a,p2k

//p1 p2

//第二次:defghij a b c k

//同理,此后,k步步前移,abck->abkc->akbc->kabc

}

//在尾部处理作了一点不同的处理优化

voidrotate(string&str,intm) {

if(str.length() == 0 || m < 0)

return;

//初始化p1,p2

intp1 = 0, p2 = m;

intn = str.length();

//处理m大于n

if(m % n == 0)

return;

//循环直至p2到达字符串末尾

while(true){

swap(str[p1],str[p2]);

p1++;

if(p2 < n - 1)

p2++;

else

break;

}

//处理尾部,r为尾部循环左移次数

intr = m - n % m;

while(r--)

//r = 1.

//外循环执行一次

{

inti = p1;

chartemp = str[p1];

while(i < p2)

//内循环执行俩次

{

str[i]= str[i + 1];

i++;

}

str[p2]= temp;

}

}


思路三,三步翻转法

char*invert(char*start,char*end) {

chartmp, *ptmp = start;

while(start != NULL && end != NULL && start < end) {

tmp= *start;

*start= *end;

*end= tmp;

start++;

end--;

}

returnptmp;

}

char*left(char*s,intpos)//pos为要旋转的字符个数,或长度,下面主函数测试中,pos=3

{

intlen = strlen(s);

invert(s,s + (pos - 1));

//如上,X->X^T,abc->cba

invert(s+ pos, s + (len - 1));//如上,Y->Y^T,def->fed

invert(s,s + (len - 1));

//如上,整个翻转,(X^TY^T)^T=YX,cbafed->defabc

returns;

}


2) 字符串是否包含问题

假设这有一个各种字母组成的字符串A,和另外一个字符串B,字符串里B的字母数相对少一些。什么方法能最快的查出所有小字符串B里的字母在大字符串A 里都有?


几种常规解法

O(n*m)的轮询方法

O(mlogm)+O(nlogn)+O(m+n)的排序方法

O(n+m)的计数排序方法


usingnamespacestd;

//计数排序,O(n+m)

voidCounterSort(stringstr,string&help_str) {

//辅助计数数组

inthelp[26] = {0};

//help[index]存放了等于index+ ‘A‘的元素个数

for(inti = 0; i < str.length(); i++)

{

intindex = str[i] - ‘A‘;

help[index]++;

}

//求出每个元素对应的最终位置

for(intj = 1; j < 26; j++)

help[j]+= help[j-1];

//把每个元素放到其对应的最终位置

for(intk = str.length() - 1; k >= 0; k--)

{

intindex = str[k] - ‘A‘;

intpos = help[index] - 1;

help_str[pos]= str[k];

help[index]--;

}

}

//线性扫描O(n+m)

voidCompare(stringlong_str,stringshort_str) {

intpos_long = 0;

intpos_short = 0;

while(pos_short < short_str.length() && pos_long <long_str.length()) {

//如果pos_long递增直到long_str[pos_long]>= short_str[pos_short]

while(

long_str[pos_long]< short_str[pos_short] && pos_long < long_str.length

()- 1)

pos_long++;

//如果short_str有连续重复的字符,pos_short递增

while(short_str[pos_short] == short_str[pos_short + 1])

pos_short++;

if(long_str[pos_long] != short_str[pos_short])

break;

pos_long++;

pos_short++;

}

if(pos_short == short_str.length())

cout<<"true"<<endl;

else

cout<<"false"<<endl;

}


O(n+m)hashtable的方法

O(n+m)bool数组方法

O(n+m)bitmap



#include<iostream>

#include<string>

usingnamespacestd;

intmain(){

stringstr1 = "ABCDEFGHLMNOPQRS";

stringstr2 = "DCGSRQPOM";

//开辟一个辅助数组并清零

inthash[26] = { 0 };

//num为辅助数组中元素个数

intnum = 0;

//扫描短字符串

for(intj = 0; j < str2.length(); j++) {

//将字符转换成对应辅助数组中的索引

intindex = str1[j] - ‘A‘;

//如果辅助数组中该索引对应元素为0,则置1,num++;

if(hash[index] == 0) {

hash[index]= 1;

num++;

}

}

//扫描长字符串

for(intk = 0; k < str1.length(); k++) {

intindex = str1[k] - ‘A‘;

//如果辅助数组中该索引对应元素为1,num--;为零的话,不作处理(不写语句)

if(hash[index] == 1) {

hash[index]= 0;

num--;

if(num == 0)

//m==0,即退出循环。

break;

}

}

//num0说明长字符串包含短字符串内所有字符

if(num == 0)

cout<<"true"<<endl;

else

cout<<"false"<<endl;

return0;

}


bool AcontainsB(char*A,char *B) {

int have = 0;

while (*B) {

have |= 1 <<(*(B++) - ‘A‘);

// A..Z 对应为0..26

}

while (*A) {

if ((have & (1<< (*(A++) - ‘A‘))) == 0) {

return false;

}

}

return true;

}


O(n+m)的素数方法


1.定义最小的26个素数分别与字符‘A‘‘Z‘对应。

2.遍历长字符串,求得每个字符对应素数的乘积。

3.遍历短字符串,判断乘积能否被短字符串中的字符对应的素数整除。

4.输出结果。


#include<iostream>

#include<string>

#include"BigInt.h"

usingnamespacestd;

//素数数组

intprimeNumber[26] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41,43, 47,

53,59, 61, 67, 71, 73, 79, 83, 89, 97, 101 };

intmain(){

stringstrOne = "ABCDEFGHLMNOPQRS";

stringstrTwo = "DCGSRQPOM";

//这里需要用到大整数

CBigIntproduct = 1;

//大整数除法的代码,下头给出。

//遍历长字符串,得到每个字符对应素数的乘积

for(inti = 0; i < strOne.length(); i++) {

intindex = strOne[i] - ‘A‘;

product= product * primeNumber[index];

}

//遍历短字符串

intj = 0

for(; j < strTwo.length(); j++) {

intindex = strTwo[j] - ‘A‘;

//如果余数不为0,说明不包括短字串中的字符,跳出循环

if(product % primeNumber[index] != 0)

break;

}

//如果积能整除短字符串中所有字符则输出"true",否则输出"false"

if(strTwo.length() == j)

cout<<"true"<<endl;

else

cout<<"false"<<endl;

return0;

}


3)字符串匹配问题KMP

思路1:暴力法O(nm)



思路2:KMP O(n+m)


voidcomputer_prefix(constchar*pattern,intnext[]){

inti, j=-1;

next[0]= j;

constintm = strlen(pattern);

for(i=1;i<m; i++){

while(j>-1&& (pattern[j+1] != pattern[i]))

j= next[j];

if(pattern[j+1]== pattern[i])

j++;

next[i]= j;

}

}


intkmp(constchar*text,constchar*pattern){

inti, j=-1;

constintn = strlen(text);

constintm = strlen(pattern);

if(m==0)return0;

if(n<m)return-1;

int*next = malloc(sizeof(int)*m);

assert(next);

computer_prefix(pattern,next);

for(i=0;i<n-m; i++){

while(j>-1&& pattern[j+1] != text[i])

j= next[j];

if(pattern[j+1]== text[i])

j++;

if(j==m-1){

free(next);

returni-j;

}

}

free(next);

return-1;

}



4)字符串编辑距离问题


AB2个字符串。要用最少的字符操作将字符串A转换为字符串B。这里所说的字符操作包括:

(1)删除一个字符;
(2)
插入一个字符;
(3)
将一个字符改为另一个字符。
将字符串A变换为字符串B所用的最少字符操作数称为字符串AB的编辑距离,记为d(A,B)。试设计一个有效算法,对任给的2个字符串AB,计算出它们的编辑距离d(A,B)
要求:
输入:第1行是字符串A,2行是字符串B
输出:字符串AB的编辑距离d(A,B)

首先给定第一行和第一列,然后,每个值d[i,j]这样计算:d[i][j] = min(d[i-1][j]+1,d[i][j-1]+1,d[i-1][j-1]+(s1[i] == s2[j]?0:1));
最后一行,最后一列的那个值就是最小编辑距离


#include <stdio.h>

#include <string.h>

chars1[1000],s2[1000];

int min(int a,intb,int c) {

int t = a < b ?a : b;

return t < c ? t: c;

}

voideditDistance(int len1,int len2)

{

int** d=newint*[len1+1];

for(intk=0;k<=len1;k++)

d[k]=newint[len2+1];


int i,j;

//初始化

for(i = 0;i <=len1;i++)

d[i][0] = i;

for(j = 0;j <=len2;j++)

d[0][j] = j;


for(i = 1;i <=len1;i++) {

for(j = 1;j <=len2;j++){

int cost = s1[i]== s2[j] ? 0 : 1;

int deletion =d[i-1][j] + 1;

int insertion =d[i][j-1] + 1;

int substitution= d[i-1][j-1] + cost;

d[i][j] =min(deletion,insertion,substitution);

}

}

printf("%d\n",d[len1][len2]);

for(intk=0;i<=len1;k++)

delete[] d[k];

delete[] d;

}

int main()

{

while(scanf("%s%s",s1,s2) != EOF)

editDistance(strlen(s1),strlen(s2));

}


5) 最长回文子串问题

思路1:暴力法O(n^3)

思路2:动态规划O(n^2) O(n^2)


class Solution {

public:

stringlongestPalindrome(string s) {

const int n= s.size();

if(n <=1) return s;

boolf[n][n];

fill_n(&f[0][0], n*n, false);

size_tmax_len=1, start = 0;

for(inti=n-1; i>=0; i--){

for(intj=i; j< n; j++){

f[i][j] = s[i]== s[j] &&(j-i<2 || f[i+1][j-1]);

if(f[i][j]){

if(max_len < (j -i +1)){

max_len = j - i +1;

start = i;

}

}

}

}

returns.substr(start, max_len);

}

};

思路3:KMP

第二个思路来源于字符串匹配,最长回文串有如下性质: 对于串S,假设它的ReverseS‘,那么S的最长回文串是SS‘的最长公共字串。

例如S = abcddca, S‘ =acddcbaSS‘的最长公共字串是cddc也是S的最长回文字串。如果S‘是模式串,我们可以对S’的所有后缀枚举(S0,S1, S2, Sn) 然后用每个后缀和S匹配,寻找最长的匹配前缀。

思路4:Manacher算法


classSolution {

public:

//Transform S into T.

//For example, S = "abba", T = "^#a#b#b#a#$".

//^ and $ signs are sentinels appended to each end to avoid boundschecking

stringpreProcess(string s) {

intn = s.length();

if(n == 0) return"^$";

stringret ="^";

for(inti = 0; i < n; i++) ret += "#"+ s.substr(i, 1);

ret+="#$";

returnret;

}

stringlongestPalindrome(string s) {

stringT = preProcess(s);

constintn = T.length();

//T[i]为中心,向左/右扩张的长度,不包含T[i]自己,

//因此P[i]是源字符串中回文串的长度

intP[n];

intC = 0, R = 0;

for(inti = 1; i < n - 1; i++) {

inti_mirror = 2 * C - i; //equals to i‘ = C - (i-C)

P[i]= (R > i) ? min(R - i, P[i_mirror]) : 0;

//Attempt to expand palindrome centered at i

while(T[i + 1 + P[i]] == T[i - 1 - P[i]])

P[i]++;

//If palindrome centered at i expand past R,

//adjust center based on expanded palindrome.

if(i + P[i] > R) {

C= i;

R= i + P[i];

}

}

//Find the maximum element in P.

intmax_len = 0;

intcenter_index = 0;

for(inti = 1; i < n - 1; i++) {

if(P[i] > max_len) {

max_len= P[i];

center_index= i;

}

}

returns.substr((center_index - max_len) / 2, max_len);

}

};


6) 最长公共子串

给定两个字符串,求出它们之间最长的相同子字符串的长度。
最直接的解法自然是找出两个字符串的所有子字符串进行比较看他们是否相同,然后取得相同最长的那个。对于一个长度为n的字符串,它有n(n+1)/2个非空子串。所以假如两个字符串的长度同为n,通过比较各个子串其算法复杂度大致为O(n4)。这还没有考虑字符串比较所需的时间。简单想想其实并不需要取出所有的子串,而只要考虑每个子串的开始位置就可以,这样可以把复杂度减到O(n3)
但这个问题最好的解决办法是动态规划法,在后边会更加详细介绍这个问题使用动态规划法的契机:有重叠的子问题。进而可以通过空间换时间,让复杂度优化到O(n2),代价是空间复杂度从O(1)一下子提到了O(n2)
从时间复杂度的角度讲,对于最长公共子串问题,O(n2)已经是目前我所知最优的了,也是面试时所期望达到的。但是对于空间复杂度O(n2)并不算什么,毕竟算法上时间比空间更重要,但是如果可以省下一些空间那这个算法就会变得更加美好。所以进一步的可以把空间复杂度减少到O(n),这是相当美好了。但有一天无意间让我发现了一个算法可以让该问题的空间复杂度减少回原来的O(1),而时间上如果幸运还可以等于O(n)

思路1:暴力求解

intlongestCommonSubstring_n3(conststring& str1, conststring& str2)

{

size_tsize1 = str1.size();

size_tsize2 = str2.size();

if(size1 == 0 || size2 == 0) return0;


//the start position of substring in original string

intstart1 = -1;

intstart2 = -1;

//the longest length of common substring

intlongest = 0;


//record how many comparisons the solution did;

//it can be used to know which algorithm is better

intcomparisons = 0;


for(inti = 0; i < size1; ++i)

{

for(intj = 0; j < size2; ++j)

{

//find longest length of prefix

intlength = 0;

intm = i;

intn = j;

while(m< size1 && n < size2)

{

++comparisons;

if(str1[m] != str2[n]) break;


++length;

++m;

++n;

}

if(longest < length)

{

longest= length;

start1= i;

start2= j;

}

}

}

returnlongest;

}


该解法的思路就如前所说,以字符串中的每个字符作为子串的端点,判定以此为开始的子串的相同字符最长能达到的长度。其实从表层上想,这个算法的复杂度应该只有O(n2)因为该算法把每个字符都成对相互比较一遍,但关键问题在于比较两个字符串的效率并非是O(1),这也导致了实际的时间复杂度应该是满足Ω(n2)O(n3)


思路2:动态规划

L[i,j]表示以s[i]t[j]为结尾的相同子串的最大长度。L[i+1,j+1]=(s[i]==t[j]?L[i,j]+1:0)


intlongestCommonSubstring_n2_n2(conststring& str1, conststring& str2)

{

size_tsize1 = str1.size();

size_tsize2 = str2.size();

if(size1 == 0 || size2 == 0) return0;


vector<vector<int>> table(size1, vector<int>(size2,0));

//the start position of substring in original string

intstart1 = -1;

intstart2 = -1;

//the longest length of common substring

intlongest = 0;

intcomparisons = 0;

for(intj = 0; j < size2; ++j)

{

++comparisons;

table[0][j]= (str1[0] == str2[j] ? 1 :0);

}


for(inti = 1; i < size1; ++i)

{

++comparisons;

table[i][0]= (str1[i] == str2[0] ? 1 :0);


for(intj = 1; j < size2; ++j)

{

++comparisons;

if(str1[i] == str2[j])

{

table[i][j]= table[i-1][j-1]+1;

}

}

}


for(inti = 0; i < size1; ++i)

{

for(intj = 0; j < size2; ++j)

{

if(longest < table[i][j])

{

longest= table[i][j];

start1= i-longest+1;

start2= j-longest+1;

}

}

}

returnlongest;

}


动态规划法优化– 能省一点是一点

仔细回顾之前的代码,其实可以做一些合并让代码变得更加简洁,比如最后一个求最长的嵌套for循环其实可以合并到之前计算整个表的for循环之中,每计算完L[i,j]就检查它是的值是不是更长。当合并代码之后,就会发现内部循环的过程重其实只用到了整个表的相邻两行而已,对于其它已经计算好的行之后就再也不会用到,而未计算的行曽之前也不会用到,因此考虑只用两行来存储计算值可能就足够。于是新的经过再次优化的算法就有了:

intlongestCommonSubstring_n2_2n(conststring& str1, conststring& str2)

{

size_tsize1 = str1.size();

size_tsize2 = str2.size();

if(size1 == 0 || size2 == 0) return0;


vector<vector<int>> table(2, vector<int>(size2,0));


//the start position of substring in original string

intstart1 = -1;

intstart2 = -1;

//the longest length of common substring

intlongest = 0;


//record how many comparisons the solution did;

//it can be used to know which algorithm is better

intcomparisons = 0;

for(intj = 0; j < size2; ++j)

{

++comparisons;

if(str1[0] == str2[j])

{

table[0][j]= 1;

if(longest == 0)

{

longest= 1;

start1= 0;

start2= j;

}

}

}


for(inti = 1; i < size1; ++i)

{

++comparisons;

//with odd/even to swith working row

intcur = ((i&1) == 1);//indexfor current working row

intpre = ((i&1) == 0);//indexfor previous working row

table[cur][0]= 0;

if(str1[i] == str2[0])

{

table[cur][0]= 1;

if(longest == 0)

{

longest= 1;

start1= i;

start2= 0;

}

}


for(intj = 1; j < size2; ++j)

{

++comparisons;

if(str1[i] == str2[j])

{

table[cur][j]= table[pre][j-1]+1;

if(longest < table[cur][j])

{

longest= table[cur][j];

start1= i-longest+1;

start2= j-longest+1;

}

}

else

{

table[cur][j]= 0;

}

}

}

returnlongest;

}

跟之前的动态规划算法代码相比,两种解法并没有实质的区别,完全相同的嵌套for循环,只是将检查最长的代码也并入其中,然后table中所拥有的行也只剩下2个。此解法的一些技巧在于如何交换两个行数组作为工作数组。可以交换数组中的每个元素,异或交换一对指针。上边代码中所用的方法类似于后者,根据奇偶性来决定那行数组可以被覆盖,哪行数组有需要的缓存数据。不管怎么说,该算法都让空间复杂度从O(n2)减少到了O(n),相当有效。


动态规划法再优化– 能用一点就只用一点(按对角线来计算)


最长公共子串问题的解法优化到之前的模样,基本是差不多了,Wikipedia上对于这个问题给出的解法也就到上述而已。但思考角度不同,还是有意外的惊喜的。不过要保持算法的时间复杂度不增加,算法的基本思路方针还是不能变的。而如若按对角线为行,一行行计算的话,其实就只需要缓存下一个数据就可以将对角线上的格子填充完毕。从字符串上讲,就是偏移一个字符串的头,然后跟另一个字符串比较看在如此固定的位置下能找到最长的公共子串是多长。

比较复杂,参考http://www.cnblogs.com/ider/p/longest-common-substring-problem-optimization.html

7) 最长公共子序列(LCS)问题

思路1:暴力求解


思路2:动态规划

动态规划的一个计算最长公共子序列的方法如下,以两个序列XY为例子:设有二维数组f[i][j]表示Xi位和Yj位之前的最长公共子序列的长度,则有:

f[1][1] = same(1,1)

f[i][j] = max{f[i ?1][j ? 1] +same(i,j), f[i ? 1][j] ,f[i][j ? 1]}

其中,same(a,b)X的第a位与Y的第b位完全相同时为“1”,否则为“0”。此时,f[i][j]中最大的数便是XY的最长公共子序列的长度,依据该数组回溯,便可找出最长公共子序列。

该算法的空间、时间复杂度均为O(n 2 ),经过优化后,空间复杂度可为O(n),时间复杂度为O(nlogn)


如果我们记字符串XiYjLCS的长度为c[i,j],我们可以递归地求c[i,j]:


c[i,j]= 0 if i=0 or j=0

c[i-1,j-1]+1 if i,j>0 and xi=xj

max(c[i,j-1],c[i-1,j] if i,j>0 and xi≠xj


int lcs(char a[],int a_len, char b[], int b_len){

int i, j;

char c =malloc(sizeof(char)* a_len*b_len);


assert(c);

//初始化

for(i=0; i<a_len; i++){

c[i][0] = a[i]==b[0] ? 1: 0;

}


for(j=0; j<b_len; j++){

c[0][j] = b[j] == a[0] ? 1:0;

}

//递推

for(i=1; i<a_len; i++){

for(j=1; j<b_len; j++){

if(a[i]==b[j]){

c[i][j] = c[i-1][j-1] +1;

}else if(c[i][j-1]>= c[i-1][j]){

c[i][j] = c[i][j-1];

}else{

c[i][j] = c[i-1][j];

}

}

}

int result =c[a_len-1][b_len];

free(c);

return result;

}


相关题:最长回文子序列

思路:动态规划

设字符串为sf(i,j)表示s[i..j]的最长回文子序列。

状态转移方程如下:

i>j时,f(i,j)=0

i=j时,f(i,j)=1

i<j并且s[i]=s[j]时,f(i,j)=f(i+1,j-1)+2

i<j并且s[i]≠s[j]时,f(i,j)=max(f(i,j-1), f(i+1,j) )

注意如果i+1=j并且s[i]=s[j]时,f(i,j)=f(i+1,j-1)+2=f(j,j-1)+2=2,这就是“当i>jf(i,j)=0”的好处。

由于f(i,j)依赖i+1,所以循环计算的时候,第一维必须倒过来计算,从s.length()-10

#include <iostream>

#include <cstring>

using namespace std;

#define MAX 101

#define max(a,b)(a)>(b)?(a):(b)

int main()

{

string s;

while (cin>>s)

{

intf[MAX][MAX];

memset(f,0,sizeof(f));

for (inti=s.length()-1;i>=0;i--)

{

f[i][i]=1;

for (intj=i+1;j<s.length();j++)

if(s[i]==s[j])

f[i][j]=f[i+1][j-1]+2;

else

f[i][j]=max(f[i][j-1],f[i+1][j]);

}

cout<<f[0][s.length()-1]<<endl;

}

return 0;

}

优化空间O(n)

起初先在第0行计算f[s.length()-1],然后用第0行的结果在第1行计算f[s.length()-2],再用第1行的结果在第0行计算f[s.length()-3],以此类推。正在计算的那行设为now,那么计算第now行时,就要用第1-now行的结果。这种方法很巧妙。

当计算完成时,如果s.length()是奇数,则结果在第0行;如果是偶数,则结果在第1行。

#define MAX 101

#define max(a,b)(a)>(b)?(a):(b)

int main()

{

string s;

while (cin>>s)

{

intf[2][MAX];

memset(f,0,sizeof(f));

int now=0;

for (inti=s.length()-1;i>=0;i--)

{

f[now][i]=1;

for (intj=i+1;j<s.length();j++)

if(s[i]==s[j])

f[now][j]=f[1-now][j-1]+2;

else

f[now][j]=max(f[now][j-1],f[1-now][j]);

now=1-now;

}

if(s.length()%2==0)

cout<<f[1][s.length()-1]<<endl;

else

cout<<f[0][s.length()-1]<<endl;

}

return 0;

}

8) 最长上升子序列

题:求一个一维数组arr[i]中的最长递增子序列的长度,如在序列1-12-34-56-7中,最长递增子序列长度为4,可以是1246,也可以是-1246

方法一:DP(O(n2))

LIS[i]表示前i个元素中以i结尾的最长递增序列的长度。要么是1(单独成一个序列),要么就是第i个元素之前的最长递增子序列加1,可以有状态方程:

LIS[i] = max{1,LIS[k]+1},其中,对于任意的k<=i-1arr[i]> arr[k]

这样arr[i]才能在arr[k]的基础上构成一个新的递增子序列。

代码如下:在计算好LIS长度之后,output函数递归输出其中的一个最长递增子序列。

#include <iostream>
using namespace std;

/* 最长递增子序列
LIS
 * 设数组长度不超过
30
 * DP
*/

int dp[31]; /* dp[i]记录到[0,i]数组的LIS
*/
int lis;    /* LIS 长度
*/

int LIS(int * arr, int size)
{
        for(int i = 0; i < size; ++i)
        {
                dp[i] = 1;
                for(int j = 0; j < i; ++j)
                {
                        if(arr[i] > arr[j] &&
dp[i] < dp[j] + 1)
                        {
                                dp[i] = dp[j] +
1;
                                if(dp[i] >
lis)
                                {
                                        lis =
dp[i];
                                }
                        }
                }
        }
        return lis;
}

/* 输出LIS
*/
void outputLIS(int * arr, int index)
{
        bool isLIS = 0;
        if(index < 0 || lis == 0)
        {
                return;
        }
        if(dp[index] == lis)
        {
                --lis;
                isLIS = 1;
        }

        outputLIS(arr,--index);

        if(isLIS)
        {
                printf("%d ",arr[index+1]);
        }
}

void main()
{
        int arr[] = {1,-1,2,-3,4,-5,6,-7};

        /* 输出LIS长度;
sizeof 计算数组长度
*/
       
printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));

        /* 输出LIS
*/
        outputLIS(arr,sizeof(arr)/sizeof(int) -
1);
        printf("\n");
}

这个方法也最容易想到也是最传统的解决方案,对于该方法和LIS,有以下两点说明:

  1. LIS可以衍生出来最长非递减子序列,最长递减子序列,道理是一样的

  2. 对于输出序列,也是可以再申请一数组pre[i]记录子序列中array[i]的前驱,道理跟本节的实现也是一样的

方法二:排序+LCS(O(n2))

这个方法是在Felix’blog(见参考资料)中看到的,因为简单,他在博文中只是提了一句,不过为了练手,虽然懒,还是硬着头皮写一遍吧,正好再写一遍快排,用quicksort +LCS,这个思路还是很巧妙的,因为LIS是单调递增的性质,所以任意一个LIS一定跟排序后的序列有LCS,并且就是LIS本身。代码如下:

#include <iostream>
using namespace std;

/* 最长递增子序列
LIS
 * 设数组长度不超过
30
 * quicksort + LCS
*/

void swap(int * arr, int i, int j)
{
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
}

void qsort(int * arr, int left, int right)
{
        if(left >= right)       return ;
        int index = left;
        for(int i = left+1; i <= right; ++i)
        {
                if(arr[i] < arr[left])
                {
                        swap(arr,++index,i);
                }
        }
        swap(arr,index,left);
        qsort(arr,left,index-1);
        qsort(arr,index+1,right);
}

int dp[31][31];

int LCS(int * arr, int * arrcopy, int len)
{
        for(int i = 1; i <= len; ++i)
        {
                for(int j = 1; j <= len; ++j)
                {
                        if(arr[i-1] ==
arrcopy[j-1])
                        {
                                dp[i][j] =
dp[i-1][j-1] + 1;
                        }else if(dp[i-1][j] >
dp[i][j-1])
                        {
                                dp[i][j] =
dp[i-1][j];
                        }else
                        {
                                dp[i][j] =
dp[i][j-1];
                        }
                }
        }
        return dp[len][len];
}

void main()
{
        int arr[] = {1,-1,2,-3,4,-5,6,-7};
        int arrcopy [sizeof(arr)/sizeof(int)];

        memcpy(arrcopy,arr,sizeof(arr));
       
qsort(arrcopy,0,sizeof(arr)/sizeof(int)-1);

        /* 计算LCS,即LIS长度
*/
        int len = sizeof(arr)/sizeof(int);
        printf("%d\n",LCS(arr,arrcopy,len));
}

方法三:DP+二分查找

编程之美》对于这个方法有提到,不过它的讲解我看得比较难受,好长时间才明白,涉及到的数组也比较多,除了源数据数组,有LIS[i]MaxV[LIS[i]],后来看了大牛Felix的讲解,我才忽然发现编程之美中的这个数组MaxV[LIS[i]]在记录信息上其实是饶了弯的,因为我们在寻找某一长度子序列所对应的最大元素最小值时,完全没必要通过LIS[i]去定位,即没必要与数据arr[i]挂钩,直接将MaxV[i]的下标作为LIS的长度,来记录最小值就可以了(表达能力太次,囧。。。),一句话,就是不需要LIS[i]这个数组了,只用MaxV[i]即可达到效果,而且原理容易理解,代码表达也比较直观、简单。

下面说说原理:

目的:我们期望在前i个元素中的所有长度为len的递增子序列中找到这样一个序列,它的最大元素比arr[i+1]小,而且长度要尽量的长,如此,我们只需记录len长度的递增子序列中最大元素的最小值就能使得将来的递增子序列尽量地长。个人认为这是一种典型的贪心算法.

方法:维护一个数组MaxV[i]记录长度为i的递增子序列中最大元素的最小值,并对于数组中的每个元素考察其是哪个子序列的最大元素,二分更新MaxV数组,最终i的值便是最长递增子序列的长度。这个方法真是太巧妙了,妙不可言。

代码如下:

#include <iostream>
using namespace std;

/* 最长递增子序列
LIS
 * 设数组长度不超过
30
 * DP + BinarySearch
*/

int MaxV[30]; /* 存储长度i+1len)的子序列最大元素的最小值
*/
int len;      /* 存储子序列的最大长度
即MaxV当前的下标*/

/* 返回MaxV[i]中刚刚大于x的那个元素的下标
*/
int BinSearch(int * MaxV, int size, int x)
{
        int left = 0, right = size-1;
        while(left <= right)
        {
                int mid = (left + right) / 2;
                if(MaxV[mid] <= x)
                {
                        left = mid + 1;
                }else
                {
                        right = mid - 1;
                }
        }
        return left;
}

int LIS(int * arr, int size)
{
        MaxV[0] = arr[0]; /* 初始化
*/
        len = 1;
        for(int i = 1; i < size; ++i) /*
寻找arr[i]属于哪个长度LIS的最大元素
*/
        {
                if(arr[i] > MaxV[len-1]) /*
大于最大的自然无需查找,否则二分查其位置
*/
                {
                        MaxV[len++] = arr[i];
                }else
                {
                        int pos =
BinSearch(MaxV,len,arr[i]);
                        MaxV[pos] = arr[i];
                }
        }
        return len;
}

void main()
{
        int arr[] = {1,-1,2,-3,4,-5,6,-7};

        /* 计算LIS长度
*/
       
printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
}

这个方法的实现巧妙而直观,让人有种“啊,原来还可以这样”的感慨,感谢Felix

本文相关代码可以到这里下载。

(全文完)

参考资料:

编程之美2.16

Felix’s Blog最长递增子序列O(NlogN)算法
转载请注明出自http://www.felix021.com/blog/read.php?1587,如是转载文则注明原出处,谢谢:)


版权声明:本文为博主原创文章,未经博主允许不得转载。

字符串问题(一)

标签:字符串   算法   

原文地址:http://blog.csdn.net/tangchenchan/article/details/47796941

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!