动态规划(附带入门例题)

特别需要注意:

1.动态规划数组(dp数组)的定义以及下标的含义。例如:在李白打酒中f[i][j][k]表示方案数,i表示经过店的数量,j表示经过花的数量,k表示剩余酒的数量。

2.递推公式或状态转移方程

3.dp数组是如何进行初始化的

4.遍历顺序

5.打印dp数组的值

什么时候会用到动态规划数组?

题型:有多少种方案。

枚举大多数情况下不行,搜索通常也不行,但是可能会成功。

所以我们这个时候考虑动态规划。

解题步骤:

1.定义数组:时刻记住你定义的数组的含义及下标。注意有时题上会帮我们定义,但是有时也会误导我们。

2.写状态转移方程。

有两种写法:f[i]由什么转移过来。f[i]可以发展到f[i+1]的什么情况。特别例如李白打酒,就是逆向思维。

3.初始化。

初始化f[0],初始化的方法有两种:根据定义的函数来写,根据实际意思。

4.枚举遍历所有的情况。用子结构递推到最终的结果。

入门例题:

1.斐波那契数列

1 1 2 3 5 8(可以不用动态规划,但非常适合作为动态规划入门,训练一下动态规划步骤和需要注意的问题)

确定dp[i]的含义,dp[i]表示第i个斐波那契数的dp[i]。

递推公式,dp[i]=dp[i-1]+dp[i-2],通过递推关系可以得到

dp数组如何初始化,dp[0]=1,dp[1]=1

遍历顺序,从前向后遍历,这样才能保证dp[i]是由最新的dp[i-1]和dp[i-2]构成

打印dp数组,目的是为了看看我们写的和应该的斐波那契数列是否一样

cs 复制代码
#include<stdio.h>
int main()
{
	long long dp[n+1];
	dp[1]=1,dp[0]=1;
	int i;
	for(i=2;i<=n;i++){
		dp[i]=dp[i-1]+dp[i-2];
	}
	printf("%lld\n",dp[n]);
	return 0;
} 

为什么这是动态规划:

  1. 最优子结构 :斐波那契数列具有最优子结构性质,F(n) = F(n-1) + F(n-2);最优子结构是**大问题的最优解 = 小问题的最优解 + 某种组合方式,**如果一个问题的解可以通过组合其子问题的最优解来获得,那么这个问题就具有最优子结构性质。

  2. 重叠子问题 :计算 F(n) 需要重复计算 F(n-1)F(n-2) 等子问题。重复计算,直接存储。

  3. 状态定义dp[i] 表示斐波那契数列的第 i

  4. 状态转移方程dp[i] = dp[i-1] + dp[i-2]

  5. 自底向上计算 :从基础情况 dp[0]dp[1] 开始,逐步计算到目标值

  • ✅ 使用数组 dp 存储子问题的解

  • ✅ 从最小子问题开始求解(dp[0]dp[1]

  • ✅ 利用已解决的子问题构建更大的解

  • ✅ 避免了重复计算(这是动态规划的关键优势)

怎么避免的重复计算?

存储计算结果:

cs 复制代码
步骤1: dp[0] = 0
步骤2: dp[1] = 1
步骤3: dp[2] = dp[1] + dp[0] = 1 + 0 = 1  ← 计算一次,存储
步骤4: dp[3] = dp[2] + dp[1] = 1 + 1 = 2  ← 直接使用存储的dp[2]
步骤5: dp[4] = dp[3] + dp[2] = 2 + 1 = 3  ← 直接使用存储的dp[3]和dp[2]
步骤6: dp[5] = dp[4] + dp[3] = 3 + 2 = 5  ← 直接使用存储的dp[4]和dp[3]

总计算次数:5次(一次加法计算一个dp值)

和递归的区别:递归没有存储结果的功能

cs 复制代码
int fib(int n) {
    if(n <= 1) return n;
    return fib(n-1) + fib(n-2);
}
cs 复制代码
                fib(5)
               /      \
          fib(4)      fib(3)
          /    \       /    \
     fib(3)  fib(2) fib(2) fib(1)
     /    \    /    \
 fib(2) fib(1) fib(1) fib(0)
 /    \
fib(1) fib(0)
  • fib(5):计算1次

  • fib(4):计算1次

  • fib(3):计算2次(在左子树和右子树各1次)

  • fib(2):计算3次

  • fib(1):计算5次

  • fib(0):计算3次

总计: 1+1+2+3+5+3 = 15次函数调用

2.爬楼梯

和上述斐波那契数列基本一样思路。只是初始化不用考虑dp[0],直接从dp[1]=1,dp[2]=2初始化。这里就不细分析了。

dp[i]表示达到第i阶有dp[i]种方法。

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

首先正确理解题目"你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。"也就是说明跳到下标0或下标1是不花费体力的,从下标0或下标1开始跳就要花费体力了。

1.确定dp数组以及下标的含义:是用动态规划,就要有一个数组来记录状态。更具本题意思,只需要一个一维数组即可记录。

dp[i]表示到达i位置的花费最小体力为dp[i]

2.确定递推公式

发现有两个途径可以得到dp[i],一个是走一个台阶dp[i-1],一个是走两个台阶dp[i-2]。

dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。

dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。

那么要选择哪条路径来到达i台阶呢?

当然是选择最小体力花费的,即dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);

