算法第十四期——动态规划(DP)初入门

news2024/11/26 5:55:45

目录

DP初步:状态转移与递推

最少硬币问题

DP基础

DP的两个特征

DP:记忆化

图解DP求解过程

最经典的DP问题:0/1背包

模板题:小明的背包

DP状态设计

DP状态转移方程(重点)

 代码

空间优化:滚动数组

(1)交替滚动(两行)

 (2)自我滚动(一行)


DP初步:状态转移与递推

最少硬币问题

有多个不同面值的硬币(任意面值),数量不限,输入金额s,输出最少硬币组合。

例:硬币面值1、2、5。支付13元,要求硬币数量最少 

例:硬币面值1、2、4、5、6。支付9元,要求硬币数量最少 

回顾用贪心求解硬币问题,但只能得到局部最优解,不能得到全局最优。

硬币问题的正解是动态规划!

DP基础

type = [1,5,10,25,50]       # 5种面值
定义数组Min[ ]记录最少硬币数量:
对输入的某个金额i,Min[i]是最少的硬币数量。(把Min[ ]叫做“状态”
第一步:只考虑1元面值的硬币
 

Min[ ]的变化叫做“状态转移

  •  i=1元时,等价于: i= i-1=0元需要的硬币数量,加上1个1元硬币

 继续,所有金额仍然都只用1元硬币

  • i=2元时,等价于: i= i-1 =1元需要的硬币数量,加上1个1元硬币
  • i=3元时...
  • i=4元时...

在1元硬币的计算结果基础上,再考虑加上5元硬币的情况。从i=5开始就行了:

  • i=5元时,等价于:

(1) i = i-5 =0元需要的硬币数量,加上1个5元硬币。Min[5]=1。

(2)原来只使用1元硬币:Min[5]=5。

取(1) (2)的最小值,所以Min[5]=1。

  •   i=6元时,等价于:

(1) i = i - 5= 1元需要的硬币数量,加上1个5元硬币。Min[6] = 2

(2)原来只使用1元硬币:Min[6] = 6
取(1)(2)的最小值,所以Min[6] = 2

  •  i=7元时,…
  •  i=8元时,…

用1元和5元硬币,结果:

递推关系:        Min[i]= min(Min[i], Min[i - 5]+ 1 )              状态转移方程
 【代码】

def solve(s):
    Min = [int(1e12)]*(s+1)         # 初始化为无穷大
    Min[0] = 0
    for j in range(cnt) :           # 5种硬币
        for i in range(type[j], s+1):
            Min[i] = min(Min[i],Min[i - type[j]] +1)    # 递推关系
    print (Min[s])
cnt = 5                             # 5种硬币
type = [1,5,10,25,50]               # 5种面值
s = int(input())
solve(s)

 递推关系:min(Min[ i ],Min[ i - type[ j ] ] +1)

习惯上把状态命名为dp[ ] ,所以应该把Min[ ]改成dp[ ]。

DP的两个特征

(1)重叠子问题。子问题是原大问题的小版本,计算步骤完全一样;计算大问题的时候,需要多次重复计算小问题。
        一个子问题的多次计算,耗费了大量时间。用DP处理重叠子问题,每个子问题只需要计算一次,从而避免了重复计算,这就是DP效率高的原因。

(2)最优子结构。首先,大问题的最优解包含小问题的最优解;其次,可以通过小问题的最优解推导出大问题的最优解,例如100块钱的最小硬币数包括90块钱的最小硬币数,100块钱的最小硬币数是由90块钱的最小硬币数推导过来的。

DP:记忆化

  • 如果各个子问题不是独立的,如果能够保存已经解决的子问题的答案,在需要的时候再找出已求得的答案,可以避免大量的重复计算
  • 基本思路:用一个表记录所有已解决的子问题的答案,不管该问题以后是否被用到,只要它被计算过,就将其结果填入表中。

图解DP求解过程

 

最经典的DP问题:0/1背包

  • 给定n种物品和一个背包,物品i的重量是w_i,其价值为v_i,背包的容量为C。
  • 背包问题:选择装入背包的物品,使得装入背包中物品的总价值最大。
  • 如果在选择装入背包的物品时,对每种物品i只有两种选择:装入背包不装入背包,称为0/1背包问题

x_i表示 物品i 装入背包的情况,
x_i=0,表示 物品i 没有被装入背包

x_i=1,表示 物品i 被装入背包
 

        首先讲一下暴力法求解:遍历所有情况,n个物品的话是2^n种情况,因为每个物品有放入和不放入两种情况。 如果n>=1000的话可以用暴力法解决,如果n>1000那只能使用动态规划来求解。

例:有5个物品,重量分别是{2,2,6,5,4},价值分别为{6,3,5,4,6},背包的容量为10。物品个数为n,容量为C,定义一个(n+1)×(C+1)的二维表dp[ ][ ](两个+1是因为0行和0列我们是不需要的)

dp[i][j]表示把前 i 个物品装入容量为 j 的背包中获得的最大价值。

 填表:按只放第1个物品、只放前2个、只放前3个....一直到放完,这样的顺序考虑。(从小问题扩展到大问题)
1、只装第1个物品。(表格横向是递增的背包容量)

 

2、只装前2个物品。
如果第2个物品重量比背包容量大,那么不能装第2个物品,情况和只装第1个一样。 

如果第2个物品重量小于等于背包容量,那么:

(1)如果把物品2装进去(重量是2),那么相当于只把1装到(容量-2)的背包中。

 

两个都装得下,就把两个都装下去。 

需要用到前面的结果,即已经解决的子问题的答案。 

(2)如果不装2,那么相当于只把1装到背包中。

两个都装得下,但选择只把1装到背包中。

最后取(1)和(2)的最大值。 

3、只装前3个物品。

如果第3个物品重量比背包大,那么不能装第3个物品,情况和只装第、2个一样。例如容量在1~5的时候情况和和只装第、2个一样。

如果第3个物品重量小于等于背包容量,那么:

(1)装3:重量是6,那么相当于只把1、2装到(容量-6)的背包中。

 (2)不装3:那么相当于只把1、2装到背包中。一取(1)和(2)的最大值。

按这样的规律一行行填表,直到结束。

装了那些物品:看每 i 行同一容量:如果价值和上一行相等,则没有装物品 i ,否则装了物品 i 。看最后一列,15>14,说明装了物品5,否则价值不会变化。

 DP的复杂度:O(n*c),n是物品数,c是容量。

模板题:小明的背包

小明的背包l lanqiao0J题号1174

题目描述

小明有一个容量为 C 的背包。

这天他去商场购物,商场一共有 N 件物品,第 i 件物品的体积为 c_i,价值为w_i

小明想知道在购买的物品总体积不超过 C 的情况下所能获得的最大价值为多少,请你帮他算算。

输入描述

输入第 1 行包含两个正整数 N,C,表示商场物品的数量和小明的背包容量。

第 2∼N+1 行包含 2 个正整数c,w,表示物品的体积和价值。

1≤N≤10^2,1≤C≤10^3,1≤w_i​,c_i≤10^3。 

输出描述

输出一行整数表示小明所能获得的最大价值。

输入输出样例

输入

5 20
1 6
2 5
3 8
5 15
3 3 

输出

37

DP状态设计

DP状态:定义二维数组dp[ ][ ],大小为N×C。
dp[i][j]:前i个物品(从第1个到第 i 个)选择性装入容量为 j 的背包中获得的最大价值
把每个dpli][j]看成一个背包:背包容量为j,装1~i这些物品。最后得到的dp[N][C]就是问题的答案:把N个物品装进容量C的背包的最大价值。

