动态规划DP问题分类和经典题型

解题关键:

理解结构特征,抽象出状态,写成状态转移方程。

动态规划理念:

1.最优化原理
   1951年美国数学家R.Bellman等人,根据一类多阶段问题的特点,把多阶段决策问题变换为一系列互相联系的单阶段问题,然后逐个加以解决。一些静态模型,只要人为地引进“时间”因素,分成时段,就可以转化成多阶段的动态模型,用动态规划方法去处理。与此同时,他提出了解决这类问题的“最优化原理”(Principle of optimality):
    “一个过程的最优决策具有这样的性质:即无论其初始状态和初始决策如何,其今后诸策略对以第一个决策所形成的状态作为初始状态的过程而言,必须构成最优策略”。简言之,一个最优策略的子策略,对于它的初态和终态而言也必是最优的。
    这个“最优化原理”如果用数学化一点的语言来描述的话,就是:假设为了解决某一优化问题,需要依次作出n个决策D1,D2,…,Dn,如若这个决策序列是最优的,对于任何一个整数k,1 < k < n,不论前面k个决策是怎样的,以后的最优决策只取决于由前面决策所确定的当前状态,即以后的决策Dk+1,Dk+2,…,Dn也是最优的。
    最优化原理是动态规划的基础。任何一个问题,如果失去了这个最优化原理的支持,就不可能用动态规划方法计算。能采用动态规划求解的问题都需要满足一定的条件: 
    (1) 问题中的状态必须满足最优化原理
    (2) 问题中的状态必须满足无后效性
    所谓的无后效性是指:“下一时刻的状态只与当前状态有关,而和当前状态之前的状态无关,当前的状态是对以往决策的总结”。

