[Python-闫式DP]

闫式DP分析法

闫老师是将DP问题归结为了有限集合中的最值问题。

动态规划有两个阶段,一是状态表示,二是状态计算。

状态表示 f(i,j)

状态表示是一个化零为整的过程,动态规划的做题思路不是暴力法的每一个物品都去枚举,而是将相似的物品化为一个子集作为一个整体,然后每个整体去枚举。

在状态表示中,我们需要知道f(i, j)代表的是什么集合,也就是动态规划五部曲中的明确dp数组的[i][j]代表的是什么。然后这个集合的属性是什么呢,对应到动规五部曲中就是dp数组的值。属性一般有三种,一般是最大值、最小值以及数量。

状态计算

状态计算就对应了化整为零的过程,是将集合f(i,j)分为若干个子集,划分需要满足两个准则,一是不重复,二是不遗漏。

划分的依据是:寻找最后一个不同点

1.01背包

下面按照卡码网上的例题来试着用闫式DP分析一下。携带研究材料(第六期模拟笔试)

朴素分析

这个题目是要找在行李空间为N时的所带物品的最大价值V,也就是说有限集合中的最大值问题。这样就可以用闫式DP分析法了。

状态表示

先进行状态表示,背包问题是一种选择问题,这类问题集合的第一维一般都是选择前i个物品,后面几维一般是限制。在这道题中,第一维就是i,第二维是所带行李体积的限制j,即f[i][j]。这个集合就是所有考虑前i个物品,且总体积不超过j的选法的集合

然后再来看值,值的选择就要看题目了,题目问的是什么,值就是什么。这个题目中要求最大价值,因此值就为每个集合中的最大值。那么题目所要求的答案就是f[N][V],是什么意思呢?就是从前N个物品中选,在体积小于等于j时的最大价值。

因此,我们明确了f(i, j)为所有考虑前i个物品,且总体积不超过j的选法中的最大值

状态计算

状态计算前面说了是化整为零的过程,我们现在需要求出集合f(i, j)的值,题目中就是求最大值,怎么求呢?一般是把集合划分为不同的子集,怎么划分呢?一般是找最后一个不同点。

这里的不同点其实就是是否选择最后一个物品i。即不选择物品i的所有方案与选择物品i的所有方案两种集合。这里显然不重复也不遗漏。

这样,如果要求集合的最大值的话,我们只需要求出左边集合的最大值与右边集合的最大值,再求出两者的最大值即可。

左边子集的最大值如何求?我们要从定义去求。

已知左边集合代表的是所有不选物品i的集合,也就是说范围为0 ~ (i - 1)<= j,即f(i - 1, j),因为前面已经定义好了这个集合的值就为满足当前条件下的最大价值。

再来看右边子集。

右边子集为所有包含物品i的选法。也就是说范围为0 ~ (i - 1) & i这个意思是说,物品i是一定在选择的物品中的,其余则需要在前i - 1个物品中选。正因为物品i已经固定要选了,所以其前i - 1个物品的体积限制在<= j - weight[i]

因此右边子集的最大值为f(i - 1, j - weight[i]) + value[i]

综上,朴素方法的状态计算为f[i][j] = max(f[i - 1][j], f[i - 1][j - weight[i]] + value[i])

代码实现

分析之后,接下来就是写代码了。

在遍历的过程中,依旧是先物品后背包的顺序进行遍历,同时由于右边子集其实是有使用范围即j >= weight[i]的,因此需要进行if判断,这里简单的写法是,先赋值左边子集的最大值,然后判断,如果在使用范围内,则取两者最大值。代码如下:

M, N = map(int, input().split())
​
weight = [0]
value = [0]
​
weight += list(map(int, input().split()))
value += list(map(int, input().split()))
​
​
f = [[0] * (N + 1) for _ in range(M + 1)]
​
for i in range(1, M + 1):
    for j in range(N + 1):
        f[i][j] = f[i - 1][j]
        if j >= weight[i]:
            f[i][j] = max(f[i][j], f[i - 1][j - weight[i]] + value[i])
​
print(f[M][N])

空间优化

朴素写法的f数组为二维的,但实际上我们需要的只是最后一行,那么就可以压缩一下变为一维数组。

朴素写法的状态计算为f[i][j] = max(f[i - 1][j], f[i - 1][j - weight[i]] + value[i])

