4.动规3

116 阅读10分钟



回文串分割(Palindrome Partitioning)

给出一个字符串s,分割s使得分割出的每一个子串都是回文串计算将字符串s分割成回文分割结果的最小切割数例如:给定字符串s="aab",返回1,因为回文分割结果["aa","b"]是切割一次生成的。

"aab"

问题

将字符串s分割成回文分割结果的最小切割数

状态

字符串前i个字符的最小分割分割的段数,也就是最小分割数+1

"aab"

F[0] = 1-->"a"

F[1] = is(str[j, 1])?(j>0?F[j-1]+1:1) : F[1-1]+1 = 1-->"a" "a" || "aa" (0<=j<=i)

F[2] = is(str[j, 2])?(j>0?F[j-1]+1:1) : F[2-1]+1 = 2-->"aa" "b" || "aab" (0<=j<=i)

"abbab"

F[0] = 1-->"a"

F[1] = is(str[j, 1])?(j>0?F[j-1]+1:1) : F[1-1]+1 = 2-->"ab" "b"(0<=j<=i)

F[2] = is(str[j, 2])?(j>0?F[j-1]+1:1) : F[2-1]+1 = 2-->"abb" "bb" "b" (0<=j<=i)

F[3] = is(str[j, 3])?(j>0?F[j-1]+1:1) : F[3-1]+1 = 1-->"abba" "bba" "ba" "a" (0<=j<=i)

F[4] = is(str[j, 4])?(j>0?F[j-1]+1:1) : F[4-1]+1 = 3-->"abbab" "bbab" "bab" "ab" "b" (0<=j<=i)

F[4]漏掉了这些状态:"abbab" "abba" "abb" "ab" "a" (0<=j<=i)

F[4-1]+1就是"abba"+"b"分割方式

上面状态不完美的原因是忽略了部分子状态,因此重新找状态:

F[4]: if is(str[0, j]) F[i] = (j>0?F[j-1]+1:1) > (i>0?F[i-1]+1:1)? F[i-1]+1 : (j>0?F[j-1]+1:1);break; (0<=j<=i)

(j>0?F[j-1]+1:1)就是"abbab" "bbab" "bab" "ab" "b"情况

(i>0?F[i-1]+1:1)就是"abbab" "abba" "abb" "ab" "a"情况

转移方程

not prefect:F[i] = is(str[j, i])? (j>0?F[j-1]+1:1) : F[i-1]+1 (0<=j<=i)

prefect:F[i] = (j>0?F[j-1]+1:1) > (i>0?F[i-1]+1:1)? F[i-1]+1 : (j>0?F[j-1]+1:1);

初始状态

F[0] = 1

返回

F[s.length - 1] - 1

代码

不完美的状态转移方程:17/20

Java

import java.util.*;


public class Solution {
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    public int minCut (String s) {
        int len = s.length();
        int F[] = new int[len];
        //F[0] = 1
        //F[i] = is(str[j, i])? (j>0?F[j-1]+1:1) : F[i-1]+1  (0<=j<=i)
        for(int i=0; i<len; ++i){
            for(int j=0; j<=i; ++j){
                F[i] = isPalindrom(s, j, i)?(j>0?F[j-1]+1:1): F[i-1]+1;
                if(isPalindrom(s, j, i))break;
            }
        }
        return F[len-1]-1;
    }
    boolean isPalindrom(String s, int left, int right){
        while(left < right){
            if(s.charAt(left++) != s.charAt(right--))
                return false;
        }
        return true;
    }
}

C++