2.问题求解模式 
    动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

   初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
     图1 动态规划决策过程示意图

    (1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
    (2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
    (3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。
    (4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

3.实现
    动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。使用动态规划求解问题,最重要的就是确定动态规划三要素:问题的阶段,每个阶段的状态以及从前一个阶段转化到后一个阶段之间的递推关系。递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。下面分别以求解最大化投资回报问题和最长公共子序列问题为例阐述用动态规划算法求解问题的一般思路。


动态规划经典例题


1.三角形找一条从顶到底的最小路径

分析

设状态为 f (i; j ),表示从从位置 (i; j ) 出发,路径的最小和,则状态转移方程为

f(i,j)=min{f(i+1,j),f(i+1,j+1)}+(i,j)

 

2.最大子数组和

设状态为 f[j],表示以 S[j] 结尾的最大连续子序列和,状态转移方程如下:

f=max(f+A[i],A[i]);//对于数组里的一个整数,它只有两种 选择:1、加入之前的 SubArray2. 自己另起一个 SubArray

maxsum=max(maxsum,f);// 求字串中最大的

 

3.回文最小划分次数

对输入的字符串划分为一组回文字符串,最小的分割次数

所以要转换成一维 DP。如果每次,从 i 往右扫描,每找到一个回文就算一次 DP 的话,就可以

转换为 f(i)= 区间 [i, n-1] 之间最小的 cut 数,为字符串长度,则状态转移方程为

 

 

4.最佳时间买卖股票

设状态f(i)表示区间[0,i-1]上的最大利润,设置状态g(i),表示区间[i,n-1]上最大利润。

则最大利润为max{f(i)+g(i)};允许在一天内买进又卖出,相当于不交易,因为题目的规定是最多两次,而不是一定要两次


5. 判断字符串s3是否由s1,s2交叉存取组成

设状态 f[i][j],表示 s1[0,i]  s2[0,j],匹配 s3[0, i+j]。如果 s1 的最后一个字符等  s3 的最后一个字符,则

 f[i][j]=f[i-1][j]

如果 s2 的最后一个字符等于 s3 的最后一个字符,  

f[i][j]=f[i][j-1]

因此状态转移方程如下: 

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]);

 

6.给定一个矩形表格,求从顶到底的最小和

Minimum Path Sum

设状态为 f[i][j],表示从起点 (0; 0) 到达 (i; j ) 的最小路径和,则状态转移方程为:

f[i][j]=min(f[i-1][j], f[i][j-1])+grid[i][j]

 

7.使两个字符串相等,最小的编辑次数

Edit Distance

设状态为 f[i][j],表示 A[0,i]  B[0,j] 之间的最小编辑距离。设 A[0,i] 的形式是

str1cB[0,j] 的形式是 str2d

1. 如果 c==d,则 f[i][j]=f[i-1][j-1]

2. 如果 c!=d

(a) 如果将 c 替换成 d,则 f[i][j]=f[i-1][j-1]+1

(b) 如果在 c 后面添加一个 d,则 f[i][j]=f[i][j-1]+1

(c) 如果将 c 删除,则 f[i][j]=f[i-1][j]+1


8.给定一串数字,1对应A2对应B,26对应Z,求有多少种解码方式

 Decode Ways

和爬楼梯问题一样,

 f (n) 表示爬 n 阶楼梯的不同方法数,为了爬到第 n 阶楼梯,有两个选择:

• 从第 n-1 阶前进 1 步;

• 从第 n-2 阶前进 2 步;

因此,有 f (n) = f (n-1) + f (n-2) 这是一个斐波那契数列。

 

9. 不同的子序列Distinct Subsequences

给定2个字符串a, b,求ba中出现的次数。要求可以是不连续的,但是ba中的顺序必须和b以前的一致。 

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

Return 3.

 

类似于数字分解的题目。dp[i][j]表示:b的前j个字符在a的前i个字符中出现的次数。

 

如果S[i]==T[j],那么dp[i][j] = dp[i-1][j-1] + dp[i-1][j]

意思是:如果当前S[i]==T[j],那么当前这个字母即可以保留也可以抛弃,所以变换方法等于保留这个字母的变换方法加上不用这个字母的变换方法。
如果S[i]!=T[i],那么dp[i][j] = dp[i-1][j]

意思是如果当前字符不等,那么就只能抛弃当前这个字符

递归公式中用到的dp[0][0] = 1dp[i][0] = 0(把任意一个字符串变换为一个空串只有一个方法

 

10.单词分解Word Break

字符串是否可以分解为给定的单词

For example, given

s = "leetcode",

dict = ["leet", "code"].

dp[i]  表示源串的前i个字符可以满足分割,那么 dp[ j ] 满足分割的条件是存在使得 dp [k] && substr[k,j]在字典里。




真题:



1.Triangle

Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

For example, given the following triangle

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

Note:
Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.


分析
设 状态为 f (i; j ),表示从从位置 (i; j ) 出发,路径的最小和,则状态转移方程为
f(i,j)=min{f(i+1,j),f(i+1,j+1)}+(i,j)

代码
// LeetCode, Triangle
// 时间复杂度 O(n^2),空间复杂度 O(1)
class Solution {
public:
int minimumTotal (vector<vector<int>>& triangle) 
{
    for (int i = triangle.size() - 2; i >= 0; --i)
    {
        for (int j = 0; j < i + 1; ++j)
            triangle[i][j] += min(triangle[i + 1][j], triangle[i + 1][j + 1]); 
    }
    return triangle [0][0]; 
};

2.Maximum Subarray

Find the contiguous subarray within an array (containing at least one number) which has the largest sum.

For example, given the array [−2,1,−3,4,−1,2,1,−5,4],
the contiguous subarray [4,−1,2,1] has the largest sum = 6.


自己最初想法:
需找最大字串的起点和终点。不是特别好解

分析:
答案: 把原字符串分成很多不同的字串,然后求出字串中最大的。

把原字符串分成很多不同的字串,通过 max(f+A[i],A[i])就可以搞定,如果之前的对我没贡献,还不如另起一个字串

设状态为 f[j],表示以 S[j] 结尾的最大连续子序列和,状态转移方程如下:

f=max(f+A[i],A[i]);//对于数组里的一个整数,它只有两种 选择:1、加入之前的 SubArray;2. 自己另起一个 SubArray。

maxsum=max(maxsum,f);// 求字串中最大的

代码:

class Solution {

public:

    int maxSubArray(int A[], int n) {

        if(0==n) return 0;

        int f=0;//f[j],表示以 A[j] 结尾的最大连续子序列和

        int maxsum=A[0];

        for(int i=0;i<n;++i)

        {

         

            f=max(f+A[i],A[i]);//是否需要另起一个字串,如果之前的对我没贡献,还不如另起一个字串。

            maxsum=max(maxsum,f); //字串中最大的

        }

        return maxsum;

    }

};


3.Palindrome Partitioning II

Given a string s, partition s such that every substring of the partition is a palindrome.

Return the minimum cuts needed for a palindrome partitioning of s.

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

题意分析: 对输入的字符串划分为一组回文字符串,最小的分割次数


分析

定义状态 f(i,j) 表示区间 [i,j] 之间最小的 cut 数,则状态转移方程为

这是一个二维函数,实际写代码比较麻烦。

所以要转换成一维 DP。如果每次,从 i 往右扫描,每找到一个回文就算一次 DP 的话,就可以

转换为 f(i)= 区间 [i, n-1] 之间最小的 cut 数,n 为字符串长度,则状态转移方程为


一个问题出现了,就是如何判断 [i,j] 是否是回文?每次都从 i 到 j 比较一遍?太浪费了,这 里也是一个 DP 问题。 

定义状态 P[i][j] = true if [i,j] 为回文,那么

P[i][j] = str[i] == str[j] && P[i+1][j-1]


代码

// LeetCode, Palindrome Partitioning II

// 时间复杂度 O(n^2),空间复杂度 O(n^2)

class Solution {

public:

  int minCut(string s)

 {

    const int n = s.size();

    int f[n+1];

    bool p[n][n];

    fill_n(&p[0][0], n * n, false);

//the worst case is cutting by each char

    for (int i = 0; i <= n; i++)

            f[i] = n - 1 - i; // 最后一个 f[n]=-1

    for (int i = n - 1; i >= 0; i--)

    {

        for (int j = i; j < n; j++)

         {

            if (s[i] == s[j] && (j - i < 2 || p[i + 1][j - 1])) 

            { 

                    p[i][j] = true;

                    f[i] = min(f[i], f[j + 1] + 1);

           }

        }

    }

    return f[0];

}

}


4.Maximal Rectangle

描述

Given a 2D binary matrix filled with 0’s and 1’s, find the largest rectangle containing all ones and return

its area.

题目就是给一个矩阵,找一个全是一的最大子矩阵。


5.Best Time to Buy and Sell Stock III

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete at most two transactions.

Note:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).


分析:

设状态f(i)表示区间[0,i-1]上的最大利润,设置状态g(i),表示区间[i,n-1]上最大利润。

则最大利润为max{f(i)+g(i)};允许在一天内买进又卖出,相当于不交易,因为题目的规定是最多两次,而不是一定要两次。

代码

// LeetCode, Best Time to Buy and Sell Stock III

// 时间复杂度 O(n),空间复杂度 O(n)

class Solution {

public:

int maxProfit(vector<int>& prices)

 {

    if (prices.size() < 2) return 0;

    const int n = prices.size();

    vector<int> f(n, 0);

    vector<int> g(n, 0);

    for (int i = 1, valley = prices[0]; i < n; ++i) {

        valley = min(valley, prices[i]);

        f[i] = max(f[i - 1], prices[i] - valley);

    }

    for (int i = n - 2, peak = prices[n - 1]; i >= 0; --i) {

        peak = max(peak, prices[i]);

        g[i] = max(g[i], peak - prices[i]);

    }

    int max_profit = 0;

    for (int i = 0; i < n; ++i)

        max_profit = max(max_profit, f[i] + g[i]);

    return max_profit;

    }

};


6.Interleaving String

Given s1, s2, s3, find whether s3 is formed by the interleaving of s1 and s2.

For example,
Given:
s1 = "aabcc",
s2 = "dbbca",

When s3 = "aadbbcbcac", return true.
When s3 = "aadbbbaccc", return false.

分析:判断字符串s3是否由s1,s2交叉存取组成


设状态 f[i][j],表示 s1[0,i] 和 s2[0,j],匹配 s3[0, i+j]。如果 s1 的最后一个字符等 于 s3 的最后一个字符,则

 f[i][j]=f[i-1][j];

如果 s2 的最后一个字符等于 s3 的最后一个字符, 则 

f[i][j]=f[i][j-1]。

因此状态转移方程如下: 

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]);

1 class Solution {
 2 private:
 3     bool f[1000][1000];
 4 public:
 5     bool isInterleave(string s1, string s2, string s3) {
 6         // Start typing your C/C++ solution below
 7         // DO NOT write int main() function 
 8         if (s1.size() + s2.size() != s3.size())
 9             return false;
10             
11         f[0][0] = true;
12         for(int i = 1; i <= s1.size(); i++)
13             f[i][0] = f[i-1][0] && (s3[i-1] == s1[i-1]);
14             
15         for(int j = 1; j <= s2.size(); j++)
16             f[0][j] = f[0][j-1] && (s3[j-1] == s2[j-1]);
17             
18         for(int i = 1; i <= s1.size(); i++)
19             for(int j = 1; j <= s2.size(); j++)
20                 f[i][j] = (f[i][j-1] && s2[j-1] == s3[i+j-1]) || (f[i-1][j] && s1[i-1] == s3[i+j-1]);
21                 
22         return f[s1.size()][s2.size()];
23     }
24 };


动规 + 滚动数组

// LeetCode, Interleaving String

// 二维动规 + 滚动数组,时间复杂度 O(n^2),空间复杂度 O(n)

class Solution {

public:

bool isInterleave(string s1, string s2, string s3)

 {

    if (s1.length() + s2.length() != s3.length())

        return false;

    if (s1.length() < s2.length())

        return isInterleave(s2, s1, s3);

    vector<bool> f(s2.length() + 1, true);

    for (size_t i = 1; i <= s2.length(); ++i)

        f[i] = s2[i - 1] == s3[i - 1] && f[i - 1];

    for (size_t i = 1; i <= s1.length(); ++i)

     {

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

        for (size_t j = 1; j <= s2.length(); ++j)

            f[j] = (s1[i - 1] == s3[i + j - 1] && f[j]) || (s2[j - 1] == s3[i + j - 1] && f[j - 1]); 

    }

    return f[s2.length()];

}

};


7.Scramble 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.



8.Minimum Path Sum

描述

Given a m n grid filled with non-negative numbers, find a path from top left to bottom right which

minimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time

分析:

设状态为 f[i][j],表示从起点 (0; 0) 到达 (i; j ) 的最小路径和,则状态转移方程为:

f[i][j]=min(f[i-1][j], f[i][j-1])+grid[i][j]


代码:

备忘录法

// LeetCode, Minimum Path Sum

// 备忘录法

class Solution

 {

public:

int minPathSum(vector<vector<int> > &grid)

 {

    const int m = grid.size();

    const int n = grid[0].size();

    this->f = vector<vector<int> >(m, vector<int>(n, -1));

    return dfs(grid, m-1, n-1);

}

private:

vector<vector<int> > f; // 缓存

int dfs(const vector<vector<int> > &grid, int x, int y)

 {

    if (x < 0 || y < 0) return INT_MAX; // 越界,终止条件,注意,不是 0

    if (x == 0 && y == 0) return grid[0][0]; // 回到起点,收敛条件

    return min(getOrUpdate(grid, x - 1, y),

    getOrUpdate(grid, x, y - 1)) + grid[x][y];

}

int getOrUpdate(const vector<vector<int> > &grid, int x, int y) 

{

    if (x < 0 || y < 0) 

            return INT_MAX; // 越界,注意,不是 0

    if (f[x][y] >= 0) 

            return f[x][y];

    else

         return f[x][y] = dfs(grid, x, y);

}

};

动规

// LeetCode, Minimum Path Sum

// 二维动规

class Solution

{

public:

int minPathSum(vector<vector<int> > &grid)

 {

    if (grid.size() == 0) return 0;

    const int m = grid.size();

    const int n = grid[0].size();

    int f[m][n];

    f[0][0] = grid[0][0];

    for (int i = 1; i < m; i++)

     {

        f[i][0] = f[i - 1][0] + grid[i][0];

    }

    for (int i = 1; i < n; i++)

     {

        f[0][i] = f[0][i - 1] + grid[0][i];

    }

    for (int i = 1; i < m; i++) 

    {

        for (int j = 1; j < n; j++)

         {

            f[i][j] = min(f[i - 1][j], f[i][j - 1]) + grid[i][j];

        }

    }

   return f[m - 1][n - 1];

}

};

动规 + 滚动数组

// LeetCode, Minimum Path Sum

// 二维动规 + 滚动数组

class Solution {

public:

int minPathSum(vector<vector<int> > &grid)

 {

        const int m = grid.size();

        const int n = grid[0].size();

        int f[n];

        fill(f, f+n, INT_MAX); // 初始值是 INT_MAX,因为后面用了 min 函数。

        f[0] = 0;

        for (int i = 0; i < m; i++)

         {

            f[0] += grid[i][0];

            for (int j = 1; j < n; j++) 

            {

                    // 左边的 f[j],表示更新后的 f[j],与公式中的 f[i[[j] 对应

                    // 右边的 f[j],表示老的 f[j],与公式中的 f[i-1][j] 对应

                    f[j] = min(f[j - 1], f[j]) + grid[i][j];

               }

            }

    return f[n - 1];

}

};


9 Edit Distance

描述

Given two words word1 and word2, find the minimum number of steps required to convert word1 to

word2. (each operation is counted as 1 step.)

You have the following 3 operations permitted on a word:

• Insert a character

• Delete a character

• Replace a character


分析

设状态为 f[i][j],表示 A[0,i] 和 B[0,j] 之间的最小编辑距离。设 A[0,i] 的形式是

str1c,B[0,j] 的形式是 str2d,

1. 如果 c==d,则 f[i][j]=f[i-1][j-1];

2. 如果 c!=d,

(a) 如果将 c 替换成 d,则 f[i][j]=f[i-1][j-1]+1;

(b) 如果在 c 后面添加一个 d,则 f[i][j]=f[i][j-1]+1;

(c) 如果将 c 删除,则 f[i][j]=f[i-1][j]+1;

动规

// LeetCode, Edit Distance

// 二维动规,时间复杂度 O(n*m),空间复杂度 O(n*m)

class Solution {

public:

int minDistance(const string &word1, const string &word2) {

    const size_t n = word1.size();

    const size_t m = word2.size();

    // 长度为 n 的字符串,有 n+1 个隔板

    int f[n + 1][m + 1];

    for (size_t i = 0; i <= n; i++)

        f[i][0] = i;

    for (size_t j = 0; j <= m; j++)

        f[0][j] = j;

    for (size_t i = 1; i <= n; i++) {

        for (size_t j = 1; j <= m; j++) {

            if (word1[i - 1] == word2[j - 1])

                f[i][j] = f[i - 1][j - 1];

            else {

                int mn = min(f[i - 1][j], f[i][j - 1]);

                f[i][j] = 1 + min(f[i - 1][j - 1], mn);

            }

        }

}

return f[n][m];

}

};


动规 + 滚动数组

// LeetCode, Edit Distance

// 二维动规 + 滚动数组

// 时间复杂度 O(n*m),空间复杂度 O(n)

class Solution {

public:

int minDistance(const string &word1, const string &word2) {

    if (word1.length() < word2.length())

        return minDistance(word2, word1);

    int f[word2.length() + 1];

    int upper_left = 0; // 额外用一个变量记录 f[i-1][j-1]

    for (size_t i = 0; i <= word2.size(); ++i)

        f[i] = i;

    for (size_t i = 1; i <= word1.size(); ++i) {

        upper_left = f[0];

        f[0] = i;

        for (size_t j = 1; j <= word2.size(); ++j) {

            int upper = f[j];

            if (word1[i - 1] == word2[j - 1])

                f[j] = upper_left;

            else

                f[j] = 1 + min(upper_left, min(f[j], f[j - 1]));

            upper_left = upper;

    }

}

return f[word2.length()];

}

};


10 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


分析 :

和爬楼梯问题一样,

设 f (n) 表示爬 n 阶楼梯的不同方法数,为了爬到第 n 阶楼梯,有两个选择:

• 从第 n-1 阶前进 1 步;

• 从第 n-2 阶前进 2 步;

因此,有 f (n) = f (n-1) + f (n-2)。 这是一个斐波那契数列。


这里也一样,多一些约束条件而已。判断两个数字时,是否小于26

代码

// LeetCode, Decode Ways

// 动规,时间复杂度 O(n),空间复杂度 O(1)+滚动数组

class Solution {

public:

int numDecodings(const string &s) {

    if (s.empty() || s[0] == '0') return 0;

    int prev = 0;//f(0)=0

    int cur = 1;//f(1)=1

    // 长度为 n 的字符串,有 n+1 个阶梯

    for (size_t i = 1; i <= s.size(); ++i) {

        if (s[i-1] == '0')

             cur = 0;

        if (i < 2 || !(s[i - 2] == '1' || (s[i - 2] == '2' && s[i - 1] <= '6'))) 

            prev = 0;

        int tmp = cur;

        cur = prev + cur;//f(i)=f(i-2)+f(i-1)

        prev = tmp;

    }

    return cur;

}

};


11 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.

给定2个字符串a, b,求b在a中出现的次数。要求可以是不连续的,但是b在a中的顺序必须和b以前的一致。  

分析

类似于数字分解的题目。dp[i][j]表示:b的前j个字符在a的前i个字符中出现的次数。


如果a[i]==b[j],那么 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]。
意思是:如果当前a[i]==b[j],那么当前这个字母即 可以保留也可以抛弃,所以变换方法等于保留这个字母的变换方法加上不用这个字母的变换方法。
如果a[i]!=b[i],那么 dp[i][j] = dp[i-1][j],
意思是如果当前字符不等,那么就 只能抛弃当前这个字符