DP状态转移方程(重点)

递推计算到dp[i][j],分2种情况:
(1)第 i 个物品的体积比容量 j 还大,不能装进容量 j 的背包。那么直接继承前 i-1 个物品装进容量j的背包的情况即可:dp[i][j] = dp[i-1][j]

(2)第i个物品的体积比容量j小,能装进背包。又可以分为2种情况:装或者不装第i个。

  1. 装第i个。从前i-1个物品的情况下推广而来,前i-1个物品是dp[i-1][j]。第i个物品装进背包后,背包容量减少c[i],价值增加w[i]。有:dp[i][j] = dp[i-1][j-c[i]] + w[i]。(其中dp[i-1][j-c[i]]:用剩下的容量去装最大价值的物品)
  2. 不装第i个。那么直接继承前 i-1 个物品装进容量j的背包的情况: dp[i][j] = dp[i-1][j]
  • 取1)和2)的最大值,状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i])
     

 代码

def solve(n,C):
    # 遍历dp矩阵,填表
    for i in range(1, n+1):
        for j in range (0,C+1):
            if c[i]>j: dp[i][j] = dp[i-1][j]
            else:      dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i])
    return dp[n][C]
N=3011
dp = [[0]*N for j in range(N)]  # 初始化dp
# 或者这样写: dp = [[0 for i in range (N)] for j in range(N)]
w = [0]*N
c = [0]*N
n,C = map(int,input ().split())
for i in range(1, n+1):
    c[i], w[i] = map(int,input ().split())