如果压缩的话,就应该写成f[j] = max(f[j], f[j - weight[i]] + value[i])

注意这里的右边子集为j - weight[i]是一个小于j的数,我们在朴素写法时新的一行需要从上一行读出,但如果只在一行里更新的话,如果还是正常的从左向右遍历,那么就会使新的值更新到这一行中,我们需要从右向左遍历,才能让新的值不会影响这一行的值。即for j in range(N, weight[i] - 1, -1),这里也恰好保证了在满足右边子集的情况下,当前的最大值等于左边子集与右边子集两者的最大值。当不满足右边子集使用范围的情况下,当前的最大值保持上一行的值不变。

代码如下:

M, N = map(int, input().split())
​
weight = [0]
value = [0]
​
weight += list(map(int, input().split()))
value += list(map(int, input().split()))
​
f = [0] * (N + 1)
​
for i in range(M + 1):
    for j in range(N, weight[i] - 1, -1):
        f[j] = max(f[j], f[j - weight[i]] + value[i])
​
print(f[N])

2.完全背包

完全背包问题与01背包问题的区别在于01背包的每个物品只能取一次,而完全背包的每个物品可以取无数次,下面来推导一下。

状态表示f[i][j]

集合是所有只从前i个物品中选,总体积不超过j的方案的集合

属性就是最大值。

状态计算

由于完全背包是可以无限次选择一个物品的,因此在划分子集时,不同点就在于选择物品i的个数。划分如下:

分割之后的f(i,j)为:(下面的weight数组用W代替,value数组用V代替)

f[i][j] = max(f[i - 1][j], f[i - 1][j - W[i]] + V[i], f[i - 1][j - 2*W[i]] + 2*V[i], ..., f[i - 1][j - k*W[i]] + k*V[i])

这样求显然非常不容易,加上循环的话时间复杂度又上去了。因此可以换一个角度。

f[i][j - W[i]] = max(f[i - 1][j - W[i]], f[i - 1][j - 2*W[i]] + V[i], f[i - 1][j - 2*W[i]] + 2*V[i], ..., f[i - 1][j - k*W[i]] + (k - 1)*V[i])

有点类似于移位相消法,上面的每一项都要比下面的每一项多一个V,所以上面的最大值就等于下面的最大值加一个V,所以f[i][j] = max(f[i - 1][j], f[i][j - W[i]] + V[i])

朴素写法
N, V = map(int, input().split())
w = [0] * (N + 1)
v = [0] * (N + 1)
for i in range(1, N + 1):
    w[i], v[i] = map(int, input().split())
​
f = [[0] * (V + 1) for _ in range(N + 1)]
​
for i in range(1, N + 1):
    for j in range(1, V + 1):
        f[i][j] = f[i - 1][j]
        if j >= w[i]:
            f[i][j] = max(f[i][j], f[i][j - w[i]] + v[i])
​
print(f[N][V])
空间优化
N, V = map(int, input().split())
​
w = [0] * (N + 1)
v = [0] * (N + 1)
​
for i in range(1, N + 1):
    w[i], v[i] = map(int, input().split())
​
f = [0] * (V + 1)
​
for i in range(1, N + 1):
    for j in range(w[i], V + 1):
        f[j] = max(f[j], f[j - w[i]] + v[i])
        
print(f[V])

01背包、完全背包小结

01背包:f[i][j] = max(f[i - 1][j], f[i - 1][j - weight[i]] + value[i])

完全背包:f[i][j] = max(f[i - 1][j], f[i][j - weight[i]] + value[i])

下面再来看看例题。以蓝桥官网中的游戏中的学问为例。

游戏中的学问

题目描述

大家应该都见过很多人手拉手围着篝火跳舞的场景吧?一般情况下,大家手拉手跳舞总是会围成一个大圈,每个人的左手拉着旁边朋友的右手,右手拉着另一侧朋友的左手。

不过,如果每一个人都随机的拉住两个不同人的手,然后再慢慢散开,事情就变得有趣多了——此时大家依旧会形成圈,不过却可能会形成多个独立的圈。当然这里我们依然要求一个人的右手只能拉另一个人的左手,反之亦然。

班里一共有 NN 个同学,由 11 到 NN 编号。Will 想知道,究竟有多少种本质不同的拉手方案,使得最终大家散开后恰好形成 kk 个圈呢?

