动态规划基础

  1. 什么是动态规划?

动态规划,英文为Dynamic Programing,简称dp,常用于解决一个问题有若干重叠子问题的题目。动态规划中每一个状态都是从上一个状态推导出来了,这一点区分于贪心,同时由这一点得出递推公式,即状态转移方程。

  1. 如何使用动态规划的思想解题?

使用dp解题,有五个步骤:

(1)确定dp数组及其下标的含义(定义一个dp数组,用带有下标的句子解释这个数组)

//假设有这样一个问题:
/*假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?*/
//那么我定义出的dp[i]的含义为“爬到第i层有dp[i]种方法”

(2)确定递推公式(分析题意及示例写出递推公式)

(3)确定dp数组如何初始化(根据递推公式,初始化dp数组)

(4)确定递推顺序(根据递推公式,确定递推顺序)

大部分题目都是从前往后递推,但也会有很少一部分题目(如背包问题的压缩版)需要从后往前递推才是合理的。至于二维dp[i][j],要注意先遍历i还是先遍历j,还是两者都可以。

(5)打印dp数组

这一点非必须(但其实常常用到),仅在debug的时候可能用到。

有大佬将上述5个步骤成为“动规五部曲”,我也依葫芦画瓢了。

  1. 当代码AC失败时如何debug?

将打印出来的dp数组和自己推导的dp数组比较,

如果打印结果和推导结果是一样的,说明思路没有问题,那么看看是不是递推公式、初始化或者遍历顺序有问题;

如果打印结果和推导结果不一样,那可能就是思路有问题了。

  1. 简单的动态规划例题:

4.1斐波那契数

题目:斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

示例1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例2:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

0 <= n <= 30

LeetCode原题链接

  1. 分析题目:

使用动规五部曲:

(1)确定dp数组及下标的含义:

dp[i]第i个表示斐波那契数的值是dp[i]。

(2)确定递推公式:

这个题写的很清楚——“后面的每一项数字都是前面两项数字的和”,那么递推公式就为“dp[i]=dp[i-1]+dp[i-2]”

(3)dp数组如何初始化:

由递推公式及题目“该数列由 0 和 1 开始”可知,数组的初始化如下:

dp[0] = 0;
dp[1] = 1;

(4)确定递推顺序:

从递推公式dp[i]=dp[i-1]+dp[i-2]”可以看出,每一项都是由前两项共同推导出来的,即前面的值确定后面的值,同时初始化条件是i=0和i=1,处于最考前的位置,那么我们可以从前往后遍历。

(5)打印dp数组:

根据斐波那契数列的概念,可以推出,前10个dp数组应该是这样:

1 1 2 3 5 8 13 21 34 55

2.C/C++代码如下:

#include<bits/stdc++.h>
using namespace std;
int N=30;
//定义dp数组
vector<int> dp(N+1);//从第0个数到第n个数,一共有n+1个数,开数组也要开这么大,同时dp数组中每个元素的默认值是0
int main(){
    int n; 
    cin >> n;
//初始化dp数组
     dp[0]=0;
     dp[1]=1;
//遍历
     if(n<=1) cout << n << endl;
    for(int i=2;i<=n;i++){
//写递推公式
        dp[i]=dp[i-1]+dp[i-2];
//打印dp数组
        //cout << dp[i] << ' '; 
    } 
    cout << dp[n] << endl;
    return 0;
} 

3.注意:写代码的时五个步骤的顺序应该是:

(1)定义dp数组

(2)初始化dp数组

(3)遍历

(4)写递归公式

(5)打印dp数组

与分析题目的时候略有不同。

4.2爬楼梯

题目:

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例1.

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例2.

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示

1 <= n <= 45

LeetCode原题链接

  1. 分析题目

(1)确定dp数组

题目中说“有多少种不同的方法可以爬上楼顶”,那么我可以定义一个dp[i]数组,表示从楼下开始向上爬,爬到第i阶有dp[i]种不同的方法。

(2)确定递推公式

由于题目设定“每次可以爬 1 或 2 个台阶”,那么假如我们想要爬到第i阶,可以从第i-1阶下一次迈步,爬1阶到第i阶,也可以从第i-2阶下一次迈步,爬2阶到第i阶,那么根据数学概率的基本原理——加法原理,我们可以写出递推公式:dp[i]=dp[i-1]+dp[i-2](其实就是斐波那契数列)

