C++算法学习心得八.动态规划算法(1)

1.动态规划理论基础

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的

对于动态规划问题,拆解为如下五步曲,

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

一些情况是递推公式决定了dp数组要如何初始化

找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的。

做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果

发出这样的问题之前,其实可以自己先思考这三个问题:

  • 这道题目我举例推导状态转移公式了么?
  • 我打印dp数组的日志了么?
  • 打印出来了dp数组和我想的一样么?

2.斐波那契数(508题)

题目描述:

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。

示例 1:

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

动规五部曲:

这里我们要用一个一维dp数组来保存递归的结果

  1. 确定dp数组以及下标的含义

dpi的定义为:第i个数的斐波那契数值是dpi

  1. 确定递推公式

为什么这是一道非常简单的入门题目呢?

因为题目已经把递推公式直接给我们了:状态转移方程 dpi = dpi - 1 + dpi - 2;

  1. dp数组如何初始化

题目中把如何初始化也直接给我们了,如下:

复制代码
dp[0] = 0;
dp[1] = 1;
  1. 确定遍历顺序

从递归公式dpi = dpi - 1 + dpi - 2;中可以看出,dpi是依赖 dpi - 1 和 dpi - 2,那么遍历的顺序一定是从前到后遍历的

  1. 举例推导dp数组

按照这个递推公式dpi = dpi - 1 + dpi - 2,我们来推导一下,当N为10的时候,dp数组应该是如下的数列:

0 1 1 2 3 5 8 13 21 34 55

