一.状态表示
一般来说使用动态规划手段解决问题在我们遇到重复子问题计算并且有着最优路径选择等问题时,我们常常要考虑多个方面并比较或重复计算,例如计算斐波那契数列时
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
如果用递归的方法你就会发现
当我们计算例如 F(5) 时,就会进行很多重复的计算,如下所示:
F(5) = F(4) + F(3)
//递归计算F(4)
F(4) = F(3) + F(2)
F(3) = F(2) + F(1)
F(2) = F(1) + F(0)
F(2) = F(1) + F(0)
//递归计算F(3)
F(3) = F(2) + F(1)
F(2) = F(1) + F(0)
F(3),F(2)被重复的计算了好几次,所以我们一般需要一个 dp(dynamic programing)表或其他数据结构来存储我们每次计算的结果不至于重复计算,如:
def fibonacci(n):
dp = [0] * (n+1)
dp[0] = 0
dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
这样我们可以清晰的知道表中相应下标对应的元素表示的状态就是我们所求的斐波那契数,而且避免了重复计算问题,使得我们进行后续操作时更加方便且具有更高的明确性和效率,所以解决动态规划问题的第一步就是确定dp表的状态表示,可以用物理中的参考系,一切事物都是围绕参考系来看待运算的。可以简单理解为dp表元素代表的特殊含义。
那么怎么确定状态表示呢?
1.题目要求
2.经验加题目要求
3.分析问题的过程中发现重复子问题
二.状态转移方程
就如我前面所说,状态表示就如同一个参考系一样,例如在物理中我们解决求速度的问题时,题目告诉你一个初速度,一个加速度,问:3秒后物体的速度是多少?是不是我们首先要明确物体是以大地为参考系,然后列速度方程:v末 = v初 + 加速度*时间。同样,在dp表确认我们像存储的元素的状态表示后,我们也要会求第dp表几个元素的值是多少,例如上文提到的斐波那契数列问题,dp表元素表示相应下标所对应斐波那契数值, dp[i] = dp[i-1] + dp[i-2],这就是状态转移方程来让我们求我们想要得到的东西。所以第二步就是根据你的状态表示列状态转移方程。
三.初始化
即保证填表的时候数组不能越界,例如 dp[i] = dp[i-1] + dp[i-2],若i = 0 那么不就访问dp[-1],dp[-2]了嘛,所以对于这两个值需要我们根据题目要求或者相关关系给他初始化。
四.填表顺序
为了填写当前状态时,所需要的状态已经计算过了,不难理解dp[i] = dp[i-1] + dp[i-2],因为我们已经初始化了dp【0】,dp【1】,若i等于4,我们就必须知道di【3】等于多少,所以我们首先要计算dp【3】,确保计算想要的值时,所需要的值都已经被计算,所以这道题我们从左到右填:
for i in range(2, n+1): dp[i] = dp[i-1] + dp[i-2]
五.返回值
要什么返回什么,还是根据题目要求来,就比如题目要你算第n个斐波那契数,那么
return dp[n];
就ok了;
六.空间优化
这一步看情况使用,一般使用滚动数组实现优化,首先明确一下什么叫空间优化,就比如dp[i] = dp[i-1] + dp[i-2],如果i = 3;那么dp[3] = dp[2] + dp[1];那么dp[1]我们就用不着了,反而浪费空间,所以怎么办呢?
int a = 0,b = 1;
for (int i = 2; i <= n; i++) {
int ne = a + b;
a = b;
b = ne;
}
return b;
使用两个变量来节省空间,首先用a和b代替bp[0] 和bp[1] ,然后每当计算完一次,将a和b往右移动一位,即将b 赋给 a,bp[2]赋给b,然后不断重复知道循环结束时,b的值代表bp[n];