字符串问题(一)

字符串问题

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");
}

这种方法也最easy想到也是最传统的解决方式,对于该方法和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]就可以达到效果,并且原理easy理解,代码表达也比較直观、简单。

以下说说原理:

目的:我们期望在前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,如是转载文则注明原出处,谢谢:)


posted @ 2017-06-25 21:59  mfmdaoyou  阅读(260)  评论(0编辑  收藏  举报