动态规划-字符串处理二题

来源:互联网 发布:驴妈妈app 知乎 编辑:程序博客网 时间:2024/05/17 21:39

第一题来自leetcode: 给一个字母组成的字符串,问最少插入多少字符,使其变成回文串。

这题估计着可以用动态规划解,不过乍看上去,不知如何下手。问题在于如何处理“最少插入字符”。显然最多插入个数是n-1(n是原字符串长度,其中一个做中点),这时原字符串没有任何片断可以作为回文;最少是0,原串已经是回文。

OK,有思路了,我们可以先找到原串中已有的回文字符串片断总长度x,那么 n-x 就是最少插入字符长度。

动态规划,用dp[i][j] 表示对于起始位置为i,长度为j的子字符串中,离散的回文片断总长度。最优子结构如下:


由于子结构是长度减一的子串,那么对于我们的两层遍历,外层应该是长度,里层是起始字符位置。同时注意所有长度为1的片断回文长度为1,作为起点。代码见下:

int turn2palindrome(const string& str){    int s = str.size();    int ** dp = new int*[s];    for(int i=0;i<s;++i){        dp[i] = new int[s+1]();    }    for(int i=0;i<s;++i){        dp[i][1] = 1; //initialize    }    for(int j=2;j<=s;++j){        for(int i=0;i+j<=s;++i){            if(str[i] == str[i+j-1]){                dp[i][j] = 2 + dp[i+1][j-2];            }else{                dp[i][j] = max(dp[i+1][j-1], dp[i][j-1]);            }        }    }    int res = s - dp[0][s];    /*delete dp*/    return res;} 


第二题同样来自leetcode: 一个全由拉丁字母(小写)组成的字符串,可能存在重复字符,问其中最长的无重复字符的子字符串长度。

首先可以估计,它的主要运算是一个时间复杂度为 O(n^2)的过程,对于每一个字符作为起始,考虑每一个新的字符加入后的新子串。问题在于如何确定这个新的字符是否在之前的子串中有过出现。暴力方法中这一步要花 O(n), 这样整个算法需要时间 O(n^3)

我们注意到题目提示字符串全部由26个小写字母组成,如果我们用一个一维数组存有该字母上次出现的位置,就可以在O(1)时间内确定它是否在目前的子串中出现,这样可以将总的时间降到O(n^2)。

动态规划,我们有dp[i][j],表示起点为str[i],长度为j的子串中,最长的无重复字符串长度。最优子结构式如下。

注意对于所有长度为1的子串,dp[i][1] = 1。实现代码如下:

int longestSubstrWithoutDuplicate(const string& str){    int n = str.size();    int *alpha = new int[26](); //record latest occurance of alpha char    for(int i=0;i<26;++i){        alpha[i] = -1;    }    int *pos = new int[n](); //pos[i] means previous occurance of str[i], -1 if not yet    for(int i=0;i<n;++i){        int j = str[i]-'a';        pos[i] = alpha[j];        alpha[j] = i;    }    int **dp = new int*[n]; //dp[i][j] means for substr starting at str[i] with length j, longest substr length without duplicate    for(int i=0;i<n;++i){        dp[i] = new int[n+1]();        dp[i][1] = 1;    }    int maxlength=1, start=0;    for(int j=2;j<=n;++j){        for(int i=0;i+j<=n;++i){            if(pos[i+j-1] < i && dp[i][j-1] == j-1){                dp[i][j] = j;                if(maxlength < j){                    maxlength = j;                    start = i;                }            }else{                dp[i][j] = max(dp[i+1][j-1], dp[i][j-1]);            }        }    }  /*delete dp[][], pos[] and alpha[]*/      printf("method3: result is %s of length %d\n", str.substr(start, maxlength).c_str(), maxlength);    return maxlength;}

20131213续,从微博网友得到一个O(n)的算法,很不错,赞一个。

string longeststrwithoutrepeat(const string& str){    int n = str.size();    if(n==0)    return str;    int* pos = new int[26]();    for(int i=0;i<26;++i){        pos[i] = -1;    }    int* dp = new int[n]();  //length of longest substr without duplicate char that ends at str[i]    dp[0] = 1;    pos[str[0] - 'a'] = 0;    int maxlength = 1, end=0;    for(int i=1;i<n;++i){        dp[i] = i - pos[str[i] - 'a'];        if(dp[i] > dp[i-1] + 1)            dp[i] = dp[i-1] + 1;        if(dp[i] == dp[i-1] + 1){            if(maxlength < dp[i]){                maxlength = dp[i];                end = i;            }        }        pos[str[i] - 'a'] = i;    }    /* delete dp, pos */    return str.substr(end-maxlength+1, maxlength);}

为什么可以把时间降到线性O(n)?我觉得原因在于该问题有其特殊性:

1.必要条件唯一。对于尾字符str[i],距离其上次出现的长度 i - pos[i]必定大于等于dp[i],即 i - pos[i]成为dp[i]的上限。

2. 充分条件唯一。当str[i] != str[i-1]时,dp[i]最多等于dp[i-1] + 1, dp[i]仅仅依赖于dp[i-1]。

反思:

再反过来重新思考其他问题,比如前文的”最少插入字符“。对于离散回文片断长度的查找,问题是在其子结构式中,关系比较的式子是dp[j] ? dp[i], 这里的i是子字符串的起始,它是由末尾 j 和长度 l 两个正交的参数共同确定的。这使得我们必须使用二维数组dp[i][j]作为动态规划的临时结构,从而算法的时间复杂度也就无法再降低了。

小结:

对于字符串处理的最大/最小问题,多数时候O(n^2)是最佳性能,但不排除有些情况能够降到线性O(n)

原创粉丝点击