class Solution {
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    int minCut(string s) {
        // write code here
        //F[i] = is(str[j, i])?F[i-1]:F[i-1]+1  (0<=j<=i)
        int len = s.length();
        int* F = new int[len];
        //F[0] = 1
        //F[i] = is(str[j, i])? (j>0?F[j-1]+1:1) : F[i-1]+1  (0<=j<=i)
        for(int i=0; i<len; ++i){
            for(int j=0; j<=i; ++j){
                F[i] = isPalindrom(s, j, i)?(j>0?F[j-1]+1:1): F[i-1]+1;
                if(isPalindrom(s, j, i))break;
            }
        }
        return F[len-1]-1;
    }
    bool isPalindrom(string s, int left, int right){
        while(left < right){
            if(s[left++] != s[right--])
                return false;
        }
        return true;
    }
};
完美转移方程:通过所有测试用例

Java

import java.util.*;


public class Solution {
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    public int minCut (String s) {
        int len = s.length();
        int F[] = new int[len];
        //F[0] = 1
        //F[i] = is(str[j, i])? (j>0?F[j-1]+1:1) : F[i-1]+1  (0<=j<=i)
        for(int i=0; i<len; ++i){
            for(int j=0; j<=i; ++j){
                //if is(str[0, j]) F[4] = (j>0?F[j-1]+1:1) > F[i-1]+1? F[i-1]+1 : (j>0?F[j-1]+1:1)
                if(isPalindrom(s, j, i)){
                    F[i] = (j>0?F[j-1]+1:1) > (i>0?F[i-1]+1:1)? F[i-1]+1 : (j>0?F[j-1]+1:1);
                    break;
                }
            }
        }
        return F[len-1]-1;
    }
    boolean isPalindrom(String s, int left, int right){
        while(left < right){
            if(s.charAt(left++) != s.charAt(right--))
                return false;
        }
        return true;
    }
}

C++

class Solution {
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    int minCut(string s) {
        // write code here
        //F[i] = is(str[j, i])?F[i-1]:F[i-1]+1  (0<=j<=i)
        int len = s.length();
        int* F = new int[len];
        //F[0] = 1
        //F[i] = is(str[j, i])? (j>0?F[j-1]+1:1) : F[i-1]+1  (0<=j<=i)
        for(int i=0; i<len; ++i){
            for(int j=0; j<=i; ++j){
                if(isPalindrom(s, j, i)){
                    F[i] = (j>0?F[j-1]+1:1) > (i>0?F[i-1]+1:1)? F[i-1]+1 : (j>0?F[j-1]+1:1);
                    break;
                }
            }
        }
        return F[len-1]-1;
    }
    bool isPalindrom(string s, int left, int right){
        while(left < right){
            if(s[left++] != s[right--])
                return false;
        }
        return true;
    }
};

虽然完美的解决了这道题

在这里插入图片描述

但对于极度扣性能的我来说,一定可以找到更舒服的状态和定义更完美的转移方程

状态

字符串前i个字符的最小分割分割次数

拿"abbab"来找状态

F[0] = 0 --> "a"

F[2] = 1 --> "a" "b" ("ab" | "a" "b")

F[3] = 1 --> "a" "bb" ("abb" | "ab" "b" | "a" "b" "b" | "a" "bb" | "ab" "b")

...

每个状态只需要关注两种方向的组合就行了

F[i-1]+1对应[0, i]的组合(0<=i<=s.len) F[j-1]+1对应[j, i]的组合(j=0;j<=i;++j)

执行逻辑:

F[i] = if isPalindrom(str[j,i]) min((j>0?F[j-1]+1:1), (i>0?F[i-1]+1:1)) (j=0;j<=i;++j)(0<=i<=s.len)

最终状态转移方程:

if isPalindrom(str[j,i]) F[i] = min((j>0?F[j-1]+1:0), (i>0?F[i-1]+1:0)); (j=0;j<=i;++j)(0<=i<=s.len)

返回

F[s.len-1]

代码

Java

import java.util.*;