给定两种方案,若存在一个人和他的一只手,满足在这两种方案中,拉着这只手的人的编号不同,则这两种方案本质不同。

输入描述

输入一行包含三个正整数N,k,PN,k,P。

其中,3≤k≤N≤30003≤k≤N≤3000,104≤p≤2×109104≤p≤2×109。

输出描述

输出一行一个整数,表示本质不同的方案数对 pp 的余数。保证 pp 一定是一个质数。

解题思路

首先先状态表示。

这里设集合为f[i][j],为i个同学围成j个圈的方案集合。属性为方案的总数。

再状态计算。

先要把集合划分,到第i个同学时的不同点就是在已有圈中加入同学i,以及同学i和原来圈中抽出两个同学再组成一个新的圈。再来想想,这种划分方法达到了不重复、不遗漏了吗?为什么原来圈中不能抽出来3个同学与同学i组成一个新的圈呢?

因为如果是抽出三个同学再组成新圈的话,这个其实是与已有圈中加入同学i重复,这时已有的圈肯定会有三个同学围成的圈,这个要考虑清楚。因此划分为在已有圈中加入同学i与在原来圈中抽出2个同学与同学i围成新圈是不重复不遗漏的。

首先来看在已有圈中加入同学i。已知目前有i个同学,那么已有圈中有i - 1个同学,也就是有i - 1个边,我们如果想要加入同学i的话,可以在i - 1个边中加入,因此方案数有(i - 1) * f[i - 1].

再来看一下右边子集,同学i与原来圈中抽出两个同学组成一个新圈。抽出两个同学的话,那么已有圈就只剩下i - 3个同学了,而且要组成一个新圈之后才有j个圈,所以目前是有j - 1个圈的,即f[i - 3][j - 1],从i - 1个同学中不放回的抽取2个同学,一共有C^2(i - 1)个方案数,再组成新圈,要记得组成三个人的新圈也是有两种情况的,所以还要乘以2,因此右边子集的情况就为:f[i - 3][j - 1] * (i - 1) * (i - 2)

要时刻记得集合的含义!这里不是两者求最大,而是求在这两种情况下的方案数之和,才是集合的方案数总和。我们要求的属性是方案数总和!代码如下:

N, k, P = map(int, input().split())
​
f = [[0] * (k + 1) for _ in range(N + 1)]
f[3][1] = 2 #初始状态
​
for i in range(4, N + 1): 
  for j in range(1, k + 1):
    f[i][j] = (((i - 1) * f[i - 1][j]) % P + ((i - 2) * (i - 1) * f[i - 3][j - 1]) % P) % P
​
print(f[N][k]) 

518. 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

解题思路

明显这个题是完全背包。

首先来看状态表示。设dp[i][j]为取前i个硬币下总金额为j的方案总数。

再来看状态计算,将dp[i][j]集合划分,由于是完全背包可以取无限次,因此可以大体分为取i和不取i。不取i的情况就是dp[i - 1][j] ,取i的情况有无限种,但可以转化为dp[i][j - V[i]],具体推导在上面已经说过。

求方案数时,初始化一般都会把dp[0]初始化为1,从而后面才会是有效的次数。

所以集合dp[i][j] = dp[i - 1][j] + dp[i][j - V[i]]

空间优化就是从小到大遍历的dp[j] = dp[j] + dp[j - V[i]]代码如下。

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0] * (amount + 1)
        dp[0] = 1
        for i in range(len(coins)):
            for j in range(coins[i], amount + 1):
                dp[j] = dp[j] + dp[j - coins[i]]
        return dp[-1]

这个题是求的组合数,因此重复的集合会算作一个,遍历顺序就是先物品再背包,这样能够保证集合的唯一性,因为物品在这种遍历下不能颠倒顺序。

377. 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

解题思路

这个题乍一眼看和上面很像,但还是有区别的,在于这里的组合可以颠倒顺序,即排列数。排列数就要要求遍历顺序为先背包后物品,因为这样可以使物品的顺序发生颠倒。下图为例:

其他都一样,代码如下:

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)
        dp[0] = 1
        for i in range(1, target + 1):
            for j in range(len(nums)):
                if i >= nums[j]:
                    dp[i] = dp[i] + dp[i - nums[j]]
        return dp[-1]

排列数、组合数小结

在完全背包问题中,组合数是不管集合中数字的顺序的,按照先物品后背包的遍历顺序;排列数中顺序不同的序列会被视作不同的组合,因此按照先背包后物品的遍历顺序。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

