3.动态规划.基础

news2024/9/9 4:34:05

3.动态规划.基础

  • 基础理论
    • 背包基础理论
      • 01背包
      • 完全背包
      • 多重背包
  • 题目
    • 1.斐波那契数
    • 2.爬楼梯
    • 3.使用最小花费爬楼梯
    • 4.不同路径
    • 5.不同路径2

基础理论

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。(很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题这些)

状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。对于动态规划问题,我将拆解为如下五步曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化;因为一些情况是递推公式决定了dp数组要如何初始化
  4. 确定遍历顺序;这一点在背包问题中可以体现
  5. 举例推导dp数组;找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的

debug时,发出这样的问题之前,其实可以自己先思考这三个问题:(如果这灵魂三问自己都做到了,基本上这道题目也就解决了)
1.这道题目我举例推导状态转移公式了么?
2.我打印dp数组的日志了么?
3.打印出来了dp数组和我想的一样么?

动态规划解决的经典问题:

  • 背包问题
  • 打家劫舍
  • 股票问题
  • 子序列问题
  • 其他(区间DP,概率DP)

背包基础理论

1.01背包(基础-重中之重) 2.完全背包 3.多重背包(知道含义,解法即可)
在这里插入图片描述

01背包

01背包-二维dp数组
有n种物体数量为1个,每个物品有各自的重量,价值,目前有一个载重量为m的背包,尝试尽可能装满的背包并使价值最大。
在这里插入图片描述
暴力解法:每个物品只有取和不取两种状态,所以可以使用回溯算法枚举所有可能,统计出最大的价值。时间复杂度为O(2^n)
在这里插入图片描述
动态规划:
1.dp数组的意义:dp[i][j]的定义为:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
2.递推公式:1.不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。);2.放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值;因此得到 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3.初始化:dp[0][slice], dp[slice][0]=0——对于0下标的初始化0;对于非0下标的初始化为任意值都可以。
4.遍历顺序:有两层for循环(一个是物品i,一个背包容量j);根据dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])需要上方和左上方的数据,因此哪个先遍历都可以。其实都可以!! 但是先遍历物品更好理解
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的。

// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}
// 完整代码
void test_2_wei_bag_problem1() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagweight = 4;

    // 二维数组
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));

    // 初始化
    for (int j = weight[0]; j <= bagweight; j++) {
        dp[0][j] = value[0];
    }

    // weight数组的大小 就是物品个数
    for(int i = 1; i < weight.size(); i++) { // 遍历物品
        for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

        }
    }

    cout << dp[weight.size() - 1][bagweight] << endl;
}

01背包-滚动数组-一维dp数组
1.dp数组的意义:dp[j]的定义为:放进容量为j的背包,价值总和最大是多少。
2. 递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) ,一个是一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],另一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值
3. 初始化么:dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0;对于非0下标,为了有意义,需要初始化为非零值,又防止dp[j]会覆盖下一层的dp值,因此统一赋值为0即可。
4.遍历顺序:正序遍历 or 倒序遍历,正序遍历时会使得物品会被重复添加-而违背了01背包的规则(因为dp[i]是由dp[i-1]推导出来的)
与二维dp数组不同,二维数组每层数据都是独立的,但在一维dp数组时,数据的更新会利用就使用新的数据,如果使用正序则会使用更新的数值dp[i-1]去更新dp[i];倒叙则能避免这一影响。
一维数组dp只能先遍历物品,再遍历背包;如果遍历顺序发生改变,一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。确实,尽管dp[j]的深入,dp[j-1]的数值还处于初始层的数值。

void test_1_wei_bag_problem() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagWeight = 4;

    // 初始化
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

完全背包

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
在这里插入图片描述

完全背包和01背包很大的差别体现在遍历顺序上:对于01背包——01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量;完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的

但如果题目稍稍有点变化,就会体现在遍历顺序上。如果问装满背包有几种方式的话? 那么两个for循环的先后顺序就有很大区别了,而leetcode上的题目都是这种稍有变化的类型。

最后,又可以出一道面试题了,就是纯完全背包,要求先用二维dp数组实现,然后再用一维dp数组实现,最后再问,两个for循环的先后是否可以颠倒?为什么? 这个简单的完全背包问题,估计就可以难住不少候选人了

