[dp] 动态规划学习路线与笔记 | 动态规划习题集

2024-02-13 11:58

本文主要是介绍[dp] 动态规划学习路线与笔记 | 动态规划习题集,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文链接:https://www.yuque.com/cppdev/algo/tyahkd
【学习路线】

  1. 对动态规划有一个初步的了解,只看问题引入的部分:什么是动态规划?动态规划的意义是什么?
  2. B站的UP主动态规划入门课,通俗易懂:动态规划第一讲
  3. 动态规划第二讲的第一题
  4. 一位博主,总结的很不错:告别动态规划,DP连刷 40 道题,我总结出了这些套路!这应该是把动态规划讲的最后的文章了
  5. 动态规划第二讲第二题
  6. 二维dp问题:01背包问题

文章目录

  • dp引入:换钱(一维dp)
  • 外卖小哥在一段时间内赚最多的钱(一维dp)
  • 选出一组不相邻的数字且和最大(一维dp)
  • [总结] DP的解题经验
  • 能不能凑出正整数s(二维dp)
  • 01背包问题(二维dp)
  • dp有哪些类型

dp引入:换钱(一维dp)

原文链接:什么是动态规划?动态规划的意义是什么?)

【问题】你想把某宝里的666块换成现金,而且想换来的钱的总数最少,因为你的钱包快装不下了

【生活中的做法】从最大的面额开始换,以此可以获得数量最少的钞票,共10张。这种思想叫贪心,每经过一次,它会尽量让数值变得更小

666 = 6 ∗ 100 + 1 ∗ 50 + 1 ∗ 10 + 1 ∗ 5 + 1 ∗ 1 666=6*100+1*50+1*10+1*5+1*1 666=6100+150+110+15+11

【贪心的方法是对的吗?】如果钱的面额够科学,贪心就是对的。但是遇到这种情况:一个国家的钞票面额只有1、5、11。那我们这次用贪心来换15块

  1. 15 = 1 ∗ 11 + 4 ∗ 1 15=1*11+4*1 15=111+41,用贪心换得5张钞票
  2. 15 = 3 ∗ 5 15=3*5 15=35,但最少的情况是这个,换得3张钞票

【贪心错在哪里?】鼠目寸光,只考虑了眼前的情况

【解决方案】我们用f(n)表示换n块所需的最小钞票的数量,那我们就有以下的情况

  1. f ( 1 ) = 1 f(1)=1 f(1)=1:换1块钱,就只要一张1块
  2. f ( 4 ) = 4 f(4)=4 f(4)=4:换4块钱,要4张1块
  3. f ( 5 ) = 1 f(5)=1 f(5)=1:换5块钱,就只要1张5块
  4. f ( 10 ) = 2 f(10)=2 f(10)=2:换10块钱,要2张5块
  5. f ( 11 ) = 1 f(11)=1 f(11)=1:换11块钱,就只需要1张11块

理解的话,我们来换一下15块

  1. c o s t ( 15 ) = f ( 11 ) + f ( 4 ) = 1 + 4 = 5 cost(15)=f(11)+f(4)=1+4=5 cost(15)=f(11)+f(4)=1+4=5:这种策略要5张
  2. c o s t ( 15 ) = f ( 5 ) + f ( 10 ) = f ( 5 ) + f ( 5 ) + f ( 5 ) = 3 cost(15)=f(5)+f(10)=f(5)+f(5)+f(5)=3 cost(15)=f(5)+f(10)=f(5)+f(5)+f(5)=3:这种策略要3张
  3. c o s t ( 15 ) = f ( 1 ) + f ( 14 ) = 1 + 4 = 5 cost(15)=f(1)+f(14)=1+4=5 cost(15)=f(1)+f(14)=1+4=5:这种策略只要5张

那我们要的就是3个中最小的情况,于是有公式:

f(n)=min{ f(n-1), f(n-5), f(n-11) } +1

有了这个公式怎么暴力求解呢?

int f_0(int n) { //用递归暴力求解int mincost = 65535;if (n==0) return 0;if (n>=1) mincost = min(mincost, f_0(n-1)+1);if (n>=5) mincost = min(mincost, f_0(n-5)+1);if (n>=11) mincost = min(mincost, f_0(n-11)+1);printf("f[n]=%d\n", n, mincost);return mincost;
}

此过程会生成一个递归树,由于最小面额为1,所以树的高度为n,则把树当成满二叉树来算结点总数 2 n − 1 2^n-1 2n1,故此方法的时间复杂度为 O ( 2 n ) O(2^n) O(2n)

