• 子序列 sub sequence问题,例:最长公共子序列,[LeetCode] Distinct Subsequences(求子序列个数)


    引言

    子序列和子字符串或者连续子集的不同之处在于,子序列不需要是原序列上连续的值。

    对于子序列的题目,大多数需要用到DP的思想,因此,状态转移是关键。

    这里摘录两个常见子序列问题及其解法。

    例题1, 最长公共子序列

    我们知道最长公共子串的求法,先温习一下,它的求法也是使用DP思想,对于 字符串s1 和字符串s2,令 m[i][j] 表示 s1上以s1[i]结尾的子串和s2上s2[j]结尾的子串的最长公共子串长度,因为公共子串必须是连续的,因此状态转移方程:m[i, j] = (s1[i] == s2[j] ? m[i-1, j-1] + 1 : 0)。因为m[i, j]的计算只需要用到 m[i-1, j-1],再之前的就用不着了,因此我们不必用一个二维数组来保存整个m[s1.length()][s2.length()],只需要保存并不断更新m[i-1, j-1]就可以了。

    代码:

    char *LCString(const char* s1, const char* s2){
        if(NULL == s1 || NULL == s2)
            return NULL;
        int size1 = 0, size2 = 0;
        const char* head1 = s1; const char* head2 = s2;
        while(*(head1++) != '') size1++;
        while(*(head2++) != '') size2++;
        printf("%d, %d
    ", size1, size2);
        
        int maxlen = 0, maxend = 0, i = 0, j = 0, tmpPre = 0;
        int m2[size2];
        for(i = 0; i < size2; m2[i] = 0, ++i);
        for(i = 0; i < size1; ++i){
            for(j = 0; j < size2; ++j){
                int len = ((s1[i] == s2[j] ? 1 : 0) + (j > 0 ? tmpPre : 0));
                if(len > maxlen) { maxlen = len; maxend = i;}
                tmpPre = m2[j];
                m2[j] = len;
            }
        }
        
        if(maxlen > 0){//提出最长子字符串
            char* lcs = new char[maxlen + 1];
            for(i = 0; i < maxlen; lcs[maxlen - i - 1] = s1[maxend - i], i++);
            lcs[maxlen] = '';
            return lcs;
        }
        return NULL;
    }

    那么,对于最长公共子序列,如何去求呢?

    首先,如果用m[][]来存长度,最后提出最长子序列要麻烦一些,因为子序列是不连续的。不过虽然麻烦,依旧可行。

    接着,依然假设m[i, j]表示 s1[i]结尾的子串 和s2[j]结尾的子串的 最长公共子序列的长度。 

    那么:

    若s1[i] == s2[j],m[i,j] = m[i-1][j-1] + 1;

    若s1[i] != s2[j],m[i,j] = Max(m[i-1][j], m[i][j-1])。

    这里因为求 m[i,j] 时,m[i-1][j], m[i][j-1], m[i-1][j-1]都有可能用到,因此咱还是老实一点用 二维数组吧。。

    template <typename T> T* Lcseq(T* list1, int size1, T* list2, int size2){
        if(NULL == list1 || NULL == list2)
            return NULL;
        int** m = new int*[size1];
        int i = 0, j = 0;
        int max = 0, maxi = 0, maxj = 0;
        for(; i < size1; i++){
            m[i] = new int[size2];
            for(j = 0; j < size2; j++){
                if(i == 0 && j == 0) m[0][0] = (list1[0] == list2[0] ? 1 : 0);
                else if(i == 0) m[i][j] = (list1[i] == list2[j] ? 1 : m[i][j-1]);
                else if(j == 0) m[i][j] = (list1[i] == list2[j] ? 1 : m[i-1][j]);
                else m[i][j] = (list1[i] == list2[j] ? m[i-1][j-1] + 1 : (m[i][j-1] > m[i-1][j] ? m[i][j-1] : m[i-1][j]));
                if(m[i][j] > max){
                    max = m[i][j];
                    maxi = i;
                    maxj = j;
                }
            }
        }
        //printf("%d, %d, %d
    ", max, maxi, maxj);
        
        //提取最大公共子序列 
        int p1 = maxi, p2 = maxj, p = max;
        T* sub = new T[max];
        while(p1 >= 0 && p2 >= 0){
            
            if(list1[p1] == list2[p2]){
                sub[--p] = list1[p1];
                //printf("p: %d, p1: %d, p2: %d
    ", p, p1, p2);
                p1--;
                p2--;
            }    
            else{
                if(p1 == 0) p2--;
                else if(p2 == 0) p1--;
                else{
                    if(m[p1-1][p2] < m[p1][p2-1]) p2--;
                    else p1--;
                }
            }
            
        }
        return sub;    
    }

    例题2,求子序列的个数,LeetCode

    Distinct Subsequences

    Given a string S and a string T, count the number of distinct subsequences of T in S.

    A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ACE" is a subsequence of "ABCDE" while "AEC" is not).

    Here is an example:
    S = "rabbbit"T = "rabbit"

    Return 3.

    class Solution {
    public:
        int numDistinct(string S, string T) {
        }
    };

    如果不用DP,用带记忆的递归也能做,就是时间比较长,而且递归需要额外的栈空间。

    class Solution {
    public:
        int numDistinct(string S, string T) {
            if(T.length() == 0) return 1;
            if(S.length() == 0) return 0;
            
            rec = new int*[S.length()];
            for(int i = 0;i < S.length(); ++i){
                rec[i] = new int[T.length()];
                for(int j = 0;j < T.length(); ++j)
                    rec[i][j] = -1;
            }
            return numDistinctCore(S, T, 0 ,0);
        }
        
        int numDistinctCore(string S, string T, int p1, int p2) {
            if(p2 == T.length()) return 1;
            if((T.length()-p2) > (S.length()-p1)) return 0;
            if(rec[p1][p2] >= 0) return rec[p1][p2];
            int sum = 0;
            for(int i = p1;i < S.length(); ++i){
                if(S[i] == T[p2])
                    sum += numDistinctCore(S, T, i+1, p2+1);
            }
            rec[p1][p2] = sum;
            return sum;
        }
    private:
        int **rec;
    };

     AC时间 388ms。

    引入DP思想的话,我们依旧用rec[i][j] 表示 "S[i]结尾子串" 中包含 "T[j]结尾子串" 的 sequence 个数。

    因为S[i]子串 包含了S[i-1]子串,所以rec[i][j] 至少等于rec[i-1][j];同时,如果S[i] == T[j],那么还可以让 S[i]和T[j] 匹配,这种情况下,sequence个数就是rec[i-1][j-1]。

    rec[i][j] = rec[i-1][j] + (S[i] == T[j] ? rec[i-1][j-1] : 0)

    代码:

    class Solution {
    public:
        int numDistinct(string S, string T) {
            int slen = S.length(), tlen = T.length();
            if(slen < tlen) return 0;
            int **rec = new int*[slen+1];
            int i, j;
            for(i = 0; i <= slen; ++i){
                rec[i] = new int[tlen+1];
                for(j = 0; j <= tlen; ++j){
                    rec[i][j] = 0;
                }
            }
            for(i = 0; i <= slen; rec[i++][0] = 1);
                    
            for(i = 1; i <= slen; ++i){
                for(j = 1; j <= tlen; ++j){
                    rec[i][j] = (rec[i-1][j] + (S[i-1] == T[j-1] ? rec[i-1][j-1] : 0));
                }
            }
            
            return rec[slen][tlen];
        }
    };

     AC时间 52ms。大幅提高。

    上面的解法用到了二维数组。后来搜到了小磊哥关于这道题的解。让 j 从T末尾遍历,这样rec[i][j] 要么依旧等于 rec[i-1][j],也就是不变,要么加上 rec[i-1][j-1],因为j是从末尾遍历到前面,因此  rec[i-1][j-1] 不会被覆盖。这样做,省去了二维数组,直接一维数组搞定。用match[] 表示 T[j]结尾的子串 的sequence个数。

    代码:

    class Solution {
    public:
        int numDistinct(string S, string T) {
            if(S.size() < T.size()) return 0;
            int match[T.size()+1];
            int i, j;
            for(match[0] = 1, i = 0; i < T.size(); match[++i] = 0);
            for(i = 1; i <= S.size(); ++i)
                for(j = T.size(); j >= 1; --j)
                    if(S[i-1] == T[j-1])
                        match[j] += match[j-1];
            return match[T.size()];
        }
    };

    这里也用到了上一篇文章中利用从后往前遍历避免值被覆盖的思想。

    16ms AC,只能说,碉堡了。。

  • 相关阅读:
    【nodejs原理&源码杂记(8)】Timer模块与基于二叉堆的定时器
    【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick
    【nodejs原理&源码赏析(6)】深度剖析cluster模块源码与node.js多进程(下)
    【nodejs原理&源码赏析(5)】net模块与通讯的实现
    【nodejs原理&源码赏析(4)】深度剖析cluster模块源码与node.js多进程(上)
    工作一年多的感慨与总结(二)
    工作一年多的感慨与总结(一)
    动手实践Mybatis插件
    MySQL存储引擎
    Tomcat类加载架构
  • 原文地址:https://www.cnblogs.com/felixfang/p/3875141.html
Copyright © 2020-2023  润新知