递归公式中用到的dp[0][0] = 1,dp[i][0] = 0(把任意一个字符串变换为一个空串只有一个方法


代码

// LeetCode, Distinct Subsequences

// 二维动规 + 滚动数组

// 时间复杂度 O(m*n),空间复杂度 O(n)

class Solution {

public:

int numDistinct(const string &S, const string &T) {

    vector<int> f(T.size() + 1);

    f[0] = 1;

    for (int i = 0; i < S.size(); ++i) {

        for (int j = T.size() - 1; j >= 0; --j) {

        f[j + 1] += S[i] == T[j] ? f[j] : 0;

        }

    }

    return f[T.size()];

}

};


12 Word Break

描述

Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated

sequence of one or more dictionary words.

For example, given

s = "leetcode",

dict = ["leet", "code"].

Return true because "leetcode" can be segmented as "leet code".


分析:

dp[i]  表示源串的前i个字符可以满足分割,那么 dp[ j ] 满足分割的条件是存在k 使得 dp [k] && substr[k,j]在字典里。
  1. class Solution {  
  2. public:  
  3.     bool wordBreak(string s, unordered_set<string> &dict) {  
  4.         // Note: The Solution object is instantiated only once and is reused by each test case.  
  5.         int n = (int)s.size();  
  6.         vector<bool> dp(n + 1, false);  
  7.         dp[0]=true;  
  8.         for(int i=1;i<=n;i++)  
  9.         {  
  10.             if(dp[i-1])  
  11.             {  
  12.                 int idx=i-1;  
  13.                 for(int j=idx;j<n;j++)  
  14.                 {  
  15.                     string cur=s.substr(idx,j-idx+1);  
  16.                     if(dict.count(cur)>0)  
  17.                         dp[j+1]=true;  
  18.                 }  
  19.             }  
  20.         }  
  21.         return dp[n];  
  22.     }  
  23. };  
13.Word Break II

Given a string s and a dictionary of words dict, add spaces in s to construct a sentence where each word is a valid dictionary word.

Return all such possible sentences.

For example, given
s = "catsanddog",
dict = ["cat", "cats", "and", "sand", "dog"].

A solution is ["cats and dog", "cat sand dog"].


跟第一题一样,就是要返回所有可能的切分, 做切分反应是用回溯,但是不剪枝肯定要超时。

这里用了一个math[i][j] 来表示 i--j 这一段是否可以切分,然后在dfs的时候利用看最后剩余的子串能否切分来剪枝


  1. class Solution {  
  2. public:  
  3.     vector<string> wordBreak(string s, unordered_set<string> &dict)   
  4.     {  
  5.         int n=s.length();  
  6.         vector<vector<bool> > match(n+1,vector<bool>(n+1,false));  
  7.         for(int i=0;i<=n;i++)  
  8.             match[0][i]=true;  
  9.         for(int len=1;len<=n;len++)  
  10.         {  
  11.             for(int start=0;start+len<=n;start++)  
  12.             {  
  13.                 string tmp=s.substr(start,len);  
  14.                 if(dict.count(tmp)>0)  
  15.                     match[len][start]=true;  
  16.                 else  
  17.                 {  
  18.                     for(int left=1;left<len;left++)  
  19.                     {  
  20.                         match[len][start]=match[left][start]&&match[len-left][start+left];  
  21.                         if(match[len][start])  
  22.                             break;  
  23.                     }  
  24.                 }  
  25.             }  
  26.         }  
  27.         if(match[n][0]==false)  
  28.             return vector<string>();  
  29.         vector<string> ans;  
  30.         vector<string> had;  
  31.         dfs(s,0,match,had,ans,dict);  
  32.         return ans;  
  33.     }  
  34.     void dfs(string& s,int k,vector<vector<bool> >& match,vector<string>& had,vector<string>& ans,unordered_set<string> &dict)  
  35.     {  
  36.         int n=s.length();  
  37.         if(k>=n)  
  38.         {  
  39.             if(!had.empty())  
  40.             {  
  41.                 string ret;  
  42.                 for(int i=0;i<had.size();i++)  
  43.                 {  
  44.                     ret.append(had[i]);  
  45.                     if(i!=had.size()-1)  
  46.                         ret.push_back(' ');  
  47.                 }  
  48.                 ans.push_back(ret);  
  49.                 return;  
  50.             }  
  51.         }  
  52.         for(int len=1;k+len<=n;len++)  
  53.         {  
  54.             string tmp=s.substr(k,len);  
  55.             if(dict.count(tmp)>0 && match[n-k-len][k+len])  
  56.             {  
  57.                 had.push_back(tmp);  
  58.                 dfs(s,k+len,match,had,ans,dict);  
  59.                 had.pop_back();  
  60.             }  
  61.         }  
  62.     }     
  63. };  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值