阿里巴巴笔试—— 一道动规引发的思考

题目1:最长公共连续子序列

1、给定一个query和一个text,均由小写字母组成。要求在text中找出以同样的顺序连续出现在query中的最长连续字母序列的长度。例如, query为“acbac”,text为“acaccbabb”,那么text中的“cba”为最长的连续出现在query中的字母序列,因此,返回结果应该为其长度3。请注意程序效率。

     这道题其实就是最长公共连续子序列问题。可以直接用动规来实现。

    i=j   dp[i][j]=dp[i-1][j-1]+1

    i!=j  dp[i][j]=0;

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int getLCS(string &s1, string &s2)
{
     int len1 = s1.length();
     int len2 = s2.length();

    vector<vector< int>> matrix( 2, vector< int>((len2 +  1),  0));
     int MaxValue =  0;
     int flag =  1;
     int tmp =  0;
     /*
    思路是:
    利用两个数组来求解滚粗的方式
    */

     for( int i =  1; i <= len1; i++)
    {
         for( int j =  1; j <= len2; j++)
        {

             if(s1[i -  1] == s2[j -  1])
            {
                matrix[flag][j] = matrix[ 1 - flag][j -  1] +  1;

                 if(MaxValue < matrix[flag][j])
                {
                    MaxValue = matrix[flag][j];
                }
            }
             else
            {
                matrix[flag][j] =  0;
            }

        }
        flag =  1 - flag;
    }

     return MaxValue;

}
    下面采用两个变量和一个数组进行滚粗

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

int getLCS2(string &s1, string &s2)
{
     int len1 = s1.length();
     int len2 = s2.length();

    vector< int> matrix(len2 +  10);
     int MaxValue =  0;
     int flag =  1;
     int tmp =  0;
     int old =  0;
     /*
    思路是:用一维数组解决
    */

     for( int i =  1; i <= len1; i++)
    {
        old =  0;
         for( int j =  1; j <= len2; j++)
        {
            tmp = matrix[j];
             if(s1[i -  1] == s2[j -  1])
            {
                matrix[j] = old +  1;

                 if(MaxValue < matrix[j])
                {
                    MaxValue = matrix[j];
                }
            }
             else
            {
                matrix[j] =  0;
            }

            old = tmp;

        }
    }
     return MaxValue;

}


题目2:最长公共子序列

    就是所谓的最长公共子序列(LCS)问题。(不要求连续出现的公共子序列)下面给出两种解法,一种利用动规数组,另外一种使用滚粗数组来完成。因为只要求长度,所以可以利用滚粗数组可以实现。

    应用公式如下:

   i=j    dp[i][j]=dp[i-1][j-1]+1    

   i!=j     dp[i][j]=max(dp[i-1][j],dp[i][j-1])


下面是动规数组,直接粗暴,算法复杂度O(n2),空间复杂度O(n2).

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int  LCS(string &query, string &text)
{
     int len1 = query.size();
     int len2 = text.size();
    vector<vector< int>> vec(len1 +  1, vector< int>(len2 +  10));


     for( int i =  1; i <= len1; i++)
    {
         for( int j =  1; j <= len2; j++)
        {
             if(query[i -  1] == text[j -  1])
            {
                vec[i][j] = vec[i -  1][j -  1] +  1;
            }
             else
            {
                vec[i][j] = vec[i][j -  1] > vec[i -  1][j] ? vec[i][j -  1] : vec[i -  1][j];
            }
        }
    }
     // PrintLCS(vec,query,text,len1,len2);
     return vec[len1][len2];

}
    由于我们只需要计算长度,所以我们可以采用两个数组来实现动规数组,利用滚粗的方式。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
进一步优化空间 假如只求长度,我们只需要最后一行的结果,因此我们只需要两行的数组。滚粗数组
int LCS2(string &query, string &text)
{
     int len1 = query.size();
     int len2 = text.size();
    vector< vector< int> > vec( 2, vector< int>(len2 +  10));

     int flag =  1;

     for( int i =  1; i <= len1; i++)
    {
         for( int j =  1; j <= len2; j++)
        {
             if(query[i -  1] == text[j -  1])
            {
                vec[flag][j] = vec[ 1 - flag][j -  1] +  1;
            }
             else
            {
                vec[flag][j] = vec[flag][j -  1] >= vec[ 1 - flag][j] ? vec[flag][j -  1] : vec[ 1 - flag][j];
            }
        }
        flag =  1 - flag;
    }
     return vec[ 1 - flag][len2];
}
3、滚粗数组的进一步优化,空间复杂度为O(n),利用两个整数分别记录上方数组,和斜上方的元素

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//再进一步优化,采用一个整数来记录上方的元素
int getLcsLength(string &query, string &text)
{
     int len1 = query.size();
     int len2 = text.size();
    vector< int > dp(len2 +  10);
     int old =  0;
     int tmp =  0;
     for( int i =  1; i <= len1; i++)
    {
        old =  0;
         for( int j =  1; j <= len2; j++)
        {
            tmp = dp[j];
             if(query[i -  1] == text[j -  1])
            {
                dp[j] = old +  1;
            }
             else
            {
                 if(dp[j] < dp[j -  1])  dp[j] = dp[j -  1];
            }
            old = tmp;

        }
    }
     return dp[len2];

}
    