// 先遍历背包,再遍历物品
void test_CompletePack(vector<int> weight, vector<int> value, int bagWeight) {

    vector<int> dp(bagWeight + 1, 0);

    for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
        for(int i = 0; i < weight.size(); i++) { // 遍历物品
            if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}
int main() {
    int N, V;
    cin >> N >> V;
    vector<int> weight;
    vector<int> value;
    for (int i = 0; i < N; i++) {
        int w;
        int v;
        cin >> w >> v;
        weight.push_back(w);
        value.push_back(v);
    }
    test_CompletePack(weight, value, V);
    return 0;
}

多重背包

题目链接
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
在这里插入图片描述
对于多重背包,对每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。在摊开M[i]时,比较耗时的步骤是在vector的动态底层扩容上push_back()。(其实这里也可以优化,先把 所有物品数量都计算好,一起申请vector的空间。
另外一个做法是从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。

    vector<int> dp(bagWeight + 1, 0);

    for(int i = 0; i < n; i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            // 以上为01背包,然后加一个遍历个数
            for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
                dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
            }
        }
    }

多重背包在面试中基本不会出现,力扣上也没有对应的题目,大家对多重背包的掌握程度知道它是一种01背包,并能在01背包的基础上写出对应代码就可以了。


题目

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.dp数组的意义:dp[i]的定义为:第i个数的斐波那契数值是dp[i];
2.递推公式:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]
3.初始化:dp[0] = 0, dp[1]=1
4.遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的。
5.打印DP数据:

	// 动态规划
    int fib(int n) {
        if(n<=1) return n;
        int dp[2];
        dp[0] = 0;
        dp[1] = 1;
        for(int i=2; i<=n; i++){
            int cur = dp[0]+dp[1];
            dp[0] = dp[1];
            dp[1] = cur;
        }
        return dp[1];
    }
	// 递归
    int fib(int n) {
        if(n<=1) return n;
        return fib(n-1)+fib(n-2);
    }

2.爬楼梯

(题目链接)

    int climbStairs(int n) {
        if(n<=1) return n;
        int dp[2];
        dp[0] = 1;
        dp[1] = 1;
        for(int i=2; i<=n; i++){
            int cur = dp[0]+dp[1];
            dp[0] = dp[1];
            dp[1] = cur;
        }
        return dp[1];
    }

3.使用最小花费爬楼梯

(题目链接)
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。示例 2:输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1];输出:6;解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。

    int minCostClimbingStairs(vector<int>& cost) {
        if(cost.size()<=1) return 0;
        int dp[2];
        dp[0] = 0;
        dp[1] = 0;
        for(int i=2; i<=cost.size(); i++){
            int cur = min(dp[0]+cost[i-2], dp[1]+cost[i-1]);
            dp[0] = dp[1];
            dp[1] = cur;
        }
        return dp[1];
    }

4.不同路径

(题目链接)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
在这里插入图片描述
1.dp数组的意义:dp[i]的定义为
2.递推公式:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]
3.初始化:dp[0] = 0, dp[1]=1
4.遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的。

	// 动态规划法
    int uniquePaths(int m, int n) {
        std::vector<std::vector<int>> dp(m, std::vector<int>(n, 0));
        for(int i=0; i<m; i++) dp[i][0] = 1;
        for(int j=0; j<n; j++) dp[0][j] = 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];
    }
    // 动态规划-改善内存
    int uniquePaths(int m, int n) {
        std::vector<int> dp(n,1);
        for(int i=1; i<m; i++){
            for(int j=1; j<n; j++){
                dp[j] = dp[j-1] + dp[j];
            }
        }
        return dp[n-1];
    }
    // 数论-分布理论-两数相乘存在溢出的情况:求组合的时候,要防止两个int相乘溢出!
    int uniquePaths(int m, int n) {
        long long res = 1;
        int denominator = m-1;
        for(int i=0; i<m-1; i++){
            res *= (m+n-2-i);
            while(denominator!=0 && res%denominator == 0){
                res /= denominator;
                denominator--;
            }
        }
        return res;
    }

5.不同路径2