异步解耦之RabbitMQ(二)__RabbitMQ架构及交换机

异步解耦之RabbitMQ(一) RabbitMQ架构 RabbitMQ是一个基于AMQP&#xff08;Advanced Message Queuing Protocol&#xff09;协议的消息代理中间件&#xff0c;它通过交换机和队列实现消息的路由和分发。以下是RabbitMQ的架构图&#xff1a; Producer&#xff08;生产者&#…

Java设计模式-组合模式(13)

大家好,我是馆长!今天开始我们讲的是结构型模式中的组合模式。老规矩,讲解之前再次熟悉下结构型模式包含:代理模式、适配器模式、桥接模式、装饰器模式、外观模式、享元模式、组合模式,共7种设计模式。 组合模式(Composite Pattern) 定义 组合(Composite)模式:又叫…

深度学习与神经网络Pytorch版 3.2 线性回归从零开始实现 1.生成数据集

3.2 线性回归从零开始实现 目录 3.2 线性回归从零开始实现 一 &#xff0c;简介 1. 原理 2. 步骤 3. 优缺点 4. 应用场景 二 &#xff0c;代码展现 1. 生成数据集(完整代码) 2. 各个函数解析 2.1 torch.normal()函数 2.2 torch.matmul()函数 2.3 d2l.plt.scatter(…

【教学类-44-54】20240201 德彪钢笔行书(实线字体)制作的数字描字帖

作品展示 背景需求&#xff1a; 找到了两款适合做数字描字贴的字体 【教学类-44-03】20240111阿拉伯数字字帖的字体&#xff08;三&#xff09;——德彪钢笔行书&#xff08;实线字体&#xff09;和print dashed&#xff08;虚线字体&#xff09;-CSDN博客文章浏览阅读1.1k次…

【HarmonyOS】鸿蒙开发之HTTP网络请求——第5章

HTTP网络请求封装 network/request.ets import { configInterface } from ./type import http from ohos.net.http import { getToken } from ../utils/storage//网络请求封装 export const request (config:configInterface)>{let httpRequest:http.HttpRequest http.c…

༺༽༾ཊ—Unity之-01-工厂方法模式—ཏ༿༼༻

首先创建一个项目&#xff0c; 在这个初始界面我们需要做一些准备工作&#xff0c; 建基础通用文件夹&#xff0c; 创建一个Plane 重置后 缩放100倍 加一个颜色&#xff0c; 任务&#xff1a;使用工厂方法模式 创建 飞船模型&#xff0c; 首先资源商店下载飞船模型&#xff0c…

二进制安全虚拟机Protostar靶场(5)堆的简单介绍以及实战 heap0

前言 这是一个系列文章&#xff0c;之前已经介绍过一些二进制安全的基础知识&#xff0c;这里就不过多重复提及&#xff0c;不熟悉的同学可以去看看我之前写的文章 什么是堆 堆是动态内存分配的区域&#xff0c;程序在运行时用来分配内存。它与栈不同&#xff0c;栈用于静态…

【Vue3+Vite】Vue3视图渲染技术 快速学习 第二期

文章目录 一、模版语法1.1 插值表达式和文本渲染1.1.1 插值表达式 语法1.1.2 文本渲染 语法 1.2 Attribute属性渲染1.3 事件的绑定 二、响应式基础2.1 响应式需求案例2.2 响应式实现关键字ref2.3 响应式实现关键字reactive2.4 扩展响应式关键字toRefs 和 toRef 三、条件和列表渲…

农业植保无人机行业研究:预计2025年市场规模可达115亿元

农业植保无人机行业市场投资前景现状如何?农业植保无人机市场&#xff0c;包括无人机自身技术、性能标准和植保标准。农业植保无人机应用植保机喷洒农药对我国而言&#xff0c;不仅具有很大的经济价值&#xff0c;还具有社会价值&#xff1a;农业植保机作业不仅有超高的工作效…

并网逆变器学习笔记8---平衡桥(独立中线模块)控制

参考文献&#xff1a;《带独立中线模块的三相四线制逆变器中线电压脉动抑制方法》---赵文心 一、独立中线模块的三相四线拓扑 独立中线模块是控制中线电压恒为母线一半&#xff0c;同时为零序电流ineu提供通路。不平衡负载的零序电流会导致中线电压脉动&#xff0c;因此需要控制…