public class Solution {
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    public int minCut (String s) {
        int len = s.length();
        int F[] = new int[len];
        //F[0] = 1
        //F[i] = is(str[j, i])? (j>0?F[j-1]+1:1) : F[i-1]+1  (0<=j<=i)
        for(int i=0; i<len; ++i){
            for(int j=0; j<=i; ++j){
                if(isPalindrom(s, j, i)){
                    F[i] = Math.min((j>0?F[j-1]+1:0), (i>0?F[i-1]+1:0));
                    break;
                }
            }
        }
        return F[len-1];
    }
    boolean isPalindrom(String s, int left, int right){
        while(left < right){
            if(s.charAt(left++) != s.charAt(right--))
                return false;
        }
        return true;
    }
}

C++

class Solution {
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    int minCut(string s) {
        // write code here
        //F[i] = is(str[j, i])?F[i-1]:F[i-1]+1  (0<=j<=i)
        int len = s.length();
        int* F = new int[len];
        //F[0] = 1
        //F[i] = is(str[j, i])? (j>0?F[j-1]+1:1) : F[i-1]+1  (0<=j<=i)
        for(int i=0; i<len; ++i){
            for(int j=0; j<=i; ++j){
                if(isPalindrom(s, j, i)){
                    F[i] = min((j>0?F[j-1]+1:0), (i>0?F[i-1]+1:0));
                    break;
                }
            }
        }
        return F[len-1];
    }
    bool isPalindrom(string s, int left, int right){
        while(left < right){
            if(s[left++] != s[right--])
                return false;
        }
        return true;
    }
};

C++ string lenght()和size()成员方法都可以求字符串长度

在这里插入图片描述

内存和运行时间都有所提高

再看另外一种思路

问题

将字符串s分割成回文分割结果的最小切割数

状态

字符串s前i个字符分割成回文分割结果的最小切割数

看"aab"字符串

F[1] = "a" --> 0

F[2] = "aa" --> 0

F[3] = "aab" F[2]+1 --> 1

F[3]可以用上F[2]的状态

看"aabaab"字符串

F[1] = "a" --> 0

F[2] = "aa" --> 0

F[3] = "aab", F[1]|"ab", F[2]|"b" --> 1

F[4] = "aaba", F[1]|"aba", F[2]|"ba", F[3]|"a"

F[5] = "aabaa" --> 0

F[6] = "aabaab", F[2]|"baab", F[5]|"a" --> 1

看"abbab"字符串

F[1] = "a" --> 0

F[2] = "ab" --> 1

F[3] = "abb", F[1]|"bb" 1, F[2]|"b" 2 --> 1

F[4] = "abba" --> 0

F[5] = "abbab", F[4]|"b" --> 1

转移方程

F[i]: j<i && isPalindrom(str[j+1, i]) min{F[i], F[j]+1}

初始状态

全给最大值

F[i] = i-1 (i)

返回

F[n]

class Solution {
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    int minCut(string s) {
        vector<int> minc(s.length()+1);
        //F[i] = i-1
        for(int i=1; i<=s.length(); ++i)
            minc[i] = i-1;
        for(int i=2; i<=s.length(); ++i){
            //isPalindrom(str[1,i]) 前i个字符是回文
            if(isPalindrom(s, 0, i-1)){
                minc[i] = 0;
                continue;
            }
            //isPalindrom(str[j,i]) j到i字符是回文,str[0,i]就是[0,j]+1
            for(int j=1; j<i; ++j){
                if(isPalindrom(s, j, i-1))
                    minc[i] = min(minc[i], minc[j]+1);
            }
        }
        return minc[s.size()];
    }
    bool isPalindrom(string s, int left, int right){
        while(left < right){
            if(s[left++] != s[right--])
                return false;
        }
        return true;
    }
};

F[0]给-1,就可以将j==0合并到循环里