(题目链接)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
在这里插入图片描述

    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
        if(obstacleGrid[0][0]==1) return 0;
        std::vector<int> dp(obstacleGrid[0].size());
        // 初始化第一行
        for(int i=0; i<dp.size(); i++){
            if(obstacleGrid[0][i]==1) dp[i] = 0;
            else if(i==0) dp[i] = 1;
            else dp[i] = dp[i-1];
        }

        for(int i=1; i<obstacleGrid.size(); i++){
            for(int j=0; j<dp.size(); j++){
                if(obstacleGrid[i][j]==1) dp[j]=0;
                else if(j!=0) dp[j] = dp[j]+dp[j-1];
            }
        }
        return dp.back();
    }

时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度;空间复杂度:O(m)
与题4的区别是遇到障碍dp[i][j]保持0就可以了;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1918316.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

外卖霸王餐系统有什么推荐的

​ 在当今数字化的商业环境中&#xff0c;各种创新的营销策略层出不穷&#xff0c;其中微客云霸王餐系统以其独特的商业模式和营销策略&#xff0c;受到了众多商家的青睐。该系统不仅为商家提供了一个高效的营销工具&#xff0c;还通过一系列的功能和优势&#xff0c;帮助商家…

Qt QChart 曲线图表操作

学习目标&#xff1a;QChart 曲线图表操作 学习内容 QT中的QChart类提供了一个功能强大的图表绘制框架,可以根据需求方便高效地绘制各种类型的图表,主要特点如下: 支持多种常见图表类型,如线图、条形图、饼图、散点图等各种类型。开发者只需要选择合适的图表类和数据即可绘制…

Android APT实战

Android开发中,注解平时我们用的比较多,也许我们会比较好奇,注解的背后是如何工作的,这篇文章帮大家一步步创建一个简单的注解处理器。 简介 APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。…

【Linux】常见指令收官权限理解

tar指令 上一篇博客已经介绍了zip/unzip指令&#xff0c;接下来我们来看一下另一个关于压缩和解压的指令&#xff1a;tar指令tar指令&#xff1a;打包/解包&#xff0c;不打开它&#xff0c;直接看内容 关于tar的指令有太多了&#xff1a; tar [-cxtzjvf] 文件与目录 ...…

2.4G芯片开发的遥控玩具方案介绍 东莞酷得

玩具从早期的简单功能&#xff0c;到现如今各种各样的智能操作&#xff0c;发展的速度也是飞速的。随着玩具市场的逐步完善与推进&#xff0c;中国的智能玩具市场也出现了很多远程遥控玩具。遥控玩具也是从最初的有线到现在的无线&#xff0c;从地上跑的到天上飞的&#xff0c;…

jmeter分布式(四)

一、gui jmeter的gui主要用来调试脚本 1、先gui创建脚本 先做一个脚本 演示&#xff1a;如何做混合场景的脚本&#xff1f; 用211的业务比例 ①启动数据库服务 数据库服务&#xff1a;包括mysql、redis mysql端口默认3306 netstat -lntp | grep 3306处于监听状态&#xf…

LeetCode 88.合并两个有序数组 C写法

LeetCode 88.合并两个有序数组 C写法 思路&#xff1a; ​ 由题nums1的长度为mn&#xff0c;则我们不需要开辟新的数组去存储元素。题目要求要有序合并&#xff0c;于是可以判断哪边数更大&#xff0c;将更大的数尾插在nums1中。 ​ 定义三个变量来控制下标&#xff0c;end1控…

Linux--线程ID封装管理原生线程

目录 1.线程的tid&#xff08;本质是线程属性集合的起始虚拟地址&#xff09; 1.1pthread库中线程的tid是什么&#xff1f; 1.2理解库 1.3phtread库中做了什么&#xff1f; 1.4线程的tid&#xff0c;和内核中的lwp 1.5线程的局部存储 2.封装管理原生线程库 1.线程的tid…

四川赤橙宏海商务信息咨询有限公司抖音电商服务靠谱吗?

在数字化浪潮席卷全球的今天&#xff0c;电商行业蓬勃发展&#xff0c;各种新兴电商平台层出不穷。其中&#xff0c;抖音电商以其独特的社交属性和庞大的用户基础&#xff0c;迅速崛起为行业新星。四川赤橙宏海商务信息咨询有限公司&#xff0c;作为专注于抖音电商服务的佼佼者…

自动编码器(Autoencoders)

在“深度学习”系列中&#xff0c;我们不会看到如何使用深度学习来解决端到端的复杂问题&#xff0c;就像我们在《A.I. Odyssey》中所做的那样。我们更愿意看看不同的技术&#xff0c;以及一些示例和应用程序。 1、引言 ① 什么是自动编码器&#xff08;AutoEncoder&#xff…

