1. 动态规划的适用场景
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
2. 动态规划的基本思想
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
2.1 重叠子问题
动态规划在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
2.2 最优子结构
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。
3. 动态规划的三要素
- 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
- 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
- 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
4. 动态规划算法的设计步骤:
- 刻画最优解的结构特征(寻找最优子结构)
- 递归地定义最优解的值(确定状态转移方程)
- 计算最优解的值(有两种方法:带备忘录自顶向下法、自底向上法)
- 利用计算出的信息构造一个最优解(通常是将具体的最优解输出)
5. 一般的解法
把动态规划的解法分为自顶向下和自底向上两种方式。
自顶向下的方式其实就是使用递归来求解子问题,最终解只需要调用递归式,子问题逐步往下层递归的求解。我们可以使用缓存把每次求解出来的子问题缓存起来,下次调用的时候就不必再递归计算了。
自底向上是另一种求解动态规划问题的方法,它不使用递归式,而是直接使用循环来计算所有可能的结果,往上层逐渐累加子问题的解。
注:一般看到string,往dp (dfs+memo) 上靠就对了
32. Longest Valid Parentheses:
dp[i]表示到i为止合法的()长度
s[i] == ')' :
dp[i] = dp[i-2] + 2 ( s[i]=='(' )
dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2] ( s[i-1] == ')' && s[i-1-dp[i-1]] == '(' )
注意判断数组下标值是否存在
72. Edit Distance
将word1转换成word2:
三种操作:插入/删除/替换 一个字符
dp[i][j]表示word1 [0, i),word2 [0, j) 子串转换成功时的最少转换次数
初始化: dp[0][j]=j 插入操作,dp[i][0]=i 删除操作
word1[i-1]==word2[j-1]: dp[i][j] = dp[i-1][j-1]
word1[i-1]!=word2[j-1]: dp[i][j] = min(dp[i-1][j-1], min(dp[i][j-1], dp[i-1][j])) + 1;
对应三种操作:替换 dp[i-1][j-1] + 1, 删除(word1[i-1]) dp[i-1][j] + 1,
插入(word1[i-1]后插入word2[j-1]才两个子串相同 <=> 删除word2[j-1]使得两个子串相同) dp[i][j-1] + 1
87. Scramble String
判断一个字符串是否由原字符串经过scramble变换得到,变换如下:
rgeat / \ rg eat / \ / \ r g e at / \ a t
判断一个字符串是scramble有两种情况:
把两个字符串从中间某位置切开
1. s1[i...i+len-1] 左边和 s2[j...j+len-1] 左边部分是不是 scramble,以及s1[i...i+len-1] 右边和 s2[j...j+len-1] 右边部分是不是 scramble;
2. s1[i...i+len-1] 左边和 s2[j...j+len-1] 右边部分是不是 scramble,以及s1[i...i+len-1] 右边和 s2[j...j+len-1] 左边部分是不是 scramble。
如果以上两种情况有一种成立,说明 s1[i...i+len-1] 和 s2[j...j+len-1] 是 scramble。
res[i][j][len] 表示起点为s1[i]和s2[j],长度均为len的两个子串是否为scramble。
状态转移方程:res[i][j][len] = || ((res[i][j][k] && res[i+k][j+k][len-k]) || (res[i][j+len-k][k]&&res[i+k][j][len-k])) 对于所有 1<=k 96. Unique Binary Search Trees dp[i]表示i个数时构成不同BST的个数 枚举每个数为顶点,依次划分为左右两部分。 状态转移方程:dp[i] += (dp[j-1]*dp[i-j]) j=[1, i] 115. Distinct Subsequences Given a string S and a string T, count the number of distinct subsequences of S which equals T. dp[i][j] 表示S[0, i)包含T[0, j)子串的个数 状态转移方程:dp[i][j] = dp[i-1][j] + ( S[i - 1] == T[j - 1] ? dp[i - 1][j - 1] : 0) 97. Interleaving String Given s1, s2, s3, find whether s3 is formed by the interleaving of s1 and s2. dp[i][j]表示s1 [0, i), s2 [0, j)两个字符串可以构成s3 [0, i+j) s1当前字符与s3当前字符相同:s1[i-1] == s3[i+j-1] 则 dp[i][j] = dp[i - 1][j] s2当前字符与s3当前字符相同: s2[j-1] == s3[i+j-1] 则 dp[i][j] = dp[i][j - 1] 状态转移方程:dp[i][j] = (dp[i - 1][j] && s1[i - 1] == s3[i - 1 + j]) || (dp[i][j - 1] && s2[j - 1] == s3[j - 1 + i]); 123. Best Time to Buy and Sell Stock III 两次股票交易,最大利润 f[i, j] 表示到prices[0, j)为止i次交易的最大利润 状态转移方程:对第j天的股票是否做卖出操作 f[i, j] = max(f[i, j-1], prices[j] - prices[k] + f[i-1, k]) { k 属于 [0, j-1] } = max(f[i, j-1], prices[j] + max(f[i-1, k] - prices[k])) {利用j循环进行k循环的优化} 两个鸡蛋测鸡蛋摔碎的临界楼层 最重要的是要找到子问题。做如下的分析,假设f{n}表示从第n层楼扔下鸡蛋,没有摔碎的最少尝试次数。第一个鸡蛋,可能的落下位置[1, n], 第一个鸡蛋从第i层扔下,有两个情况: 所以,当第一个鸡蛋,由第i个位置落下的时候,要尝试的次数为1 + max(i - 1, f{n - i}),那么对于每一个i,尝试次数最少的,就是f{n}的值。状态转移方程如下: f{n} = min(1 + max(i - 1, f{n - 1}) ) 其中: i的范围为(1, n), f{1} = 1 推广动态规划,可以推广为n层楼,m个鸡蛋。如下分析: 假设f{n,m}表示n层楼、m个鸡蛋时找到最高楼层的最少尝试次数。当第一个鸡蛋从第i层扔下,如果碎了,还剩m-1个鸡蛋,为确定下面楼层中的安全楼层,还需要f{i-1,m-1}次,找到子问题;不碎的话,上面还有n-i层,还需要f[n-i,m]次,又一个子问题。 状态转移方程如下: f{n, m} = min(1 + max(f{n - 1, m - 1}, f{n - i, m}) ) 其中: i为(1, n), f{i, 1} = i
dp[i][j] = dp[i - 1][j]
if t[j - 1] != s[i - 1]
; s[i - 1]肯定不能在子串中dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1]
if t[j - 1] == s[i - 1]
; 去掉当前匹配字符的s子串dp[0][j] = 0
for all positive j
;dp[i][0] = 1
for all i
. 空是任何字符串的子集