1.直接考虑动态规划的题目类型:
求最值:因为最优子结构性质作为动态规划问题的必要条件,一定是让你求最值的,以后碰到那种恶心人的最值题,思路往动态规划想就对了,这就是套路。
子序列:因为子序列类型的问题,穷举出所有可能的结果都不容易,而动态规划算法做的就是穷举 + 剪枝,它俩天生一对儿。所以可以说只要涉及子序列问题,十有八九都需要动态规划来解决,往这方面考虑就对了。
一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。
2.总结一下动态规划的设计流程:
首先明确 dp 数组所存数据的含义。这步很重要,如果不得当或者不够清晰,会阻碍之后的步骤。
然后根据 dp 数组的定义,运用数学归纳法的思想,假设 dp[0…i−1] 都已知,想办法求出 dp[i],一旦这一步完成,整个题目基本就解决了。
但如果无法完成这一步,很可能就是 dp 数组的定义不够恰当,需要重新定义 dp 数组的含义;或者可能是 dp 数组存储的信息还不够,不足以推出下一步的答案,需要把 dp 数组扩大成二维数组甚至三维数组。
3.动态规划的模板框架
带备忘录的递归-自顶向下
自顶向下主要是定义dp函数
参数:状态
返回值:要求的数值
备忘录:memo[state],=初始化值,还未计算过;!=初始化值,状态state的结果值
先初始化整个备忘录,用一个正常情况下结果取不到的值进行初始化整个备忘录,说明还没被计算过。例如结果都是大于零的数,则初始化为0;如果结果能取到零,就初始化为-1: memo(memo,-1,sizeof(memo))
注意 如果用特殊值表示“还没计算过“,必须和其他特殊值区(比如无解)区分开。
//这里是把结果数组和备忘录合在一起,如果答案和标记不能用正负/0区分,就单独用dp[]记录答案,用memo记录是否计算过
int dp(int state)
{
1.在备忘录中检查,若有直接return备忘录:if(memo[state]>=0) return memo[state];
2.base case:给定最初始的状态返回值
3.初始化memo[state]:如果是求最大值,就初始化为0,如果求最小值,就初始化为INF或者所有状态可能取到的最大值//为了求最值
4.开始自顶向下:
for 选择:(选择就是能使状态发生变化的动作)
{
该选择是否使得状态转移到一个有效的状态,否:continue
memo[state]=min/max{memo[state],状态转移方程}
}
5.记入备忘录
return memo[state]
迭代法-自底向上
迭代法不需要备忘录,因为他是自底向上的,不会出现重复计算的情况,所以也不需要判断是否计算过
自底向上主要是定义数组dp[i]
i是状态
dp[i]是状态i对应的答案
给出base case的结果
初始化dp[i]:求最小值,就初始化为INF(或者该状态可能取到的最大值);求最大值,就初始化为0
//这一步和上面初始化memo[state]一样,是为了求最值,不是初始化备忘录
自底向上
for 状态(从base case开始):
for 选择(每个能使状态发生改变的动作):
{
是否使状态到达有效状态,否则continue
dp[i]=min/max(dp[i],状态转移方程)
}