【js/ts】js/ts高精度加减乘除函数

加法 /*** 高精度加法函数&#xff0c;处理字符串或数字输入&#xff0c;去除尾部多余的零* param {string|number} a - 被加数* param {string|number} b - 加数* returns {string} - 计算结果&#xff0c;去除尾部多余的零* throws {Error} - 如果输入不是有效的数字&#x…

油罐车的罐体结构介绍

油罐车的罐体一般用优质低碳钢板制成&#xff0c;罐内被隔板分为前、后两部分&#xff0c;相互隔离。每个舱内部均配备一道防波板&#xff0c;以加强罐体的稳定性并减缓行驶中油料对罐体的冲击。其车身由车架、车厢等组成&#xff0c;车架是整个油罐车的骨架&#xff0c;承载着…

项目收获总结--本地缓存方案选型及使用缓存的坑

本地缓存方案选型及使用缓存的坑 一、摘要二、本地缓存三、本地缓存实现方案3.1 自己编程实现一个缓存3.2 基于 Guava Cache 实现本地缓存3.3 基于 Caffeine 实现本地缓存3.4 基于 Encache 实现本地缓存3.5 小结 四、使用缓存的坑4.1 缓存穿透4.2 缓存击穿4.3 缓存雪崩4.4 数据…

浏览器输入URL后的过程

总体流程&#xff1a; 1. 用户输入URL并按下回车 当用户在浏览器的地址栏中输入一个 URL 并按下回车&#xff0c;浏览器开始解析用户输入并判断这是一个合法的 URL。 2. DNS 解析 缓存查找&#xff1a;浏览器首先查看本地 DNS 缓存中是否有对应的 IP&#xff0c;如果有则直接…

分布式系统—存储ceph部署

目录 一、存储概述 1 单机存储设备 2 单机存储的问题 3 商业存储解决方案 4 分布式存储 二、ceph概述 1 ceph优点 2 ceph架构 3 ceph核心组件 4 OSD存储后端 5 ceph数据存储过程 6 ceph版本发行生命周期 7 ceph集群部署 三、基于 ceph-deploy 部署 Ceph 集群 1 …

多位冒充者曝光!全域外卖官方到底是谁?

当前&#xff0c;全域外卖的热度持续攀升&#xff0c;不少创业者在寻找入局途径的同时&#xff0c;也开始打听与全域外卖官方有关的各种信息。其中&#xff0c;以全域外卖官方是谁为代表的身份类信息更是成为了多个创业者群内的热议问题。 而就目前的讨论情况来看&#xff0c;虽…

hyperworks软件许可优化解决方案

Hyperworks软件介绍 Altair 仿真驱动设计改变了产品开发&#xff0c;使工程师能够减少设计迭代和原型测试。提升科学计算能力扩大了应用分析的机会&#xff0c;使大型设计研究能够在限定的项目时间完成。现在&#xff0c;人工智能在工程领域的应用再次改变了产品开发。基于物理…

晋升业内新宠儿,MoE模型给了AI行业两条关键出路

文 | 智能相对论 作者 | 陈泊丞 今年以来&#xff0c;MoE模型成了AI行业的新宠儿。 一方面&#xff0c;越来越多的厂商在自家的闭源模型上采用了MoE架构。在海外&#xff0c;OpenAI的GPT-4、谷歌的Gemini、Mistral AI的Mistral、xAI的Grok-1等主流大模型都采用了MoE架构。 …

回收站清空了怎么恢复回来?8个数据恢复方法汇总分享!

在日常工作中&#xff0c;我们常常会遇到一个令人头痛的问题&#xff1a;回收站清空了怎么恢复回来&#xff1f;这种情况其实比想象中更常见。有时在整理桌面时可能会不小心彻底清理文件&#xff0c;或者误开启了回收站的自动清理功能&#xff0c;甚至可能因为病毒或bug而意外丢…

dxf数据结构

DXF&#xff08;Drawing Exchange Format&#xff0c;绘图交换格式&#xff09;是Autodesk公司开发的一种CAD&#xff08;计算机辅助设计&#xff09;文件格式&#xff0c;用于实现AutoCAD与其他软件之间的CAD数据交换。DXF格式文件是一种开放的矢量数据格式&#xff0c;具有多…