class Solution {
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    int minCut(string s) {
        vector<int> minc(s.length()+1);
        //F[i] = i-1
        for(int i=0; i<=s.length(); ++i)
            minc[i] = i-1;
        //F[0]给-1,就可以将j==0合并到循环里
        for(int i=2; i<=s.length(); ++i){
            //isPalindrom(str[1,i]) 前i个字符是回文
            //isPalindrom(str[j,i]) j到i字符是回文,str[0,i]就是[0,j]+1
            for(int j=0; j<i; ++j){
                if(isPalindrom(s, j, i-1))
                    minc[i] = min(minc[i], minc[j]+1);
            }
        }
        return minc[s.size()];
    }
    bool isPalindrom(string s, int left, int right){
        while(left < right){
            if(s[left++] != s[right--])
                return false;
        }
        return true;
    }
};

还是不太满意,因为判断一个字符串是不是回文时间复杂度是O(n)

继续对判断字符串是不是回文的函数进行动规优化-->整个字符串任意区间是否是回文结果保存在二维矩阵中,判断分割数的循环中再判断一个区间是不是回文时间复杂度就是O(1)了

问题

字符串区间[i, j]是否是回文串

状态

区间[i, j]是否是回文串

转移方程

F[i, j]: [j+1, j-1] && s[i] == s[j] (j>=i状态才有意义)

j<i+1 即j==i return true;

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

简单举个例子

F[3,4]就依赖于F[4,3]的状态,也就是当前的状态依赖于二维矩阵中左下角的状态,j>=i状态才有意义则二维矩阵从定义得到所有状态的结果为之都保持上三角矩阵,我们需要定义n^2的矩阵(n是字符串长度)

class Solution {
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    int minCut(string s) {
        vector<vector<int>> ispal(s.length(), vector<int>(s.length(), 0));
        ispal[0][0] = 1;
        for(int i=s.length()-1; i>=0; --i){
            for(int j=i; j<s.length(); ++j){
                if(i == j) ispal[i][j] = 1;
                else if(j - i == 1) ispal[i][j] = (s[i] == s[j]);
                else
                    ispal[i][j] = (ispal[i+1][j-1] && s[i] == s[j]);
            }
        }
        vector<int> minc(s.length()+1);
        //F[i] = i-1
        for(int i=0; i<=s.length(); ++i)
            minc[i] = i-1;
        //F[0]给-1,就可以将j==0合并到循环里
        for(int i=2; i<=s.length(); ++i){
            //isPalindrom(str[1,i]) 前i个字符是回文
            //isPalindrom(str[j,i]) j到i字符是回文,str[0,i]就是[0,j]+1
            for(int j=0; j<i; ++j){
                if(ispal[j][i-1])
                    minc[i] = min(minc[i], minc[j]+1);
            }
        }
        return minc[s.size()];
    }
};



编辑距离(Edit Distance)

给定两个单词word1和word2,请计算将word1转换为word2至少需要多少步操作。 你可以对一个单词执行以下3种操作:

( a) 在单词中插入一个字符

( b) 删除单词中的一个字符

( c) 替换单词中的一个字符

问题

word1转成word2的最小操作次数(编辑数)

分析

"ab"转成"bc"有三种办法:

(1).删除'a'-->"b";插入'c'-->"bc"

(2).'a'替换成'b'-->"bb";'b'替换成'c'-->"bc"

(3).'a'替换成''-->"b";插入'c'-->"bc"

子问题

word1前i个字符转成word2的最小操作次数

状态

F(i, j): word1前i个字符转成word2的前j个字符的最小操作次数

"ab"(word1)转成"bc"(word2)
F(i): word1前i个字符转成word2的最小操作次数

F(i):F(i-1)+1  (删除第i个字符)

F(1):最好的情况是strlen(word2)-1,最差是strlen(word2)

这个状态只考虑了删除,重新考虑:...如下:

当前的状态要考虑能够使用到前面已知的状态,当前word1前i个字符转成word2的前j个字符的状态可以通过:word1的前i-1个字符转成word2的前j-1个字符的状态加可能需要的一次替换^[word1的前i-1已经转成word2的前j-1个,则word1的第i个字符至少一次替换就可以得到word2的第j个字符从而word1转成word2,如果word1[i]==word2[j]则不用替换]得到; word1的前i个字符转成word2的前j-1个字符的状态加一次插入^[word1的前i个字符已经转成word2的前j-1个字符,则word1插入word2的第j个字符就可以得到word2]得到; word1的前i-1个字符转成word2的前j个字符的状态加一次删除^[word1的前i-1个字符已经转成word2的前j个字符,则word1删除word1的第i个字符就可以得到word2]得到; 三种前面已知的状态得到,因此当前状态word1前i个字符转成word2的前j个字符的值就取自三者中最小的

F(1, 1): "a" --> "b" 发生一次替换 = 1

F(2, 1): "ab" --> "b" F(1, 1)+删除'b' = 2

F(1, 2): "a" --> "bc" F(1, 1)+插入'c' = 2

F(2, 2): "ab" --> "bc" min{F(1, 1):"bb" + 'b'替换成'c' = 2; F(2, 1):"b" + 插入'c' = 3; F(1, 2):"bcb" + 'b'删除 = 3} = 2

F(2, 2)可以通过:F(1, 1)加一次替换; F(2, 1)加一次插入; F(1, 2)加一次删除得到,取三者最小值即可

具体步骤如下:

状态有两个参数,很明显所有的状态需要二维矩阵才能表示完

下标j012
is[i/j]""bc
0""状态:F(0, 0)
始末:""-->""
操作:无
编辑数:0
状态:F(0, 1)
始末:""-->"b"
操作:插入"b"
编辑数:1
状态:F(0, 2)
始末:""-->"bc"
操作:插入"b";插入"b"
编辑数:2
1a状态:F(1, 0)
始末:"a"-->""
操作:删除"a"
编辑数:1
状态:F(1, 1)
始末:"a"-->"b"
操作:
 替换:"a"-->"b" = F(0, 0)+1
 插入:""-b->"b" = F(1, 0)+1
 删除:"ba"-a->"b" = F(0, 1)+1
编辑数:min{F(0, 0), F(1, 0), F(0, 1)}+1 = 1
状态:F(1, 2)
始末:"a"-->"bc"
操作:
 替换:"ba"-->"bc" = F(0, 1)+1
 插入:"b"-c->"bc" = F(1, 1)+1
 删除:"bca"-a->"bc" = F(0, 2)+1
编辑数:min{F(0, 1), F(1, 1), F(0, 2)}+1 = 2
2b状态:F(2, 0)
始末:"ab"-->""
操作:删除"a";删除"b"
编辑数:2
状态:F(2, 1)
始末:"ab"-->"b"
操作:
 替换:"b"-->"b" = F(1, 0)+0^[条件判断word1[2]==word1[1]]
 插入:"b"-c->"bc" = F(2, 0)+1
 删除:"bca"-a->"bc" = F(1, 1)+1
编辑数:min{F(1, 0), F(2, 0), F(1, 1)}+1 = 1
状态:F(2, 2)
始末:"ab"-->"bc"
操作:
 替换:"bb"-->"b" = F(1, 1)+1
 插入:"b"-c->"bc" = F(2, 1)+1
 删除:"bcb"-b->"bc" = F(1, 2)+1
编辑数:min{F(1, 1), F(2, 1), F(1, 1)}+1 = 1
F(i, j)利用三个已知状态的状态转移方向如下:

在这里插入图片描述

F(i, j)转移方程提取

F(i, j)可以通过

  • 替换:F(i-1, j-1) + (word1[i]==word2[j]?0:1)
  • 插入:F(i, j-1) + 1
  • 删除:F(i-1, j) + 1

字符串string下标是从0开始的,因此判断word1的第i个和word2的第j个字符是否相同应该是:word1[i-1]==word2[j-1]

转移方程

F(i, j):

初始状态

F(0, 0) = 0

F(i, 0) = i (删除)

F(0, j) = j (插入)

返回

F(strlen(word1), strlen(word2))

需要注意的是word1,word2只要有一个是空字符串,就直接返回非空字符串的长度

