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

第一题来自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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值