在这里插入图片描述

改良1:用数组保存之前的计算结果,避免重复计算,复杂度O(n)。这就是dp,暂时可以把dp理解成用数组记录历史记录的递归或递推

int min(int a, int b) {return a>b?b:a;
}
int f_1(int n) { int i, mincost, f[105];f[0]=0;for (i=1; i<=n; ++i) { //递推的方式:从下往上算mincost = 65535;if (i>=1) mincost = min(mincost, f[i-1]+1);if (i>=5) mincost = min(mincost, f[i-5]+1);if (i>=11) mincost = min(mincost, f[i-11]+1);f[i]=mincost;}return f[n];
}f[105]={0};		 //把每次计算的值保存下来
int f_2(int n) { //递归的方式:从上往下找需要算的内容int mincost = 65535;if (n==0) return 0;if (n>=1) mincost = min(mincost, f[n-1]==0 ? f_2(n-1)+1: f[n-1]+1 );//看f[n-1]之前有没有算过(有个装逼名字叫:记忆化搜索)if (n>=5) mincost = min(mincost, f[n-5]==0 ? f_2(n-5)+1: f[n-5]+1 );if (n>=11) mincost = min(mincost, f[n-11]==0 ? f_2(n-11)+1: f[n-11]+1 );f[n]=mincost;printf("f[%d]=%d\n", n, f[n]); //看到底计算了谁return mincost;
}

对比:暴力<动态规划<贪心(从效率和使用条件的角度)

  1. 【贪心】上面所提,贪心的问题在于鼠目寸光,但只要面额设置的足够科学,贪心是对的。这可以说明,使用贪心的条件是很苛刻严格的,要经过严格的数学证明,才能够确定根据局部最优所得的结果对不对。
  2. 【贪心vs动态规划】但对于动态规划。它在哪种面额下都是可以的,这可以说明,动态规划(dp)的条件相比于贪心是更不苛刻的,正是因为贪心更严格,所以贪心的效率更高
  3. 【动态规划vs暴力】从上面可以看到,动态规划没有重复计算、也减去了没有必要的计算内容。减去不必要的计算任务,我们叫剪枝。也就是说动态规划自带剪枝,这是动态规划优于暴力的地方。

看到这里,相信你对DP有初步的了解。如果对自己没有信心,不要立刻看DP的严格定义。继续看一些例子,加深对它的感性认识。直到你对它有所感悟的时候,而且感悟到达极点的时候,再去看别人的总结,会恍然大悟,瞬间打通所有。

外卖小哥在一段时间内赚最多的钱(一维dp)

链接:动态规划 (第1讲)

题目:外卖小哥想在11个小时内赚最多的钱

  1. 横条前的数字:第i个外卖单子,一共有8个单子
  2. 横条的长度:做该单需要花费的时间段
  3. 横条上的红色数字:完成该单的报酬
    在这里插入图片描述

【思路】选与不选
【抽象问题】OPT[i]:完成前i项单子,赚的最多的钱

  1. OPT[1]:完成前1项单子,最多能赚5块(完成第一单)
  2. OPT[2]:完成前2项单子,最多能赚5块(只做第一单)
  3. OPT[3]:完成前3项单子,最多能赚8快(只做第三单)
  4. OPT[4]:完成前4项单子,最多能赚8块(只做第三单)
  5. OPT[8]:怎么算?

【OPT[8]】完成前8项单子赚的最多的钱,那就有两种可能:对于第8项单子,我是做,还是不做

  1. 做第8个单子:然后8-11的时间段就不能做别的了,所以只能做8之前的单子,这里记做pre[8]
  2. 不做第8个单子:那就是OPT[7]

然后在两种情况下找到最大值,即是OPT[8],于是有以下公式:
在这里插入图片描述

【pre[]】pre数组怎么求呢?根据每个单子的结束时间来确定

【最终答案】
在这里插入图片描述

选出一组不相邻的数字且和最大(一维dp)

链接:动态规划第二讲第一题

题目:在一个数组arr中,找出一组不相邻的数字,使得最后的和最大
在这里插入图片描述

#include<stdio.h>
int max(int a, int b) {return a>b?a:b;
}int dp_1(int arr[], int n) { //非递归int opt[105];int i;//初态opt[0]=arr[0];opt[1]=max(arr[0], arr[1]);//通式(选、不选中的最大值):opt[i] = max{ opt[i-1], arr[i]+opt[i-2] }for (i=2; i<n; ++i) {opt[i] = max( opt[i-1], arr[i]+opt[i-2] );printf("opt[%d]=%d\n", i, opt[i]);}return opt[n-1];
}int opt[105] = {0}; //保存记录
int dp_2(int arr[], int n) { //递归int i=n-1; //arr从0开始存储if (opt[i]!=0) return opt[i]; //计算过了if (i==0) {opt[i]=arr[0];return opt[i];} else if (i==1) {opt[i]=max(arr[0], arr[1]);return opt[i];} else {if (opt[i-1]==0) opt[i-1] = dp_2(arr, n-1);if (opt[i-2]==0) opt[i-2] = dp_2(arr, n-2);opt[i] = max( opt[i-1], arr[i]+opt[i-2] );printf("opt[%d]=%d\n", i, opt[i]);return opt[i];}
}int main() {int arr[105] = {1, 2, 4, 1, 7, 8, 3};printf("%d", dp_2(arr, 7) ); //前7个数字return 0;
}

[总结] DP的解题经验

原文链接:告别动态规划,DP连刷 40 道题,我总结出了这些套路!这应该是把动态规划讲的最后的文章了

【步骤一】确定数组

  1. 梳理问题的条件
  2. 确定数组维度:一般影响因子有几个,数组就有几个维度
  3. 准确定义数组的含义

【步骤二】找出数组元素之间的关系

  1. 从末态找
  2. 分析问题的过程

【步骤三】找出初始值,即出口(从初态找)

  • 找初始值容易出错,有时候容易找的不完整,所以不能仅局限于在数组元素中下标>=0的程度,要多考虑几位

【注意】

  1. 下标问题:下标一般存1-n,0可以作为边界检查、也不容易搞乱

能不能凑出正整数s(二维dp)

链接:动态规划第二讲第二题

【题目】给定一个正整数s, 判断一个数组arr中,是否有一组数字加起来等于s
在这里插入图片描述

【第一步】确定数组

  1. 确定问题:①选数字;②数字和等于S;③问题:能不能选出这么一组数字
  2. 数组维数:影响因子是①数字之和、②单个数字 --> 二维dp
  3. 定义数组含义(即问题):Subset[k][s]:从前k个元素中选一组数字,它们的和是s,subset[k][s]等于1表示能选出来

【第二步】确定元素间的关系
从后往前,当前数字是选还是不选,只要其中一个能凑出,就返回true
在这里插入图片描述
【第三步】找初始值,也就是出口

  1. s==0:空集就好了,这时候返回true
  2. k==0:只有0可以选了,如果这时候第0个数值刚好是s,则返回true,否则返回false

【代码】

#include<stdio.h>
#include<string.h>// 从arr[]中的前k个元素中,选择一组元素,它们的和刚好等于s,则返回1
int dp[105][105]; //数组
int dp_rec(int arr[], int k, int s) { //递归int val1, val2;if (dp[k][s]!=-1) return dp[k][s]; //有历史记录,直接返回// 递归出口if (s==0) dp[k][s]=0;else if (k==0) dp[k][s] = arr[0]==s;// 通式else if (arr[k]>s) //选了第k个就超出了s,那不选dp[k][s] = dp_rec(arr, k-1, s);	else {val1 = dp_rec(arr, k-1, s); //不选val2 = dp_rec(arr, k-1, s-arr[k]); //选第k个dp[k][s] =  val1 || val2;}printf("dp[%d][%d]=%d\n", k, s, dp[k][s]);return dp[k][s];
}int main() {int i,j;// 测试int n=6;int s=9;int arr[6] = {3, 34, 4, 12, 5, 2}; //0到n-1memset(dp, -1, sizeof(dp));printf("\n可以凑到吗? %d\n", dp_rec(arr, n-1, s) );for (i=0; i<n; i++) {for (j=0; j<n; j++)printf("%d\t", dp[i][j]);printf("\n");}return 0;
}

01背包问题(二维dp)

视频链接:01背包问题
链接:01背包在线计算器

【题目】有n个物品及它们的重量和价格,问小偷的背包只能装下20kg,怎么装偷的最多?
在这里插入图片描述
【步骤1】确定数组

  1. 确定问题:①选物品;②物品的总重量≤W;③问题:求物品的总价值最大
  2. 数组维数:影响因子是①物品的总重量、②单个物品的重量 --> 二维dp
  3. 定义数组含义(即问题):B[k][w]:在前k个商品中选取一组商品,重量≤w,物品总价值最大为B[k][w]

【步骤2】找出数组元素的关系
在这里插入图片描述

【步骤3】找初始值

B[0][0]->B[0][w](第一行)都为0:没有物品时,最大价值为0
B[0][0]->B[k][0](第一列)都为0:有物品,没有包来装,最大价值也是0

B(k,w)数组的值:
在这里插入图片描述
【代码】

#define N 5   //5个商品
#define W 20  //背包重量
int B[N+1][W] = {0};
int w[N+1] = {0, 2,3,4,5,9}; //从1-N开始记录
int v[N+1] = {0, 3,4,5,8,10}; //从1-N开始记录
void Knapsack(int B[N+1][], int w[], int v[]) {int i,j;int val1, val2;// 初始值(其实不用赋予初始值,B[][]数组定义时初始值即为0)for (j=0; j<=W; ++j) B[0][j]=0; //第一行for (i=0; i<=N; ++i) B[i][0]=0; //第一列// 递推for (i=1; i<=N; ++i) { //考虑前k个物品for (j=1; j<=W; ++j) { //背包的重量if ( w[i] > j ) { //第i件物品太重,背包已经装不下了B[i][j] = B[i-1][j]; //不考虑第i件} else {val1 = B[i-1][j];    //不选第i件val2 = B[i-1][j-w[i]]+v[i]; //选第i件B[i][j] = val1>val2 ? val1 : val2;}}}
}

dp有哪些类型

dp有哪些种类

这篇关于[dp] 动态规划学习路线与笔记 | 动态规划习题集的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C#如何动态创建Label,及动态label事件

《C#如何动态创建Label,及动态label事件》:本文主要介绍C#如何动态创建Label,及动态label事件,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#如何动态创建Label,及动态label事件第一点:switch中的生成我们的label事件接着,

SpringCloud动态配置注解@RefreshScope与@Component的深度解析

《SpringCloud动态配置注解@RefreshScope与@Component的深度解析》在现代微服务架构中,动态配置管理是一个关键需求,本文将为大家介绍SpringCloud中相关的注解@Re... 目录引言1. @RefreshScope 的作用与原理1.1 什么是 @RefreshScope1.

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

mybatis-plus 实现查询表名动态修改的示例代码

《mybatis-plus实现查询表名动态修改的示例代码》通过MyBatis-Plus实现表名的动态替换,根据配置或入参选择不同的表,本文主要介绍了mybatis-plus实现查询表名动态修改的示... 目录实现数据库初始化依赖包配置读取类设置 myBATis-plus 插件测试通过 mybatis-plu

Java进阶学习之如何开启远程调式

《Java进阶学习之如何开启远程调式》Java开发中的远程调试是一项至关重要的技能,特别是在处理生产环境的问题或者协作开发时,:本文主要介绍Java进阶学习之如何开启远程调式的相关资料,需要的朋友... 目录概述Java远程调试的开启与底层原理开启Java远程调试底层原理JVM参数总结&nbsMbKKXJx

基于Canvas的Html5多时区动态时钟实战代码

《基于Canvas的Html5多时区动态时钟实战代码》:本文主要介绍了如何使用Canvas在HTML5上实现一个多时区动态时钟的web展示,通过Canvas的API,可以绘制出6个不同城市的时钟,并且这些时钟可以动态转动,每个时钟上都会标注出对应的24小时制时间,详细内容请阅读本文,希望能对你有所帮助...

Vue中动态权限到按钮的完整实现方案详解

《Vue中动态权限到按钮的完整实现方案详解》这篇文章主要为大家详细介绍了Vue如何在现有方案的基础上加入对路由的增、删、改、查权限控制,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、数据库设计扩展1.1 修改路由表(routes)1.2 修改角色与路由权限表(role_routes)二、后端接口设计

前端 CSS 动态设置样式::class、:style 等技巧(推荐)

《前端CSS动态设置样式::class、:style等技巧(推荐)》:本文主要介绍了Vue.js中动态绑定类名和内联样式的两种方法:对象语法和数组语法,通过对象语法,可以根据条件动态切换类名或样式;通过数组语法,可以同时绑定多个类名或样式,此外,还可以结合计算属性来生成复杂的类名或样式对象,详细内容请阅读本文,希望能对你有所帮助...

Nginx实现动态封禁IP的步骤指南

《Nginx实现动态封禁IP的步骤指南》在日常的生产环境中,网站可能会遭遇恶意请求、DDoS攻击或其他有害的访问行为,为了应对这些情况,动态封禁IP是一项十分重要的安全策略,本篇博客将介绍如何通过NG... 目录1、简述2、实现方式3、使用 fail2ban 动态封禁3.1 安装 fail2ban3.2 配