动态规划
最近听了九章算法复习了动态规划,对该算法进行了总结。后续会继续补充
文章目录
解题步骤:
1、确定状态
1.1最后一步要求什么,最优策略的最后一步。
1.2由最后一步向前转化,确定子问题,减小规模。
2、转移方程
把想法转化成式子。
3、初始条件和边界情况
4、确定计算顺序
从前到后,从后到前?
先介绍三种常见的动态规划类型:
1、最值型动态规划 min/max
example: CoinChange
你有三种硬币,分别面值2元,5元和7元,每种硬币都有足够多
买一本书需要27元
如何用最少的硬币组合正好付清,不需要对方找钱
问题分析:
1、最后一步:
我们不知道最优的策略是什么,但最优策略肯定是k枚硬币a1,a2,…,ak加起来是27
所以一定有一枚最后的硬币:ak
除掉这枚硬币,前面硬币的面值加起来是27-ak
1.1:我们不关心前面的k-1枚硬币是怎么拼出27-ak的,而且我们不知道ak和k,但我们确定前面的硬币拼出了27-ak
1.2:因为是最优策略,所以拼出27-ak的硬币数一定要少,即拼出27-ak的策略也是最优的,否则就不是最优策略了
由此问题规模缩小,我们可以确定子问题:用最少枚硬币拼出27-ak
我们用dp[x]表示拼出x需要的最优硬币数
子问题 :
我们并不知道最后的硬币ak是多少,可能是2、5、7中的一个
如果ak是2, dp(27) = dp(27 - 2) + 1
如果ak是5, dp(27) = dp(27 - 5 ) + 1
如果ak是7, dp(27) = dp(27 - 7 ) + 1
由于需要最少的硬币数,所以
f ( 27 ) = m i n { f ( 27 − 2 ) + 1 , f ( 27 − 5 ) + 1 , f ( 27 − 7 ) + 1 } f(27) = min\{f(27 - 2 ) + 1, f(27 - 5) + 1 , f(27 - 7) + 1\} f(27)=min{
f(27−2)+1,f(27−5)+1,f(27−7)+1}
分析出这个式子后,其实有了一种递归的解法:
#include <iostream>
#include <algorithm>
using namespace std;
int f(int X)
{
if(X == 0) return 0;
int res = INT16_MAX;
if(X >= 2){
res = min(res,f(X-2) + 1);
}
if(X >= 5)
{
res = min(res,f(X-5) + 1);
}
if(X >= 7)
{
res = min(res,f(X-7) + 1);
}
return res;
}
int main()
{
cout<<f(27);
}
但如果我们输入一个比27大一点的数,比如1000,理论上是可以拼出来的(200个5…等拼法,但我们发现其速度非常慢)
这是为什么呢?
原因是,在递归的计算中,我们可以将递归过程抽象成上图,我们发现f(20)、f(18)等是多次被计算的而一次计算的背后又是多层递归,所以其时间和空间复杂度是非常大的。
为了避免重复的计算,我们可以想到,如果将计算过的结果保存下来,那么计算开销是不是减少了呢
2、转移方程
其实就是:
d p ( 27 ) = m i n { d p ( 27 − 2 ) + 1 , d p ( 27 − 5 ) + 1 , d p ( 27 − 7 ) + 1 } dp(27) = min\{dp(27 - 2 ) + 1, dp(27 - 5) + 1 , dp(27 - 7) + 1\} dp(27)=min{
dp(27−2)+1,dp(27−5)+1,dp(27−7)+1}
3、边界情况
对于上述方程,我们需要考虑如下问题:
x-2,x-5,x-7 小于0时怎么办?(数组越界) 什么时候停下来?
我们提出如下解决方案:
不能拼出的Y就定义dp[Y] 为正无穷
所以dp[1] = min(dp(1 - 2 ) + 1, dp(1 - 5 ) + 1, dp(1 - 7) + 1) = 正无穷
并设置初始条件,dp[0] = 0
4、计算顺序:
依次计算,dp[0] , dp[1] , dp[2] …
由上述顺序可以看到,当计算到dp[x]时,dp[x-2],dp[x-5],dp[x-7]均以得出。
代码实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/*
coinChange
*/
class Solution{
public:
int coinChange(vector<int>& A, int M)
{
vector<int> dp(M+1);
dp[0] = 0;
int n = A.size();
for(int i = 1; i <= M; i ++){
dp[i] = INT16_MAX;
for(int j = 0; j < n; j ++){
if(i >= A[j] && dp[i- A[j]] != INT16_MAX) //dp[i - A[j]] == INT16_MAX 说明拼不出
dp[i] = min(dp[i - A[j]] + 1,dp[i]);
}
}
if(dp[M] == INT16_MAX) //所给硬币无法满足条件
dp[M] = -1;
return dp[M];
}
};
int main()
{
Solution ans;
vector<int> coin = {
2,5,7};
cout<<ans.coinChange(coin,1000);
}
2、计数型动态规划 +
example:Unique Paths
题意:
给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者向右走一步
问有多少种不同的方式走到右下角
还是按照四步走:
问题分析:
1、最后一步:
无论机器人什么方式到达最下,到达最后的格子只能有上图两种情况:即对于m行n列的方格来说,只能是由M[m-2,n-1] 到 M[m-1,n-1]或M[m-1,n-2]到M[m-1,n-1]
子问题:
加法定理:如果机器人有X种方法到达M[m-2,n-1] 有Y种方法到达M[m-1,n-2],则有X+Y种方法到达M[m1,n-1]
问题转化为:有多少种方法到达M[m-2,n-1] 和 M[m-1,n-2]
假设dp[i,j]表示有多少种方法到达(i,j)
2、转移方程:
对于每一个格子,有dp[i,j] = dp[i-1,j] + dp [i, j -1 ]
3、边界条件:
dp[0,0]显然为1
边界情况:i = 0 或 j = 0时,只有一种方式到达,即f[i,j] = 1
4、计算顺序:
逐行计算,这样就不会有遗漏或前置条件未知。
时间复杂度O(m*n)
空间复杂度O(m*n)
代码实现
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
int robot(int m,int n){
vector<vector<int>> dp(m,vector<int>(n));
for(int i = 0; i < m; i ++){
for(int j = 0; j < n; j++){
if(i == 0 || j == 0)
dp[i][j] = 1;
else
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
int main()
{
Solution ans;
int m,n;
cin>>m>>n;
cout<<ans.robot(m,n);
}
3、可行性型动态规划 or/and
所谓可行性,就是看能否达到要求,返回True or Flase
example: Can Jump
有n块石头分别在x轴的0,1, ... , n-1位置
一只青蛙在-石头0,想跳到石头n-1
如果青蛙在第i块石头上,它最多可以向右跳距离ai
问青蛙能否跳到石头n-1
例子:
输入: a = [2,3,1,1,4]
输出: True
输入: a = [3,2,1,0,4]
输出: False
问题分析
1、确定状态:
**要跳到最后一块石头n-1 **
子问题:能跳到石头i, i < n-1 ,且需满足:青蛙可以跳到i,最后一步最大距离A[i] >= n - 1 -i
2、转移方程:
对于石头j,能跳到该石头的条件是:
存在 A[i] ,该石头可达(dp[i] = true)且 0<= i < j 使得 i + A[i] >= j
如果满足,则dp[j] = True
3、边界条件:
dp[0] = 0;
4、计算顺序:
从左到右计算dp[0] ~ dp[n-1] 返回 dp[n - 1]
代码实现
#include <iostream>
#include <vector>
using namespace std;
class Solution
{
public:
bool canJump(vector<int>& A)
{
int n = A.size();
vector<bool> dp(n);
dp[0] = true;
for(int j = 0; j < n; j++){
for(int i = 0; i <= j; i++){
if(dp[i] && A[i] >= j - i){
dp[j] = true;
break;
}
}
}
return dp[n-1];
}
};
int main()
{
Solution ans;
int n;
cin>>n;
vector<int> A(n);
for(int i = 0; i < n; i++)
{
cin>>A[i];
}
cout<<ans.canJump(A);
}