4、我们有时候需要把最长公共子序列打印出来,我们采用递归的方式进行打印。具体实现如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//根据动规矩阵 和递归打印最长公共字符串
void  PrintLCS(vector<vector< int>> &vec, string &query, string &text,  int i,  int j)
{

     if(i ==  0 || j ==  0)
         return ;
     if(query[i -  1] == text[j -  1])
    {
        PrintLCS(vec, query, text, i -  1, j -  1);  //从后面开始递归回状态
        cout << query[i -  1];
    }
     else  if(vec[i -  1][j] >= vec[i][j -  1])  //
    {
        PrintLCS(vec, query, text, i -  1, j);
    }
     else
    {
        PrintLCS(vec, query, text, i, j -  1);
    }
}
      4、上面的算法只能把一个可能性打印出来,但是有时候需要把所有可能的公共子序列都打印出来,此时,我们需要使用下面的算法来实现。

      这个时候我们需要新建一个辅助数组,记录公共子序列的走向。我们采用递归回溯的方法来实现搜索所有可能的路径。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//下面我们来实现把所有的最长公共子序列都打印出来试试
vector< char> result;

void Display_Lcs( int i,  int j, string &x, vector<vector< int> > &mvec,  int current_len,  int lcs_max_len)
{
     if(i ==  0 || j ==  0)
    {
         if(result.size() != lcs_max_len)
        {
            result.clear();
             return;
        }

         for( int s =  0; s < lcs_max_len; s++)
        {
            cout << result[lcs_max_len - s -  1];
        }
        cout << endl;
        result.clear();
         return;
    }
     if(mvec[i][j] ==  1)
    {
        current_len--;
        result.push_back(x[i -  1]);
        Display_Lcs(i -  1, j -  1, x, mvec, current_len -  1, lcs_max_len);
    }
     else
    {
         if(mvec[i][j] ==  2)
        {
            Display_Lcs(i -  1, j, x, mvec, current_len -  1, lcs_max_len);
        }
         else
        {
             if(mvec[i][j] ==  3)
            {
                Display_Lcs(i, j -  1, x, mvec, current_len, lcs_max_len);
            }
             else
            {
                Display_Lcs(i, j -  1, x, mvec, current_len, lcs_max_len);
                Display_Lcs(i -  1, j, x, mvec, current_len -  1, lcs_max_len);
            }
        }
    }
}

int  LCS4(string &query, string &text)
{
     int len1 = query.size();
     int len2 = text.size();
    vector<vector< int>> vec(len1 +  1, vector< int>(len2 +  10));
    vector<vector< int> >mvec(len1 +  1, vector< int>(len2 +  10));

     for( int i =  1; i <= len1; i++)
    {
         for( int j =  1; j <= len2; j++)
        {
             if(query[i -  1] == text[j -  1])
            {
                vec[i][j] = vec[i -  1][j -  1] +  1;
                mvec[i][j] =  1;

            }
             else  if(vec[i -  1][j] > vec[i][j -  1])
            {
                vec[i][j] = vec[i -  1][j];
                mvec[i][j] =  2;
            }
             else  if(vec[i -  1][j] < vec[i][j -  1])
            {
                vec[i][j] = vec[i][j -  1];
                mvec[i][j] =  3;
            }
             else
            {
                vec[i][j] = vec[i][j -  1];
                mvec[i][j] =  4;
            }
        }
         int m;
    }

     // PrintLCS(vec,query,text,len1,len2);
    Display_Lcs( len1, len2, query, mvec, vec[len1][len2], vec[len1][len2] );
     return vec[len1][len2];

}

 题目3:最长递增子序列

  最长递增子序列LIS(Longest Increasing Subsequence):
