[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

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

hdu4826(三维DP)

这是一个百度之星的资格赛第四题 题目链接:http://acm.hdu.edu.cn/contests/contest_showproblem.php?pid=1004&cid=500 题意:从左上角的点到右上角的点,每个点只能走一遍,走的方向有三个:向上,向下,向右,求最大值。 咋一看像搜索题,先暴搜,TLE,然后剪枝,还是TLE.然后我就改方法,用DP来做,这题和普通dp相比,多个个向上

hdu1011(背包树形DP)

没有完全理解这题, m个人,攻打一个map,map的入口是1,在攻打某个结点之前要先攻打其他一个结点 dp[i][j]表示m个人攻打以第i个结点为根节点的子树得到的最优解 状态转移dp[i][ j ] = max(dp[i][j], dp[i][k]+dp[t][j-k]),其中t是i结点的子节点 代码如下: #include<iostream>#include<algorithm

动态规划---打家劫舍

题目: 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 思路: 动态规划五部曲: 1.确定dp数组及含义 dp数组是一维数组,dp[i]代表

hdu4865(概率DP)

题意:已知前一天和今天的天气概率,某天的天气概率和叶子的潮湿程度的概率,n天叶子的湿度,求n天最有可能的天气情况。 思路:概率DP,dp[i][j]表示第i天天气为j的概率,状态转移如下:dp[i][j] = max(dp[i][j, dp[i-1][k]*table2[k][j]*table1[j][col] )  代码如下: #include <stdio.h>#include

usaco 1.1 Broken Necklace(DP)

直接上代码 接触的第一道dp ps.大概的思路就是 先从左往右用一个数组在每个点记下蓝或黑的个数 再从右到左算一遍 最后取出最大的即可 核心语句在于: 如果 str[i] = 'r'  ,   rl[i]=rl[i-1]+1, bl[i]=0 如果 str[i] = 'b' ,  bl[i]=bl[i-1]+1, rl[i]=0 如果 str[i] = 'w',  bl[i]=b