(3)初始化dp数组

由于n是正整数,我们不需要考虑i=0的情况

由递推公式可知每一项dp都是由数组中该项的前两项推导出来,将这个逻辑推到极致,可知我们至少需要知到dp[1]和dp[2],因此dp数组的初始化如下:

dp[1]=1;
dp[2]=2;

(4)确定遍历顺序

由(3)可知,遍历顺序由前到后

(5)打印dp数组

经推导,dp数组的前5项如下:

1 2 3 5 8

  1. C/C++代码如下

#include<bits/stdc++.h>
using namespace std;
int N=45;
vector<int> dp(N);
int main(){
    int n;
    cin >> n;
    dp[1]=1;
    dp[2]=2; 
    if(n<=2) cout << n << endl;
    for(int i =3; i<= n;i++){
        dp[i]=dp[i-1]+dp[i-2];
        //打印dp数组
        //cout << dp[i] << ' '; 
    }
    cout << dp[n] << endl;
    return 0;
} 

4.3最小花费爬楼梯

题目:

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

提示:

2 <= cost.length <= 1000
0 <= cost[i] <= 999

分析题目:

动规五部曲

(1)确定dp数组:

题目中说“请你计算并返回达到楼梯顶部的最低花费”,那么我就定义一个dp[i],表示到达第i层阶梯时的最低花费为dp[i]

(2)确定递推公式:

这是上一个问题的变种,加了一个cost数组,表示在当前层i若要向上爬需要有一定的花费,即cost[i],通过cost数组的长度,我们可以知道到达楼梯顶部,其实是从第i个阶梯向上再爬一次,进行一定花费,顶部的编号其实是i+1。

结合对上一个问题的分析,可知第i层的最少花费可以由第i-1层或第i-2层的最少花费+继续向上爬一步的花费结合加法原理推导出来,即dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])

(3)确定初始化条件:

由递推公式可知,每一项都是由该项的前两项推导出来,将这个逻辑推到极致,那么我么至少需要直到dp[0]和dp[1]的值,那么该题的初始化条件如下:

dp[0]=0; 
dp[1]=0; //在没有看清题目要求前,我写的是dp[1]=cost[1],以为只能从下标为0的阶梯开始

因为是可以从下标为0或者下标为1的阶梯直接出发的,可以没有经历向上爬的过程,所以最少的花费是0

(4)确定遍历顺序:

由于每一项都是由前两项推导出来的,由前导后,因此遍历顺序也是从后向前

(5)打印dp数组

很容易地将示例2中到达各个阶梯地最小花费写出来:

输入:cost = [1,100,1,1,1,100,1,1,100,1],1一共有10个阶梯需要爬
   //数组下标 0   1  2 3 4   5  6 7  8  9
输出:6
dp[0]=dp[1]=0,dp[2]=1,dp[3]=dp[4]=2,dp[5]=dp[6]=3
dp[7]=dp[8]=4,dp[9]=5,dp[10]=6

  1. C/C++代码:

#include<bits/stdc++.h>
using namespace std;

vector<int> cost; //建立一个动态的cost数组,长度暂时不定 
vector<int> dp(cost.size()+1); //cost数组的长度+1,就事dp数组的长度,原因是我们的dp数组的下标是从0到cost.size()的,数组的大小应该是cost.size()+1 
 
int main(){
    //向cost数组中输入数据 
    while(1){
        int m; 
        cin >> m;
        cost.push_back(m); //将当前输入地整数压入cost数组中 
        if(char c=getchar()=='\n') break; //读到回车停止输入 
    }

    dp[0]=0;
    dp[1]=0;
        for(int i=2; i<=cost.size();i++){
        dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        //打印dp数组 
        //cout << dp[i] << " " ;
    } 
    cout << dp[cost.size()]<< endl;
    return 0;
}

这次我的代码采用的是传统的OJ方式,所以不能直接在力扣上AC,要抽象成函数才可以通过AC,后续题目我会提供传统OJ和适应力扣的OJ方式的代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值