给定一个长度为N的数组,找出一个最长的单调自增子序列(不一定连续,但是顺序不能乱)。例如:给定一个长度为6的数组A{5, 6, 7, 1, 2, 8},则其最长的单调递增子序列为{5,6,7,8},长度为4。

分析:其实此LIS问题可以转换成最长公子序列问题,为什么呢?

原数组为A {5, 6, 7, 1, 2, 8}
排序后:A‘{1, 2, 5, 6, 7, 8}
因为,原数组A的子序列顺序保持不变,而且排序后A‘本身就是递增的,这样,就保证了两序列的最长公共子序列的递增特性。如此,若想求数组A的最长递增子序列,其实就是求数组A与它的排序数组A‘的最长公共子序列。


此外,本题也可以使用动态规划来求解,读者可以继续思考。


下面我们直接用动态规划的思想来解决:

      设长度为N的数组为{a0,a1, a2, ...an-1),则假定以aj结尾的数组序列的最长递增子序列长度为L(j),
则L(j)={ max(L(i))+1, i<j且a[i]<a[j] }。也就是说,我们需要遍历在j之前的所有位置i(从0到j-1),
找出满足条件a[i]<a[j]的L(i),求出max(L(i))+1即为L(j)的值。最后,我们遍历所有的L(j)(从0到N-1),
找出最大值即为最大递增子序列。时间复杂度为O(N^2)。
例如给定的数组为{5,6,7,1,2,8},则L(0)=1, L(1)=2, L(2)=3, L(3)=1, L(4)=2, L(5)=4。
所以该数组最长递增子序列长度为4,序列为{5,6,7,8}。

      这个解决的思想就是用到了替换的思想,每次只保留最后末尾的值来显示最长递增。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int getLIS(vector< int> &s)
{
     int len = s.size();
    vector< int> longest(len,  1);
     int Max =  1;
     for( int i =  1; i < len; i++)
    {
         for( int j =  0; j < i; j++)
        {
             if(s[j] < s[i] && longest[i] < longest[j] +  1)
            {
                longest[i] = longest[j] +  1;
                 if(Max < longest[i])
                {
                    Max = longest[i];
                }
            }
        }
    }

     return Max;
}
     解法二:

     这种解法用得比较巧妙。用到了二叉搜索树的概念。

基本思路:
新建一个O(N) 的辅助数组
需要遍历所有的dp[0]~dp[i-1]。而这也是优化的关键点,即如何减少每次对前面i项的遍历。
试想一下,如果某个val[j]<val[i],使得dp[i]更新为dp[j]+1,那么所有小于dp[j]的项其实无需再考虑。
对于长度相同的项,只记录下末尾数最小的项,因为如果这一项都无法满足小于val[i],其他项就更不可能满足这个条件。
根据第二种方法,我们只需要建立一个len数组,其中len[k]表示当前长度为k的递增子序列最小的末尾数是len[k]。
考虑第i项时,只需二分len数组已存在的下标范围,找到满足len[k]<val[i]且k最大的位置,并用val[i]更新len[k+1],
如果len[k+1]在更新前没赋值过,就将len数组的最大下标增一,最后的结果就是len数组的最大下标。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int BiSearch(vector< int > &Bitree,  int len,  int value)
{
     int left =  0, right = len -  1;
     int mid;
     while(left <= right)
    {
        mid = left + (right - left) /  2;
         if(Bitree[mid] > value)
            right = mid -  1;
         else  if(Bitree[mid] < value)
            left = mid +  1;
         else
             return mid;
    }
     return left;
}

int searchLIS(vector< int> &arr)
{
     int len =  1;
     int size = arr.size();
    vector< int> Bitree(size,  0);
    Bitree[ 0] = arr[ 0];
     int pos =  0;
     for( int i =  1; i < size; ++i)
    {
         if(arr[i] > Bitree[len -  1])  //如果大于Bitree中最大的元素,则直接插入到B数组尾
        {
            Bitree[len] = arr[i];
            ++len;
        }
         else
        {
            pos = BiSearch(Bitree, len, arr[i]);  //二分查找需要插入的位置
            Bitree[pos] = arr[i];
        }
    }
     return len;
}

 题目三:最长回文子串

    这道题就是求一个字符串里面的最长的一个回文子串。这里并没有用到动规的方法。

而是直接采用搜索的方法。分别对长度为奇数和长度为偶数的回文子串进行搜索。

搜索到最长的返回。

