My thinking about dynamic programming
动态规划是一种以空间换时间的技巧,通常消耗的空间在接受范围内,但是速度却可以从指数
级下降到多项式的时间。在学习动态规划之前,需要先了解:
Steps to solve a DP
1) Identify if it is a DP problem
2) Decide a state expression with
least parameters
3) Formulate state relationship4) Do tabulation (or add memoization)
下面我们就根据步骤来讲解:
Step 1 : How to classify a problem as a Dynamic Programming Problem?
- 需要求最大最小值或者计数在某个条件下的个数
- 满足最优子结构、重叠子问题和无后效性
Step 2 : Deciding the state
DP问题的核心就是状态和状态的转移。状态的定义将会决定状态转移的方式。所以,下面来看一下如何来定义DP
的状态吧。
状态简单地说就是参数,用来确定子问题的参数。参数的个数最好尽量的少,这样可以减少状态空间。
例如,在我们的背包问题中,我们通过下标和重量来定义状态:DP[index][weight].
这里DP[index][weight]告诉我们可以获得的最大价值(value),我们可以通过index和weight
的变化来写出转移方程,即确定了子问题。
正如我们所知,DP是通过已经计算的结果来得到最终的结果的方法。所以,下面我们就来介绍如何寻找
previous state 和 current state 的关系
Step 3 : Formulating a relation among the states
这部分应该是DP中最难的一部分了,而且需要练习和观察。下面给出一个例子:
Given 3 numbers {1, 3, 5}, we need to tell
the total number of ways we can form a number 'N'
using the sum of the given three numbers.
(allowing repetitions and different arrangements).
Total number of ways to form 6 is: 8
1+1+1+1+1+1
1+1+1+3
1+1+3+1
1+3+1+1
3+1+1+1
3+3
1+5
5+1
首先我们先定义状态,我们可以定义state(n):the total number of arrangements to form n by using {1, 3, 5} as elements.
所以我们的最终目标是计算state(n).
下面我们来展示如何得到state(7):
1) Adding 1 to all possible combinations of state (n = 6)
Eg : [ (1+1+1+1+1+1) + 1]
[ (1+1+1+3) + 1]
[ (1+1+3+1) + 1]
[ (1+3+1+1) + 1]
[ (3+1+1+1) + 1]
[ (3+3) + 1]
[ (1+5) + 1]
[ (5+1) + 1]
2) Adding 3 to all possible combinations of state (n = 4);
Eg : [(1+1+1+1) + 3]
[(1+3) + 3]
[(3+1) + 3]
3) Adding 5 to all possible combinations of state(n = 2)
Eg : [ (1+1) + 5]
通过细心的思考和发现,我们可以得到:
state(7)=state(6)+state(4)+state(2)
或者说,
state(7)=state(7-1)+state(7-3)+state(7-5)
再把它推广一下:
state(n)=state(n-1)+state(n-3)+state(n-5)
所以我们暂且可以写出如下的代码:
int solve(int n)
{
// base case
if (n < 0)
return 0;
if (n == 0)
return 1;
return solve(n-1) + solve(n-3) + solve(n-5);
}
但是,这份代码的时间是指数级的,因为存在大量的重复计算,所以下一步就是需要储存已经计算的结果。
Step 4 : Adding memoization or tabulation for the state
我们只需要把计算过的结果保存在数组或者哈希表中(较少情况下,超大背包的时候也许有用)。
所以把上面的那份代码加一个记忆化储存结果就可以了:
// initialize to -1
int dp[MAXN];
// this function returns the number of
// arrangements to form 'n'
int solve(int n)
{
// base case
if (n < 0)
return 0;
if (n == 0)
return 1;
// checking if already calculated
if (dp[n]!=-1)
return dp[n];
// storing the result and returning
return dp[n] = solve(n-1) + solve(n-3) + solve(n-5);
}