print(solve(n,C))

空间优化:滚动数组

把dp[ ][ ]优化成一维的dp[ ],以节省空间。
dp[i][ ]是从上面一行dp[i-1]算出来的,第i行只跟第i-1行有关系,跟更前面的行没有关系:
dp[i][j]= max(dp[i - 1][j], dp[i - 1][j - c[i]]+w[i])
优化:只需要两行dp[0][ ]、dp[1][ ],用新的一行覆盖原来的一行,交替滚动
经过优化,空间复杂度从O(N×C)减少为O(C)

(1)交替滚动(两行)

定义dp[ ][i]:用dp[0][ ]和dp[1][ ]交替滚动。

dp[now][j] = max(dp[old][j], dp[old][j-c[i]]+w[i])
优点:逻辑清晰、编码不易出错,建议初学者采用这个方法。

 

例如上图的两行,第一行i-1被第二行的i用完,就用i+1覆盖第一行的i-1,然后第二行的i被i+1用完就用用i+2覆盖第二行的i,以此类推,哪一行用完就用新的一行覆盖。

做法:now始终指向正在计算的最新的一行,old指向已计算过的旧的一行。对照原递推代码,now相当于i,old相当于i-1。

def solve(n, C):
    now = 0;old = 1        # 初始化两行
    for i in range(1, n+1):
        old, now = now, old     # 交换
        for j in range (0,C+1):
            if c[i] >j: dp[now][j] = dp[old][j] # dp[i][j] = dp[i-1][j]
            else:       dp[now][j] = max(dp[old][j], dp[old][j-c[i]]+w[i]) # dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i])
    return dp[now][C]
N = 3011
dp = [[0 for i in range(N)] for j in range(2)]  #注意先后
w = [0]*N
c = [0]*N
n,C = map(int,input().split())
for i in range(1, n+1):
    c[i], w[i] = map(int,input().split())
print(solve(n,C))

 (2)自我滚动(一行)

继续精简:用一个一维的dp[ ]就够了,自己滚动自己。

原式中dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i])的右边两个dp[i-1]是同一个行,所以只需要一行,用之前计算的结果赋值给现在的自己即可,类似于a=a+1。

自我滚动:dp[j] = max(dp[j], dp[j-c[i]]+w[i])

def solve(n,C):
    for i in range(1, n+1):
        for j in range (C,c[i]-1,-1):    # 倒过来
            dp[j] = max(dp[j], dp[j-c[i]]+w[i])
    return dp[C]
N = 3011
dp = [0]*N
w = [0]*N
c = [0]*N
n,C = map(int,input ().split())
for i in range(1,n+1):
    c[i], w[i] = map(int,input().split())