代码如下所示:     

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int MaxPromelineSubSuqueue(string &str)
{
     int len = str.length();

     int maxlength =  0;
     int start =  0;
     //查找长度为奇数的最长回文子串
     for( int i =  1; i < len; i++)
    {
         int j = i -  1, k = i +  1;
         while(j >=  0 && k < len && str.at(j) == str.at(k))
        {

             if(k - j +  1 > maxlength)
            {
                maxlength = k - j +  1;
                start = j;
            }
            j--;
            k++;


        }
    }
     //查找长度为偶数的最长回文子串
     for( int i =  0; i < len; i++)
    {
         int j = i;
         int k = i +  1;
         while(j >=  0 && k < len && str.at(j) == str.at(k))
        {
             if(k - j +  1 > maxlength)
            {
                maxlength = k - j +  1;
                start = j;
            }
            j--;
            k++;
        }
    }
     if(maxlength >  0)
    {

        string s = str.substr(start, maxlength);
        cout << s << endl;
    }

     return maxlength;

}


题目四:最长不重复子串

    根据题目的意思是:从一个字符串中找到一个连续子串,该子串任何两个字符不能相同,求子串的最大长度并输出最长不重复子串。

例如输入abcbef,输出cbef

这道题有点hash表的味道。新建一个位数256的数组,对字符进行映射。然后逐个去找最长的不重复的子串。

基本思路如下:

O(N)的算法,具体思路如下:
以abcbef这个串为例,用一个数组pos记录每个元素曾出现的下标,初始化为-1。从s[0]开始,依次考察每个字符,例如pos['a'] == -1,说明a还未出现过,令pos['a'] = 0,视为将‘a’加入当前串,同时长度+1,同理pos['b'] = 1,pos['c'] = 2,考察s[3],pos['b'] != -1,说明'b'在前面已经出现过了,此时可得到一个不重复串"abc",刷新当前的最大长度,然后更新pos['b']及起始串位置。
过程如下:
1、建一个256个单元的数组,每一个单元代表一个字符,数组中保存上次该字符出现的位置;
2、依次读入字符串,同时维护数组的值;
3、如果遇到冲突了,考察当前起始串位置到冲突字符的长度,如果大于当前最大长度,
则更新当前最大长度并维护冲突字符的位置,更新起始串位置,继续第二步。


具体代码如下所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

int GetMaxSubStr(string &str )
{
     int hash[ 256];  //hash记录每个字符的出现位置
     for ( int i =  0; i <  256; i++) //初始化
    {
        hash[i] = - 1;
    }
     int CurrentStart =  0, CurrentLength =  0, MaxStart =  0, MaxEnd =  0;
     int strLen = str.length();
     for( int i =  0; i < strLen; i++)
    {
         if(CurrentStart > hash[str[i]])  //如果没有重复
        {
            hash[str[i]] = i;
        }
         else
        {
            CurrentLength = i - CurrentStart;  //当前长度
             if(CurrentLength > MaxEnd - MaxStart)  //如果当前长度最长
            {
                MaxEnd = i;
                MaxStart = CurrentStart;
            }
            CurrentStart = hash[str[i]] +  1//更新当前最长的起点
            hash[str[i]] = i;  //更新字符出现的位置
        }
    }
     if (MaxEnd ==  0) //没有重复字符,返回源串
    {
        cout << str << endl;
         return strLen;
    }
    CurrentLength = strLen - CurrentStart;  //当前长度
     if(CurrentLength > MaxEnd - MaxStart) //如果当前长度最长
    {
        MaxEnd = strLen;
        MaxStart = CurrentStart;
    }
     int MaxLength = MaxEnd - MaxStart;
    string s = str.substr(MaxStart, MaxLength);
    cout << s << endl;
     return MaxLength;
}

题目五:最大子序列和

     这道题即不说,已经见过很多次了。基本思想是:抛弃没有贡献的前段,重新开始。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int MaxSumSubSequeue(vector< int> &arr)
{
     int len = arr.size();
     int sum =  0;
     int Max =  0;
     for( int i =  0; i < len; i++)
    {
         if(sum + arr[i] >  0)
        {
            sum += arr[i];
        }
         else
        {
            sum =  0;
        }

         if(sum > Max)
        {
            Max = sum;
        }
    }
     return Max;
}

题目六:字符串编辑距离

      这道题是动规的一个非常经典的题目,简直是屡见不鲜了。掌握动规的基本思想即可解决这个问题。

基本解法如下:

这是一个经典的动规问题
以str1c和str2d,从最后一个字符串来看


如果c=d则有 dp[i][j]=dp[i-1][j-1];