【Android 字节码插桩】Gradle插件基础 Transform API的使用

前言 啪~我给大家开个会&#xff08;手机扔桌子上&#xff09; 什么叫做 客户无感的数据脱敏&#xff01;&#xff1f; 师爷给翻译翻译什么叫做客户无感的数据脱敏&#xff1f; 什么特么的叫做客户无感数据脱敏&#xff1f; 举个栗子~ 客户端Sdk新升级了一个版本&#xff0c;增…

UnityShader(九)Unity中的基础光照(下)

目录 标准光照模型 自发光 高光反射 &#xff08;1&#xff09;Phong模型 &#xff08;2&#xff09;Blinn模型 漫反射 环境光 逐顶点还是逐像素 逐像素光照 逐顶点光照 总结 标准光照模型 光照模型有许多种&#xff0c;但在早期游戏引擎中&#xff0c;往往只使用一…

linux -- 并发 -- 并发来源与简单的解决并发的手段

互斥与同步 当多个执行路径并发执行时&#xff0c;确保对共享资源的访问安全是驱动程序员不得不面对的问题 互斥&#xff1a;对资源的排他性访问 同步&#xff1a;对进程执行的先后顺序做出妥善的安排 一些概念&#xff1a; 临界区&#xff1a;对共享的资源进行访问的代码片段…

1、缓存击穿背后的问题

当面试官问&#xff1a;你知道什么是缓存击穿吗&#xff0c;你们是如何解决的&#xff1f; 首先我们要了解什么是缓存击穿&#xff1f;以及缓存击穿会引发什么问题&#xff1f; 缓存击穿就是redis中的热点数据过期&#xff0c;缓存失效&#xff0c;导致大量的请求直接打到数据…

【免费分享】数据可视化-银行动态实时大屏监管系统,含源码

一、动态效果展示 1. 动态实时更新数据效果图 ​ 2. 鼠标右键切换主题 二、确定需求方案 1. 屏幕分辨率 这个案例的分辨率是16:9&#xff0c;最常用的的宽屏比。 根据电脑分辨率屏幕自适应显示&#xff0c;F11全屏查看&#xff1b; 2. 部署方式 B/S方式&#xff1a;支持…

使用了不受支持的协议。 ERR_SSL_VERSION_OR_CIPHER_MISMATCH的问题解决办法

windwos 2008 R2 使用IIS部署的项目申请使用https协议的时候&#xff0c;通过安全加密协议访问网站提示不受支持的协议 错误原因分析 这种错误通常表示客户端和服务器之间存在协议版本或加密套件不兼容导致在SSL&#xff08;Secure Socket Layer&#xff09; 1.协议版本不兼容&…

壹[1],Xamarin开发环境配置

1&#xff0c;环境 VS2022 注&#xff1a; 1&#xff0c;本来计划使用AndroidStudio&#xff0c;但是也是一堆莫名的配置让人搞得很神伤&#xff0c;还是回归C#。 2&#xff0c;MAUI操作类似&#xff0c;但是很多错误解来解去&#xff0c;且调试起来很卡。 3&#xff0c;最…

哪个牌子的头戴式耳机好?推荐性价比高的头戴式耳机品牌

随着科技的不断发展&#xff0c;耳机市场也呈现出百花齐放的态势&#xff0c;从高端的奢侈品牌到亲民的平价品牌&#xff0c;各种款式、功能的耳机层出不穷&#xff0c;而头戴式耳机作为其中的一员&#xff0c;凭借其优秀的音质和降噪功能&#xff0c;受到了广大用户的喜爱&…

C++文件操作(2)

文件操作&#xff08;2&#xff09; 1.二进制模式读取文本文件2.使用二进制读写其他类型内容3.fstream类4.文件的随机存取文件指针的获取文件指针的移动 1.二进制模式读取文本文件 用二进制方式打开文本存储的文件时&#xff0c;也可以读取其中的内容&#xff0c;因为文本文件…

Flask 入门3:Flask 请求上下文与请求

1. 前言 Flask 在处理请求与响应的过程&#xff1a; 首先我们从浏览器发送一个请求到服务端&#xff0c;由 Flask 接收了这个请求以后&#xff0c;这个请求将会由路由系统接收。然后在路由系统中&#xff0c;还可以挂入一些 “勾子”&#xff0c;在进入我们的 viewFunction …