应用场景
DP是求解多阶段决策问题最优化的一种算法思想,它用于解决具有重叠子问题、具有子结构特征的问题。
能够解决的问题所具有的特征
重叠子问题、最优子结构。用DP可以高效率的处理具有这两个特征的问题。
1. 重叠子问题
首先,子问题是原大问题的小版本,计算步骤一致;其次,计算大问题时,需要多次重复计算小问题。这就是重叠子问题。
但是值得我们思考的是:在一些时候我们对一个同样的子问题会进行多次重复计算,耗费了大量的时间(参考斐波那契数列)。用DP处理重叠子问题时,每个子问题只计算一次,从而避免了重复计算,这就是DP效率高的原因。
具体做法:首先分析得到最优子结构,然后递归或带记忆化搜索的递归进行编程,从而实现高效的计算。
下面是递归法求解斐波那契数列:
int fib(int n){
if(n==1||n==2) return 1;
return fib(n-1)+fib(n+2);
}
用纯递归的方法来解决问题的时间复杂度为O(2^n),可以看出非常浪费时间。
(DP代码往下看)
2. 最优子结构:
在我们使用DP时,我们需要注重DP的一个性质就是“无后效性”。简单来说就是你现在做的事情与未来没有关系(参考斐波那契数列)。无后效性时DP的必要条件。因为只有这样我们才能降低算法的复杂度,应用DP才有意义。
DP的两种编程方法
处理DP中的大问题和小问题。有两种思路:自顶向下(先大问题,再小问题);自底向上(先小问题,再大问题)。
1.自顶而下与记忆化
int memoize[N];
int fib(int n){
if(n==1||n==2) return 1;
if(memoize[n]!=0) return memoize[n];
memoize[n]=fib(n-1)+fib(n-2);
return memoize[n];
}
线性规划的时间复杂度为O(n)。相比上面的代码,时间复杂度得到了极大的改善。
2.自底而上与制表递推
这种方法与自顶而下相反,避免了递归编程。我们需要一个表(dp[])来维护我们的数据。
const int N = 255;
int dp[N];
int fib(int n){
dp[1]=dp[2]=1;
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
代码的时间复杂度也为O(n).
下节分享DP的设计和实现