如果是
插入  dp[i][j]=dp[i][j-1]+1
删除  dp[i][j]=dp[i-1][j]+1
替换  dp[i][j]=dp[i-1][j-1]+1


代码如下所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
int EditDistance(string &s1, string &s2)
{
     int len1 = s1.length();
     int len2 = s2.length();

    vector<vector< int>> dp(len1 +  1, vector< int>(len2 +  10));
     for( int i =  1; i <= len1; ++i)
    {
        dp[i][ 0] = i;
    }
     for( int i =  1; i <= len2; ++i)
    {
        dp[ 0][i] = i;
    }

     for( int i =  1; i <= len1; i++)
    {
         for( int j =  1; j <= len2; j++)
        {
             if(s1[i -  1] == s2[j -  1])
            {
                dp[i][j] = dp[i -  1][j -  1];
            }
             else
            {
                 int mn = min(dp[i][j -  1], dp[i -  1][j]);
                dp[i][j] = min(dp[i -  1][j -  1], mn) +  1;
            }
        }
    }

     return dp[len1][len2];

}
/*
利用滚粗数组实现
*/

int EditDistance2(string &s1, string &s2)
{

     if(s1.length() > s2.length())
         return EditDistance2(s2, s1);
     int len1 = s1.length();
     int len2 = s2.length();
    vector< int> dp(len2 +  10);
     int upperleft =  0;
     int old =  0;

     for( int i =  1; i <= len2; i++)
    {
        dp[i] = i;
    }

     for( int i =  1; i <= len1; i++)
    {
        old = i;
         for( int j =  1; j <= len2; j++)
        {
            upperleft = dp[j];

             if(s1[i -  1] == s2[j -  1])
            {
                dp[j] = old;
            }
             else
            {
                dp[j] = min(dp[j], min(dp[j -  1], old)) +  1;
            }

            old = upperleft;
        }
    }

     return dp[len2];
}


题目七:Word Break &&Word Break II

       这个题目的意思是说,给你一个字符串,和一个字典。看看能不能用字典的元素组成这个字符串。题目II需要把所有可能都找出来。

     我们先来解决题目I:

     对于这道题我们首先想到的是用深度搜索来解决这个问题,意思就是说,用for循环和递归,一个一个的查找有没有合适的分割,有合适的分割就进行递归查找其他的部分是否由合适的分割。但是这种深度搜索的复杂度太高了。

     然后我们考虑的是用动态规划的方法来实现。采用动规的方法,需要总结出这个动规的初始状态和动规的转移方程。

    设状态为f(i) ,表示s[0,i]是否可以分词(二维状态,我们采用bool型来表示)。显而易见的一个规律是,0到i直接,存在任何一个s[0,j]为true,并且s[i,j]属于字典,则为正,假如s[0,j]和s[i,j]属于字典这两个任何一个为false ,则为false。因此我们有:

    f(i)=any_of(f(j)&&s[j+1,i]属于dict),0<=j<i。

下面是两种解法的代码:

 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

class Solution1
{
public:
     bool wordBreak(string s, unordered_set<string> &dict)
    {

         //这是一个蛮力解决的方法。 深度搜索
         int len = s.length();
         if(len <  1)
             return  true;
         bool flag =  false;
         for( int i =  1; i <= len; i++)
        {
            string tmpstr = s.substr( 0, i);
             if(dict.find(tmpstr) != dict.end())
            {
                 if(tmpstr.length() == len) return  true;
                flag = wordBreak(s.substr(i), dict);
            }

             if(flag ==  true)
            {
                 return  true;
            }
        }


    }

};

