动态规划1--基本步骤

动态规划

最近听了九章算法复习了动态规划,对该算法进行了总结。后续会继续补充

解题步骤:

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(272)+1,f(275)+1,f(277)+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(272)+1,dp(275)+1,dp(277)+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);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值