cpp 复制代码
class Solution {
public:
    int fib(int n) {
        if(n<=1)return n;//
        vector<int>dp(n+1);//定义一个动态规划数组,含义是第i个斐波那契数
        dp[0] = 0;//初始值设定
        dp[1] = 1;
        //遍历顺序,从前向后遍历
        for(int i = 2;i <= n;i++){
            dp[i] = dp[i - 1] + dp[i-2];//递推公式
        }
        return dp[n];//求第n个斐波那契数
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

递归法:

cpp 复制代码
class Solution {
public:
    int fib(int n) {
        if(n < 2)return n;
        return fib(n - 1) + fib(n - 2);
    }
};
  • 时间复杂度:O(2^n)
  • 空间复杂度:O(n),算上了编程语言中实现递归的系统栈所占空间

3.爬楼梯(70题)

题目描述:

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

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

注意:给定 n 是一个正整数。

示例 1:

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

动规五部曲:

定义一个一维数组来记录不同楼层的状态

  1. 确定dp数组以及下标的含义

dpi: 爬到第i层楼梯,有dpi种方法

dpi = dpi - 1 + dpi - 2

不考虑dp0如何初始化,只初始化dp1 = 1,dp2 = 2,然后从i = 3开始递推,这样才符合dpi的定义。

确定遍历顺序

从递推公式dpi = dpi - 1 + dpi - 2;中可以看出,遍历顺序一定是从前向后遍历的

cpp 复制代码
class Solution {
public:
    int climbStairs(int n) {
        if(n <= 1)return n; 因为下面直接对dp[2]操作了,防止空指针
        vector<int>dp(n+1);
        dp[1] = 1;
        dp[2] = 2;
        // 注意i是从3开始的
        for(int i = 3;i <= n;i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

4.使用最小花费爬楼梯 (746题)

题目描述:

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 costi(下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例 1:

  • 输入:cost = 10, 15, 20
  • 输出:15
  • 解释:最低花费是从 cost1 开始,然后走两步即可到阶梯顶,一共花费 15 。

动态规划:dpi的定义:到达第i台阶所花费的最少体力为dpi

可以有两个途径得到dpi,一个是dpi-1 一个是dpi-2

dpi - 1 跳到 dpi 需要花费 dpi - 1 + costi - 1

dpi - 2 跳到 dpi 需要花费 dpi - 2 + costi - 2

那么究竟是选从dpi - 1跳还是从dpi - 2跳呢?

一定是选最小的,所以dpi = min(dpi - 1 + costi - 1, dpi - 2 + costi - 2);

初始化 dp0 = 0,dp1 = 0,,而且dpi由dpi-1dpi-2推出,所以是从前到后遍历cost数组就可以了

cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        vector<int>dp(cost.size()+1);//定义数组
        dp[0] = 0;//初始化
        dp[1] = 0;
        //从i = 2从前向后遍历
        for(int i = 2;i <= cost.size();i++){
            dp[i] = min(dp[i - 1] + cost[i - 1],dp[i - 2] + cost[i - 2]);//递推公式
        }
        return dp[cost.size()];
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

5.不同路径 (62题)

题目描述:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 "Start" )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 "Finish" )。

问总共有多少条不同的路径?

深搜:

cpp 复制代码
class Solution {
private:
    int dfs(int i, int j, int m, int n) {
        if (i > m || j > n) return 0; // 越界了
        if (i == m && j == n) return 1; // 找到一种方法,相当于找到了叶子节点
        return dfs(i + 1, j, m, n) + dfs(i, j + 1, m, n);
    }
public:
    int uniquePaths(int m, int n) {
        return dfs(1, 1, m, n);
    }
};

**动态规划:**dpij :表示从(0 ,0)出发,到(i, j) 有dpij条不同的路径。

想要求dpij,只能有两个方向来推导出来,即dpi - 1j 和 dpij - 1

此时在回顾一下 dpi - 1j 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dpij - 1同理。

那么很自然,dpij = dpi - 1j + dpij - 1,因为dpij只有这两个方向过来。

首先dpi0一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp0j也同理

递推公式dpij = dpi - 1j + dpij - 1,dpij都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。

这样就可以保证推导dpij的时候,dpi - 1j 和 dpij - 1一定是有数值的

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        vector<vector<int>>dp(m,vector<int>(n,0));//定义dp二维数组,大小为mxn
        for(int i = 0;i < m;i++)dp[i][0] = 1;//首先对最上面一行进行初始化为1
        for(int j = 0;j < n;j++)dp[0][j] = 1;//对最右边的一列初始化为1
        //我们沿着从左到右,从上到下遍历的顺序进行遍历
        for(int i = 1;i < m;i++){
            for(int j = 1;j < n;j++){
                dp[i][j] = dp[i-1][j] + dp[i][j-1];//递推公式
            }
        }
        return dp[m-1][n-1];//返回值
    }
};
  • 时间复杂度:O(m × n)
  • 空间复杂度:O(m × n)

数论方法:

在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。

那么有几种走法呢? 可以转化为,给你m + n - 2个不同的数,随便取m - 1个数,有几种取法。

那么这就是一个组合问题了。

cpp 复制代码
class Solution {
public:
    int uniquePaths(int m, int n) {
        long long numerator = 1; // 分子
        int denominator = m - 1; // 分母
        int count = m - 1;
        int t = m + n - 2;
        while (count--) {
            numerator *= (t--);
            while (denominator != 0 && numerator % denominator == 0) {
                numerator /= denominator;
                denominator--;
            }
        }
        return numerator;
    }
};
  • 时间复杂度:O(m)
  • 空间复杂度:O(1)

总结:

动态规划理论基础:动态规划是一个问题有很多子问题时候使用非常适合,其是由上一个状态推到出来的,贪心算法是由局部最优推出全局最优,动态规划五部曲,定义dp数组,初始化,遍历顺序,递推公式,打印

斐波那契数: F(n) = F(n - 1) + F(n - 2)数列需要满足这个条件,我们首先定义dp数组,第i个数的斐波那契数值是dpi,初始化,根据状态转移公式,我们需要知道两个初始值,dp0 = 0,dp1 = 1,根据状态转移公式知道遍历顺序是从前向后遍历,递推公式就是题目条件,注意细节i遍历起始和终止位置

爬楼梯:总共有N个台阶,每一次可以走1或2个台阶,问总共有几种走法能到n台阶,和斐波那契数一个道理,首先设置dp数组,dp代表爬到i层楼梯有dpi种方法,初始化,dp1 = 1,dp2 = 2,从i=3开始遍历,从前向后遍历,dpi = dpi - 1 + dpi - 2递推公式,注意递归的边界问题

使用最小花费爬楼梯:dpi的定义:到达第i台阶所花费的最少体力为dpi,可以有两个途径得到dpi,一个是dpi-1 一个是dpi-2。dpi = min(dpi - 1 + costi - 1, dpi - 2 + costi - 2),初始化 dp0 = 0,dp1 = 0,从前到后遍历,注意i 的遍历范围i=2开始还要注意终止条件。

不同路径:dpij :表示从(0 ,0)出发,到(i, j) 有dpij条不同的路径定义dp数组,dpij,只能有两个方向来推导出来,即dpi - 1j 和 dpij - 1,dpij = dpi - 1j + dpij - 1,首先对最上面一行进行初始化为1,对最右边的一列初始化为1,我们沿着从左到右,从上到下遍历的顺序进行遍历。

相关推荐
To_OC25 分钟前
LC 1 两数之和:面试第一道必考题,暴力解法直接被面试官 pass
javascript·算法·leetcode
鱼鱼不愚与5 小时前
《原来如此 | 第01期:为什么导航软件能预测红绿灯倒计时?》
算法
博客18008 小时前
酷宝的使用方法,超好用的免费界面库,C++、MFC可用
c++·mfc·界面库·库来帮·酷宝
郝学胜_神的一滴10 小时前
CMake 026:属性体系精讲、四大作用域全解 & 实战代码落地
c++·cmake
复杂网络10 小时前
论最小 Agent 计算机的形态
算法
kisshyshy1 天前
🍦 雪糕、食堂、火车厢:三幅漫画吃透栈、队列与链表
javascript·算法
众少成多积小致巨1 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
猿人谷1 天前
不只是 CPU 阈值:STAR 如何用 GAT + Transformer 做容器级自动扩缩容?
人工智能·算法
复杂网络1 天前
Stable Diffusion 视觉大模型微调技术深度调研
算法
复杂网络1 天前
基于 Stable Diffusion 架构的视觉大模型代表性工作与原理深度解析
算法