class Solution
{
public:
     bool wordBreak(string s, unordered_set<string> &dict)
    {

         int len = s.length();
        vector< bool> bvec(len +  1false);
        bvec[ 0] =  true;
         for( int i =  1; i <= len; i++)
        {
             for( int j =  0; j < i; j++)
            {
                 if(bvec[j] && (dict.find(s.substr(j, i - j)) != dict.end()))
                {
                    bvec[i] =  true;
                     break;
                }
            }

        }
         return bvec[len];
    }
};


     解决了问题I,下面我们来解决问题II。要把所有的可能性求出来,我们需要再问题I的基础上完成。基本思想是:定义一个二维的数组来,代替一维的数组,毕竟只有二维的数组才能指明方向。  然后用深度搜索的方法,以及回溯剪枝的形式把这些可能性给遍历回来,虽然复杂度有点高,不过要求所有可能性那也是没有办法的了。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Solution
{
public:
    vector<string> wordBreak(string s, unordered_set<string> &dict)
    {

         int len = s.size();

        vector<vector< bool>> prev(len +  1, vector< bool>(len,  false));
        vector< bool> dp(len +  1false);
        dp[ 0] =  true;
         for( int i =  1; i <= len; i++)
        {
             for( int j =  0; j < i; j++)
            {
                 if(dp[j] && dict.find(s.substr(j, i - j)) != dict.end())
                {
                    dp[i] =  true;
                    prev[i][j] =  true;
                }
            }
        }
        vector<string> path;
        vector<string> ret;
        gen_path(s, s.length(), prev, path, ret);
         return ret;

    }

     void gen_path( const string &s,  int cur,  const vector<vector< bool>> &prev, vector<string> &path, vector<string> &ret)
    {
         if(cur ==  0)
        {
            string tmp;
             int i = path.size() -  1;
             for(; i >  0; i--)
            {
                tmp += path[i] +  " ";

            }
            tmp += path[ 0];

            ret.push_back(tmp);
        }

         for( int i =  0; i < s.length(); i++)
        {
             if(prev[cur][i])
            {
                path.push_back(s.substr(i, cur - i));
                gen_path(s, i, prev, path, ret);
                path.pop_back();

            }
        }
    }
};

     问题八:Scarmble String

     这道题的题目有点难以理解。相当费劲啊。

Given a string s1, we may represent it as a binary tree by partitioning it to two non-empty 
substrings recursively.


Below is one possible representation of s1 = "great":


    great
   /    \
  gr    eat
 / \    /  \
g   r  e   at
           / \
          a   t
To scramble the string, we may choose any non-leaf node and swap its two children.


For example, if we choose the node "gr" and swap its two children, it produces a scrambled string "rgeat".


    rgeat
   /    \
  rg    eat
 / \    /  \
r   g  e   at
           / \
          a   t
We say that "rgeat" is a scrambled string of "great".


Similarly, if we continue to swap the children of nodes "eat" and "at", 
it produces a scrambled string "rgtae".


    rgtae
   /    \
  rg    tae
 / \    /  \
r   g  ta  e
       / \
      t   a
We say that "rgtae" is a scrambled string of "great".


Given two strings s1 and s2 of the same length, determine if s2 is a scrambled string of s1.

    其实这个题目简单的说就是:

s1和s2是scramble的话,那么必然存在一个在s1上的长度l1,将s1分成s11和s12两段,同样有s21和s22。那么要么s11和s21是scramble的并且s12和s22是scramble的;要么s11和s22是scramble的并且s12和s21是scramble的。

      基本思路:其实上面就说了基本思路了。这道题原本是可以用动规来做的。不过由于不是很理解动规的做法,无奈之下只能用深度搜索的方法来做了。

     深度搜索的复杂度比较高,因此我们需要采用很多的剪枝判断条件。也用到了一些优化的小技巧来实现。

具体代码如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution
{
public:
     bool isScramble(string s1, string s2)
    {

         int len1 = s1.length();
         int len2 = s2.length();
         if(s1 == s2)
             return  true;

         if(len1 != len2)
             return  false;

         int A[ 26] = { 0};
         for( int i =  0; i < len1; i++)
        {
            A[s1[i] -  'a']++;
        }
         for( int i =  0; i < len2; i++)
        {
            A[s2[i] -  'a']--;
        }

         for( int i =  0; i <  26; i++)
        {
             if(A[i] !=  0)
                 return  false;
        }
         bool result =  false;

         for( int i =  1 ; i < s1.size() ; ++i)
        {
            result = isScramble(s1.substr( 0, i) , s2.substr( 0, i)) && isScramble(s1.substr(i) , s2.substr(i));
             if(result)  return  true;
            result = isScramble(s1.substr( 0, i) , s2.substr(s1.size() - i, i)) && isScramble(s1.substr(i) , s2.substr( 0 , s1.size() - i));
             if(result)  return  true;
        }
         return  false;


    }
};


 题目九:Triage

题目
[
    [2]
   [3,4]
  [6,5,7]
 [4,1,8,3]
]
由上面往下面走,求路径最小的。

这个一道经典的动规题目
首先求动规的转移方程
dp[i][j]=min(dp[i][j+1],dp[i][j+1])+1;