print(solve(n,C))

为什么容量C是从大到小循环呢?

j从小往大循环是错误的 

 例如i = 2时,左图的dp[5]经计算得到dp[5] = 9,把dp[5]更新为9。
右图中继续往后计算,当计算dp[8]时,得dp[8] = dp[5]’+3= 9+3 = 12,这个答案是错的。
错误的产生是滚动数组重复使用同一个空间引起的。

j从大到小循环是对的

例如i = 2时,首先计算最后的dp[9] = 9,它不影响前面状态的计算。 

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

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

相关文章

【机组组合】基于Benders分解算法解决混合整数规划问题——机组组合问题(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…

【鸟哥杂谈】腾讯云 CentOS8 Linux环境下通过docker安装mysql

忘记过去,超越自己 ❤️ 博客主页 单片机菜鸟哥,一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2023-01-15 ❤️❤️ 本篇更新记录 2023-01-15 ❤️🎉 欢迎关注 🔎点赞 👍收藏 ⭐️留言📝&#x1f64…

Vite中如何更好的使用TS

TS 是JS的一个类型检查工具,检查我们代码中可能会存在的一些隐形问题;同时可以使我们的编译器具备一些语法提示功能。 如果我们使用create-vue(vue3官方脚手架工具)创建了项目,该项目基于 Vite 且 TypeScript 已经准备…

SpringBoot+Vue使用easypol出现无法导出Excel表的问题

本篇博文目录1.异常信息2.原因3.解决办法4.详细的SpringBooteasypol前后端分离实现excel导出的步骤1.异常信息 今天在使用easypol导出Excel表的时候,发现能够从后端接口返回数据,但是就是无法导出Excel,控制台输出devicepolicies:1 Uncaught (in promise) error,并且…

2022年度总结 - 明月醉窗台

2022年度总结 - 明月醉窗台 1.2022年成果 - 感谢CSDN提供了自我创作的平台,有多少个夜晚我将其作为心灵的寄托... - 感谢各位小伙伴的抬爱和学习过程中的共同见证,将人生视为一条向上的曲线是明确且坚持不懈的抉择... - 立足当下,不忘脚踏实…

【算法基础】1.5 前缀和与差分

文章目录前缀和题目描述解法讲解二维前缀和题目描述解法讲解差分题目描述解法讲解二维差分题目描述解法讲解前缀和 题目描述 输入一个长度为 n 的整数序列。 接下来再输入 m 个询问,每个询问输入一对 l,r。 对于每个询问,输出原序列中从第 l 个数到第 …

IVD-Net:多模态UNet在MRI中的椎间盘定位和分割

摘要 本文提出了一种多模态磁共振图像中的椎间盘(IVD)定位和分割体系结构,它扩展了UNet。与单一的图像相比,多模态的数据带来了互补的信息有助于更好的数据表示和判别能力。 在本文中,每种MRI模态的数据都以不同的路…

Waf功能、分类与绕过

一. waf工作原理 Web应用防火墙是通过执行一系列针对HTTP/HTTPS的安全策略来专门为Web应用提供保护的一款产品。 常见的系统攻击分为两类: 一是利用Web服务器的漏洞进行攻击,如DDOS攻击、病毒木马破坏等攻击;二是利用网页自身的安全漏洞进…

【C语言】实现通讯录(详解)

目录 一、需要实现的功能 1.1 类型和结构体的定义 二、拆分代码 2.1 游戏菜单 2.1.1 函数调用 2.1.2 函数体的实现 2.1.2运行效果 2.2 初始化结构体 2.2.1 函数调用 2.2.2 函数体的实现 2.2.3 运行结果 2.3 添加联系人信息 2.3.1 函数调用 2.3.2 函数体的实现 2…

操作系统之光--鸿蒙

鸿蒙是什么?鸿蒙包含Openharmony和harmonyOS。Openharmony是华为向开放原子开源基金会捐赠了鸿蒙开源部分的代码,归属于开放原子开源基金会。HarmonyOS是基于Openharmony的商业发行版本。目前大家华为手机上运行就是它。鸿蒙能做什么?很明显&…

【算法基础】1.6 双指针算法

文章目录双指针思想最长连续不重复子序列数组元素的目标和题目讲解判断子序列双指针思想 双指针算法,就是可以将 n ^ 2 优化到 n。 最长连续不重复子序列 给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度…

IO流的学习

文章目录一、File类的使用创建File实例File常用方法总结二、IO流分类读入数据的基本操作步骤(第一种)步骤(第二种)写出数据的基本操作步骤读入并写出数据的基本操作(复制)步骤注意处理流之一:缓…

【Nginx】静态资源部署(下)

文章目录静态资源的缓存处理缓存概述浏览器缓存相关指令expires指令add_header指令Nginx的跨域问题解决同源策略跨域问题跨域问题的案例演示解决方案静态资源防盗链什么是资源盗链Nginx防盗链的实现原理:针对目录进行防盗链静态资源的缓存处理 缓存概述 什么是缓存…

【爪洼岛冒险记】第4站:Java中如何进行控制台输入输出?用Java实现猜数字小游戏;超详细讲解Java中的方法:方法的重载,方法的签名

🌱博主简介:是瑶瑶子啦,一名大一计科生,目前在努力学习JavaSE。热爱写博客~正在努力成为一个厉害的开发程序媛! 📜所属专栏:爪洼岛冒险记【从小白到大佬之路】 ✈往期博文回顾:链接: 【爪洼岛冒…

【文件指针+文件顺序读写操作函数】

1.文件的打开和关闭 1.1 什么是文件指针 2.文件操作函数 2.1 fgetc函数和fputc函数2.2 fgets函数和fputs函数2.3 fscanf函数和fprintf函数2.4 fwrite函数和fread函数 1.文件的打开和关闭 1.1 什么是文件指针? 每个被使用的文件都在内存中开辟了一个相应的文件…

文件操作详解

文章目录前言一、什么是文件?1.程序文件2.数据文件3.文件名二、文件的打开和关闭1.文件指针2.文件的打开和关闭3.打开模式汇总4.例子三.操作函数介绍1.基本函数2.fseek2.ftell3.rewind4.feof尾话前言 恐怕很多人学完了c语言甚至不知道文件操作,也确实&am…

FPGA学习笔记-知识点3-Verilog语法1

1.关键字 2.运算符 按其功能可分为以下几类: 1) 算术运算符(,&#xff0d;,&#xff0c;/,&#xff05;) 2) 赋值运算符(,<) 3) 关系运算符(>,<,>,<) 4) 逻辑运算符(&&,||,!) 5) 条件运算符( ? &#xff1a;) 6) 位运算符(,|,^,&,^) …

【微信小程序】动态设置导航栏标题

&#x1f3c6;今日学习目标&#xff1a;第十八期——动态设置导航栏标题 &#x1f603;创作者&#xff1a;颜颜yan_ ✨个人主页&#xff1a;颜颜yan_的个人主页 ⏰预计时间&#xff1a;25分钟 &#x1f389;专栏系列&#xff1a;我的第一个微信小程序 文章目录前言使用配置文件…

消息中间件简介

UNIX的进程间通信就开始运用消息队列技术&#xff0c;一个进程将数据写入某个特定的队列中&#xff0c;其它进程可以读取队列中的数据&#xff0c;从而实现异步通信。对于如今的分布式系统&#xff0c;消息队列已经演变为独立的消息中间件产品&#xff0c;相比于RPC同步通信的方…

设计模式_行为型模式 -《模板方法模式》

设计模式_行为型模式 -《模板方法模式》 笔记整理自 黑马程序员Java设计模式详解&#xff0c; 23种Java设计模式&#xff08;图解框架源码分析实战&#xff09; 行为型模式用于描述程序在运行时复杂的流程控制&#xff0c;即描述多个类或对象之间怎样相互协作共同完成单个对象都…