3.dp数组初始化

看⼀下递归公式,dp[i]由dp[i - 1],dp[i - 2]推出,所以只需要初始化两个就行,不需要初始化整个数组。根据该题只初始化dp[0]和dp[1]即可,其他的最终都是dp[0]dp[1]推出。

那么 dp[0] 应该是多少呢? 根据dp数组的定义,到达第0台阶所花费的最⼩体⼒为dp[0],dp[0] 应该是 cost[0],例如 cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] 的话,dp[0] 就是 cost[0] 应该是1。实际上,根据题意,当跳的时候才会消耗体力,也就是说 到达 第 0 个台阶是不花 费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。所以初始化 dp[0] = 0,dp[1] = 0;

4.确定遍历顺序

从前到后遍历cost数组就可以了。

cs 复制代码
int minCostClimbingStairs(int* cost, int costSize) {
    int dp[costSize + 1];
    
    dp[0] = 0;
    dp[1] = 0;
    
    for (int i = 2; i <= costSize; i++) {
        int option1 = dp[i - 1] + cost[i - 1];
        int option2 = dp[i - 2] + cost[i - 2];
        dp[i] = option1 < option2 ? option1 : option2;//取最小的体力花费
    }
    
    return dp[costSize];
}
上述代码解析:
cs 复制代码
例子数据:
cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
楼顶位置:下标 10(数组长度 10,最后一个元素下标 9)

逐步计算:

cs 复制代码
到达台阶2:i=2
从台阶1来:dp[1] + cost[1] = 0 + 100 = 100
从台阶0来:dp[0] + cost[0] = 0 + 1 = 1
dp[2] = min(100, 1) = 1  (选择从台阶0来)
到达台阶3 (i=3)
从台阶2来:dp[2] + cost[2] = 1 + 1 = 2
从台阶1来:dp[1] + cost[1] = 0 + 100 = 100
dp[3] = min(2, 100) = 2  (选择从台阶2来)
到达台阶4 (i=4)
从台阶3来:dp[3] + cost[3] = 2 + 1 = 3
从台阶2来:dp[2] + cost[2] = 1 + 1 = 2
dp[4] = min(3, 2) = 2  (选择从台阶2来)
到达台阶5 (i=5)
从台阶4来:dp[4] + cost[4] = 2 + 1 = 3
从台阶3来:dp[3] + cost[3] = 2 + 1 = 3
dp[5] = min(3, 3) = 3  (两者相同)
............
到达楼顶(台阶10) (i=10)
从台阶9来:dp[9] + cost[9] = 5 + 1 = 6
从台阶8来:dp[8] + cost[8] = 4 + 100 = 104
dp[10] = min(6, 104) = 6  (选择从台阶9来)
cs 复制代码
最终结果
dp数组 = [0, 0, 1, 2, 2, 3, 3, 4, 4, 5, 6]
最小花费 = dp[10] = 6
相关推荐
weixin_445402302 小时前
C++中的命令模式变体
开发语言·c++·算法
季明洵2 小时前
C语言实现顺序表
数据结构·算法·c·顺序表
Hgfdsaqwr2 小时前
实时控制系统优化
开发语言·c++·算法
2301_821369613 小时前
嵌入式实时C++编程
开发语言·c++·算法
sjjhd6523 小时前
多核并行计算优化
开发语言·c++·算法
恶魔泡泡糖3 小时前
51单片机串口通信
c语言·单片机·嵌入式硬件·51单片机
weixin_395448913 小时前
main.c_cursor_0130
前端·网络·算法
知无不研3 小时前
c语言动态内存规划
c语言·动态内存管理·内存泄露·基础知识·malloc·realloc·calloc
半壶清水3 小时前
[软考网规考点笔记]-操作系统核心知识及历年真题解析
网络·网络协议·算法