里面有个小技巧就是从倒数第二行开始计算。重复利用原来的数组。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution
{
public:
     int mininumTatal(vector<vector< int> > &triangle)
    {
         int len = triangle.size();
         for( int i = len -  2; i >=  0; i--)
        {
             for( int j =  0; j < triangle[i].size(); j++)
            {
                triangle[i][j] += min(triangle[i +  1][j], triangle[i +  1][j +  1]);
            }
        }

         return triangle[ 0][ 0];
    }
};


     题目十:Palindrome Partitioning II

这道题的意思是:给你一个字符串,用最小的刀切割字符串,使得字符串要么是回文字符串,要么是单个字符(没有回文的情况下)。

For example, given s = "aab",
Return 1 since the palindrome partitioning ["aa","b"] could be produced using 1 cut.

这道题我们第一眼看上是可以用深度搜索来解决问题的。

    我们当然是从整个字符串是否是回文开始判断,其实for循环加上递归和剪枝就可以完成了。

但是用深度搜索复杂度比较高,下面我们用动态规划来实现:

动规的基本套路就是:初始化状态+状态转移方程。

首先我们来看看思路,我们先假设所有的字符都需要切割,因此有最大的切割术,数组n值为-1,数组n-1值为0,这样一直下去。

然后来看看状态转移方程:

        我们还是需要一个辅助的数组dp[i][j],这个数组主要是用来判断回文用的,因为回文字符串有相应规律,其判断条件是[s[i]==s[j]&&(j-i>2||dp[i+1][j-1])],然后,用二级for循环进行计算。

     具体实现的代码如下:

深度搜索的代码如下所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
下面是一个剪枝回溯的方法:*/

bool isPalindrome(string &s,  int start,  int end)
{
     while(start < end)
    {
         if(s[start++] != s[end--])
             return  false;
    }
     return  true;
}
void DFS(string &s,  int start,  int depth,  int &min)
{
     if(start == s.size())
    {
         if(min > depth -  1)
            min = depth -  1;

         return ;
    }

     for( int i = s.size() -  1; i >= start; i--)
    {
         if(isPalindrome(s, start, i))
        {
            DFS(s, i +  1, depth +  1, min);
        }
    }

}
int minCut1(string s)
{
     int min = INT_MAX;
    DFS(s,  00, min);
     return min;
}
动态规划:

 

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int minCut(string s)
{
     int len = s.size();

    vector<vector< bool> >dp(len, vector< bool>(len,  false));
    vector< int> f(len +  10);
     for( int i =  0; i <= len; i++)
    {
        f[i] = len - i -  1;
    }

     for( int i = len -  1; i >=  0; i--)
    {
         for( int j = i; j < len; j++)
        {
             if(s[i] == s[j] && (j - i >  2 || dp[i +  1][j -  1]))
            {
                dp[i][j] =  true;
                f[i] = min(f[i], f[j] +  1);
            }
        }
    }
     return f[ 0];

}
   

      题目十一:Interleaving String

     这道题的意思是说,给你两个字符,然后有一个第三个字符串,问这个第三个字符是否有前面两个字符的唯一两个子序列呢。

这是一道典型动规题目。

首先我们判断两个字符串的长度之和等于第三个字符串。

然后我们利用动规的方法来实现实现解决:

我们先来看看初始化状态:

       f[i][0]=f[i-1][0]&&(s1[i-1]==s3[i-1]);

       f[0][i]=f[0][i-1]&&(s2[i-1]==s3[i-1]);

     我们从最后一个字符串来考虑问题

如果 s1[i-1]==s3[i+j-1]相同,前面的状态f[i-1][j]是true。
如果 s2[j-1]==s3[i+j-1]相同,前面的状态f[i][j-1]是true。
两个其中一个正确即可,实现状态转移。

代码实现如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution
{
public:
     bool isInterleave(string s1, string s2, string s3)
    {

         int len1 = s1.size();
         int len2 = s2.size();
         int len3 = s3.size();
         if(len3 != len1 + len2)
             return  false;

        vector<vector< bool>> f(len1 +  1, vector< bool>(len2 +  1true));

         //先求初始化状态
         for( int i =  1; i <= len1; i++)
        {
            f[i][ 0] = f[i -  1][ 0] && (s1[i -  1] == s3[i -  1]);
        }
         for( int i =  1; i <= len2; ++i)
        {
            f[ 0][i] = f[ 0][i -  1] && (s2[i -  1] == s3[i -  1]);

        }
         //根据动态规划的状态转移方程求解结果
         for( int i =  1; i <= len1; i++)
        {
             for( int j =  1; j <= len2; j++)
            {
                f[i][j] = (s1[i -  1] == s3[i + j -  1] && f[i -  1][j]) ||
                          (s2[j -  1] == s3[i + j -  1] && f[i][j -  1]);

            }
        }

         return f[len1][len2];

    }
};


      题目十二:

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.

      这道题就是说,T在S中有多少组合代替方式