代码

class Solution {
public:
    /**
     * 
     * @param word1 string字符串 
     * @param word2 string字符串 
     * @return int整型
     */
    int minDistance(string word1, string word2) {
        // write code here
        int word1_len = word1.size(); //i
        int word2_len = word2.size(); //j
        if(!word1_len) return word2_len;
        if(!word2_len) return word1_len;
        vector<vector<int>> mid_res(word1_len+1, vector<int>(word2_len+1, 0));
        //F(i, 0) = i (删除)
        for(int i=1; i<=word1_len; ++i)
            mid_res[i][0] = i;
        // F(0, j) = j (插入)
        for(int j=1; j<=word2_len; ++j)
            mid_res[0][j] = j;
        for(int i=1; i<=word1_len; ++i){
            for(int j=1; j<=word2_len; ++j){
                mid_res[i][j] = min(mid_res[i-1][j-1]+(word1[i-1]==word2[j-1]?0:1), min(mid_res[i-1][j]+1, mid_res[i][j-1]+1));
            }
        }
        return mid_res[word1_len][word2_len];
    }
};



不同子序列(Distinct Subsequences)

给定两个字符串S和T,返回S子序列等于T的不同子序列个数有多少个?

字符串的子序列是由原来的字符串删除一些字符(也可以不删除)在不改变相对位置的情况下的剩余字符(例如,"ACE"is a subsequence of"ABCDE"但是"AEC"不是)

例如:

S="nowcccoder", T = "nowccoder"

返回3

问题

字符串S和T,相同的子序列个数

子问题

S字符串的前i个字符与T字符串的前j个字符相同的子序列的个数

状态

F[i, j]

分析

F[i, 0] = 1: j取0时,S字符串的前i个字符与T字符串的前0个字符的相同子序列是1-->空集是任何集合的子集

F[0, j] = 0: i取0时,S字符串已经是空,则不能找到T的子序列

S="rabbbit", T = "rabbit"中:

F[1, 1] = 1 = F[0, 0] + F[0, 1]

F[0, 0] = 1, F[0, 1] = 0

转移方程

S[i] == T[j]:
    F[i, j]:F[i-1, j-1] + F[i-1, j] ①
S[i] != T[j]:
    F[i, j]:F[i-1, j]
j0123456
T[0,j]"""r""ra""rab""rabb""rabbi""rabbit"
iS[0,i]F[i,j]
0""1000000
1"r"1①:1②:0②:0②:0②:0②:0
2"ra"1②:1①:1②:0②:0②:0②:0
3"rab"1②:1②:1①:1①:0②:0②:0
4"rabb"1②:1②:1①:2①:1②:0②:0
5"rabbb"1②:1②:1①:3①:3②:0②:0
6"rabbbi"1②:1②:1②:3②:3①:3②:0
7"rabbbit"1②:1②:1②:3②:3②:3①:3

初始状态

F[i, 0] = 1

F[0, j] = 0

返回

F[S.size(), T.size()]

代码

class Solution {
public:
    /**
     * 
     * @param S string字符串 
     * @param T string字符串 
     * @return int整型
     */
    int numDistinct(string S, string T) {
        // write code here
        int len_s = S.size();
        int len_t = T.size();
        vector<vector<int>> F(len_s+1, vector<int>(len_t+1, 0));
        //F[i, 0] = 1
        //F[0, j] = 0
        for(int i=0; i<=len_s; ++i)
            F[i][0] = 1;
        //for(int j=0; j<=len_s; ++j)
            //F[0][j] = 0;
        for(int i=1; i<=len_s; ++i)
            for(int j=1; j<=len_t; ++j){
                if(S[i-1] == T[j-1]) //字符串下别从0开始,第i个字符下标就是i-1
                    F[i][j] = F[i-1][j-1]+F[i-1][j];
                else
                    F[i][j] = F[i-1][j];
            }
        return F[len_s][len_t];
    }
};