动态规划总结
一个简单的模板
for() //遍历状态(如函数的可变参数,数组的索引)
if() //选择
dp[i] = dp[i-1]; //迭代公式(dp数组自底向上迭代,dp函数自顶向下递归)
else
...
维基百科中关于动态规划的描述是,只能运用于有最优子结构的问题,并且在处理的问题有很多重叠子问题时,特别有效。
最优子结构是指局部最优值能决定全局最优值。(这一点似乎和贪婪算法很相似)。
动态规划是一个求解最优化问题的方法,所以它的核心思想就是枚举,而确定状态迭代公式是动态规划问题的关键。
解法:
- 暴力递归(递归树)
- 因为存在重叠子问题,所以进行剪枝(带备忘录的递归),即自顶向下;
- 通过dp数组(代替备忘录)的迭代,进行自底向上
滚动数组思想
一种常见的动态规划优化方法,如果当前状态只和前面某几个状态相关的时候,就可以考虑这种优化方法,目的是给空间复杂度「降维」。比如斐波那契数列,只需要前两个状态,那么就只需要两个变量即可。
用二维数组举例(来源.)
int i, j, d[100][100];
for(i = 1; i < 100; i++)
for(j = 0; j < 100; j++)
d[i][j] = d[i - 1][j] + d[i][j - 1];
上面的d[i][j]只依赖于d[i - 1][j], d[i][j - 1];
运用滚动数组
int i, j, d[2][100];
for(i = 1; i < 100; i++)
for(j = 0; j < 100; j++)
d[i % 2][j] = d[(i - 1) % 2][j] + d[i % 2][j - 1];
题目类型
动态规划的题目分为两大类,一种是求最优解类,另一种就是计数类,它们都存在一定的递推性质(即最优子结构)。这两种情况都需要进行枚举。
例1、正则表达式匹配
题解参考了英文leetcode中的题解
确定迭代公式:
这道题关键点在于,* 匹配了多少次。它要么匹配0次,即dp[i][j] = dp[i][j-2],表示*和它前面的字符,都可以被忽略。要么它可以匹配多次。
(dp[i][j]为true,则表示s[0…i) 和p[0…j)能够匹配)
当匹配多次时,就在s字符串中不断去掉这样的重复字符,直到 * 只需要匹配0次。即dp[i][j] = dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == ‘.’)。可知这种迭代需要保证两个字符相等。
完整的公式为
- p[j - 1] != '*'时,dp[i][j] = dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == ‘.’);
- p[j - 1] == '*'时,dp[i][j] = dp[i][j - 2] (*匹配0次)或者 dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == ‘.’);
值得注意的是,看着公式是从大到小递归迭代,理解起来有点困难。但是编写代码的时候,i 和 j 都是从小到大的。
比如对于aabb和ab,首先判断对于空串,ab的每一个从a开始的子串,能不能匹配。接着判断对于a,ab的每一个从a开始的子串,能不能匹配。然后是aa,…,最后就是aabb。
2、不同路径
3、买卖股票
参考链接121-123
分析这个问题,可知:最后一天时的利润,可以是前一天时的利润+(最后两天买入卖出的利润),括号里是可选的,因为它不一定存在利润。
难点在于如何定义出各种状态,以及状态转移公式。
dp[i][j] 表示到下标为 i 的这一天,持股状态为 j (持有股票,还是持有现金)时,我们手上拥有的最大现金数。