这也是一道经典的动态规划的问题。

    还是根据动规的基本规律来搞,

首先是初始化状态  dp[i][0]=1;

然后我们用两个for循环来实现,如果S[i-1]==T[j-1]我们可以知道i,j的状态是有i-1,j-1或者i-1,j转换过来,所以对他们求和。

如果不等的话,直接去掉S的一位dp[i][j]=dp[i-1][j](即是忽略这一位。)

我们直接上代码吧

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution
{
public:
     int numDistinct(string S, string T)
    {

         int len1 = S.length();
         int len2 = T.length();

        vector<vector< int >> dp(len1 +  1, vector< int>(len2 +  10));
         //初始状态
         for( int i =  0; i <= len1; i++)
        {
            dp[i][ 0] =  1;
        }

         for( int i =  1; i <= len1; i++)
        {
             for( int j =  1; j <= len2; j++)
            {
                 if(S[i -  1] == T[j -  1])
                {
                    dp[i][j] = dp[i -  1][j -  1] + dp[i -  1][j];
                }
                 else
                {
                    dp[i][j] = dp[i -  1][j];
                }
            }
        }

         return dp[len1][len2];
    }
};


     题目十三:

Best Time to Buy and Sell Stock III

 

    这个题目的意思是:
两次买卖,使得利润最大
我们可以这样考虑:
新建两个数组,一个数组表示从0到i天的最大利润,第二个数组表示第i天到n-1天的最大利润,
从左往右考虑,f1max=f[i]-Min ,但是这个要考虑之前最大值。
从右往左考虑,f2max=Max-f[i] ,但是要考虑之前的最大值。
根据上面两个数组,我们再扫描一下数组,求出f1+f2的数组的最大值。

代码实现如下所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

class Solution
{
public:
     int maxProfit(vector< int> &prices)
    {

         int len = prices.size();
         if(len <  2)
             return  0;
        vector< int> f1(len,  0);
        vector< int> f2(len,  0);

         int MinPrices = prices[ 0];
         int MaxPrices = prices[len -  1];

         for( int i =  1; i < len; i++)
        {
             if(prices[i] < MinPrices)
            {
                MinPrices = prices[i];
            }
            f1[i] = max(prices[i] - MinPrices, f1[i -  1]);
        }

         for( int i = len -  2; i >=  0; i--)
        {
             if(prices[i] > MaxPrices)
            {
                MaxPrices = prices[i];
            }
            f2[i] = max(MaxPrices - prices[i], f2[i +  1]);
        }
         int Max = f1[ 0] + f2[ 0];
         for( int i =  1; i < len; i++)
        {
             if(Max < f1[i] + f2[i])
            {
                Max = f1[i] + f2[i];
            }
        }
         return Max;
    }
};



     题目十四:

Decode Ways

 

     

A message containing letters from A-Z is being encoded to numbers using the following mapping:

'A' -> 1
'B' -> 2
...
'Z' -> 26

Given an encoded message containing digits, determine the total number of ways to decode it.

For example,
Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).

The number of ways decoding "12" is 2.

     这道题其实就是变相的斐波拉契数列。只不过这道题是有约束条件的。很明显每两个数可以分别拆成两种方式,一种是分别一个一个单,以及两位数。特别要注意的是'0',以及大于'7'的数字。这种动规的题目,需要从后面

代码如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution
{
public:
     int numDecodings(string s)
    {

         if(s.length() ==  0)
             return  0;
         int len = s.length();
         if(len <  1)
             return  0;
        vector< int> arr(len,  0);
         int i = len -  1;
        arr[i] = (s.at(i) ==  '0') ?  0 :  1//从后面开始算起
        i--;  //从倒数第二位开始算起
         while(i >=  0)
        {
             if(s.at(i) ==  '0'//下一位是10
            {
                arr[i] =  0;
            }
             else  if(s.at(i) ==  '1' || s.at(i) ==  '2' && s.at(i +  1) <=  '6'//可以分解成两种可能
            {
                 if(i == len -  2)
                {
                    arr[i] = arr[i +  1] +  1;
                }
                 else
                {
                    arr[i] = arr[i +  1] + arr[i +  2];
                }
            }
             else  //只有一种可能分解
            {
                arr[i] = arr[i +  1];
            }
            i--;
        }
         return arr[ 0];

    }
};


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值