动态规划 线性 DP 经典四题一遍吃透

动态规划 线性 DP 经典四题一遍吃透

文章目录


线性dp 是动态规划问题中最基础、最常⻅的⼀类问题。它的特点是状态转移只依赖于前⼀个或前⼏个状态,状态之间的关系是线性的,通常可以⽤⼀维或者⼆维数组来存储状态。 我们在⼊⻔阶段解决的《下楼梯》以及《数字三⻆形》其实都是线性dp,⼀个是⼀维的,另⼀个是⼆ 维的。

台阶问题

题目描述

在这里插入图片描述


题目解析

本题就是上一节下楼梯的问题的加强版,总体思路不变,下面我们还是按照动规5板斧来分析一下这道题。
1、状态表示
dp[i]表示走到第i个台阶的所有方案数
2、状态转移方程
第i个台阶的方案数等于从i-1阶到i-k阶的所有方案数之和,因为本题数据比较大,用long long都无法保证数据不越界,所以题目规定方案数还需要模100003,第i个台阶的方案数等于从i-1阶到i-k阶的所有方案数之和再模上100003,所以但是注意是可能越界访问的,比如i为3,k为10,i-k是可能访问到数组负下标的,所以访问台阶时需要保证i-k始终大于等于0。
dp[i] = (dp[i] + dp[i - j])% 100003(j从1到k)。
3、初始化
本题大家可能会首先想到先将数组前k个数据初始化,但是这样操作比较麻烦,小编介绍一个新方法,直接将dp[0]置为1即完成初始化操作,然后从dp[1]开始往后循环计算方案数。
4、填表顺序
从左往后
5、输出结果
f[n]即为结果

代码

#include<iostream>usingnamespace std;typedeflonglong LL;constint N =1e5+10, MOD =1e5+3;int n, k; LL dp[N];intmain(){ cin >> n >> k;//初始化 dp[0]=1;//循环填表for(int i =1; i <= n; i++){for(int j =1; j <= k; j++){//保证不越界访问if(i - j <0)break; dp[i]=(dp[i]+ dp[i - j])% MOD;}}//输出结果 cout << dp[n]<< endl;return0;}

最大子段和

题目描述

在这里插入图片描述

题目解析

本题小编介绍一种在面对子序列和子数组问题时定义状态表示的技巧。
1、状态表示
以某个位置为结尾,结合题意来定义状态表示。
以本题为例,f[i]表示以i位置为结尾的所有子数组中,最大和是多少。
在这里插入图片描述
2、状态转移方程
推导状态转移方程我们还是根据最后一步来划分情况。
首先明确本题数据是从数组下标1开始存储的,那么如果n为1,也就是只有一个数据,那么最大子段就是它本身,所以f[i] = a[i]。
如果n大于1,第i个格子的最大子段和有两种可能:
第一最大子段和是它本身a[i],因为第i个格子之前的最大子段和可能是负数。
f[i] = a[i]
第二最大子段和是第i个格子之前的所有子段中的最大子段加上第i个格子的数据,第i个格子之前的所有子段中的最大子段加就是下图中红框中左右子段的最大值,红框中每一行表示一个子段。
f[i] = f[i - 1] + a[i]
在这里插入图片描述
那么总的来说第i个格子的最大子段和就是两种可能中取较大值:
f[i] = max(a[i], f[i - 1] + a[i]) 3、初始化
当填第一个格子f[1]时,根据状态转移方程:f[1] = max(a[1], f[0] + a[1]),而根据我们之前的分析,第一个格子的值就是它本身:f[i] = a[i],所以只要我们把f[0]初始化为0,即可满足要求。
4、填表顺序
从左往右
5、输出结果
结果为f数组中的最大值

代码

#include<iostream>#include<algorithm>usingnamespace std;constint N =2e5+10;int n;int a[N], f[N];intmain(){//处理输入 cin >> n;for(int i =1; i <= n; i++){ cin >> a[i];}//初始化 f[0]=0;//按序填表for(int i =1; i <= n; i++){ f[i]=max(a[i], a[i]+ f[i -1]);}//输出结果int ret =-0x3f3f3f3f;for(int i =1; i <= n; i++){ ret =max(ret, f[i]);} cout << ret << endl;return0;}
//优化版(无a数组)#include<iostream>#include<algorithm>usingnamespace std;constint N =2e5+10;int n;int f[N];intmain(){//处理输入 cin >> n;//初始化 f[0]=0;//按序填表+更新retint ret =-0x3f3f3f3f;for(int i =1; i <= n; i++){int x; cin >> x; f[i]=max(x, x + f[i -1]); ret =max(ret, f[i]);}//输出结果 cout << ret << endl;return0;}

传球游戏

题目描述

在这里插入图片描述

题目解析

1、状态表示
首先我们要知道某个格子中的信息是从第一个格子开始到该格子一共有多少种方案。
然后分析该格子应该包含哪些信息,因为题目规定所有同学站成一个圆圈,所以球可能从左边格子来也可能从右边格子来,要传递球所以必然需要知道该格子编号和左边、右边格子的编号,所以格子应该包含编号信息。然后本题还有一个条件:传球次数,也就说我还需要知道球传递了多少次后落在了谁手里,所以还应该包含传球次数信息。所以我们这里用一个一维数组是存不下两个信息的,所以我们可以用一个二维数组f[i][j]。
f[i][j]表示球传递了i次之后,最终落在了j同学手里一共有多少种方案。
2、状态转移方程
本题推导状态转移方程需要分类讨论,因为题目规定同学围成一个圈,但是我们要用二维线性数组存储数据,所以需要分三种情况:
第一种是接受球的是编号为1的同学,此时传递球的同学编号可能是2和n,所以编号为1的同学的传球方案数就是2和n同学的方案数之和:
dp[i][1] = dp[i - 1][2] + dp[i - 1][n]
第二种是接受球的是编号为从2到n - 1的同学,假设为j,此时传递球的同学编号可能是j - 1和j + 1,所以编号为1的同学的传球方案数就是j - 1和j + 1同学的方案数之和:
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1]
第三种是接受球的是编号为n的同学,此时传递球的同学编号可能是1和n - 1,所以编号为n的同学的传球方案数就是1和n - 1同学的方案数之和:
dp[i][n] = dp[i - 1][1] + dp[i - 1][n - 1]
在这里插入图片描述
小编再补充解释一下本题的代码实现和二维数组填表顺序,总的来说是是从上往下按顺序一行一行的填,行表示传球次数,列表示同学编号,因为同学编号是从1开始的,所以第0列没有意义,第一行表示传球次数为0,此时根据题目规定球在一号同学手里,dp[0][1]就表示球传递了0次最后落在1号同学手里一共有多少种方案,所以dp[0][1]是一个合法格子,那么第一行除了一号同学其他同学的格子就没有意义了,这时我们需要思考把第一行一号同学初始化为0还是1,根据我们前面分析的状态转移方程,填格子都需要累加上一行两个格子的方案数,如果我们把dp[0][1]初试化为0,那么后面所有格子累加的结果都为0,所以我们要把dp[0][1]初始化为1,这里我们可以总结一下,对于这类求方案数的情况,因为一般情况状态转移方程都是累加的,所以一般会将初试情况初始化为1。
在这里插入图片描述
3、初始化
f[0][1] = 1
4、填表顺序
从上往下依次填每一行
5、输出结果
f[m][1]

代码

#include<iostream>usingnamespace std;constint N =40;int n, m;//n个同学,传递m次int dp[N][N];//次数,同学编号intmain(){ cin >> n >> m;//初始化 dp[0][1]=1;//按序填表//从1开始枚举传递次数(枚举行)for(int i =1; i <= m; i++){//填第1个同学 dp[i][1]= dp[i -1][2]+ dp[i -1][n];//填第2到n-1个同学//从2开始枚举同学编号(枚举列)for(int j =2; j < n; j++){ dp[i][j]= dp[i -1][j -1]+ dp[i -1][j +1];}//填第n个同学 dp[i][n]= dp[i -1][1]+ dp[i -1][n -1];}//输出结果 cout << dp[m][1]<< endl;return0;}

乌龟棋

题目描述

在这里插入图片描述

题目解析

先分析一下题意,本题有点类似我们玩过的飞行棋,投骰子决定走多少步,一共有6种情况(1-6步),本题是抽卡片决定走多少步,一共有4种情况(1-4步),并且本题格子是一维的,走到某个格子即可得到该格子的分数,题目要求我们找出从起点走到终点格子的最大分数。
1、状态表示
本题dp数组每个格子要存储六个信息:走到x格子时,用1卡片i张、用2卡片j张、用3卡片k张、用4卡片z张的情况下的最大分数,所以我们需要一个五维数组dp存储,但其实一维数组下标x可以通过另外4个变量计算出来,因为走到x格子本质是从下标1开始,用了i张1卡片、j张2卡片、k张3卡片、z张4卡片后走到的位置:
x = 1 + i + 2 * j + 3 * k + 4 * z
所以我们用一个四维dp数组存储,需要用到x时通过另外4个变量计算得到。
2、状态转移方程
推导状态转移方程本质就是推导dp[i][j][k][z],根据我们前面的分析,dp[i][j][k][z]对应的一维数组下标是x,那么走到x一共有四种可能:分别是从x-1到x、x-2到x、x-3到x、x-4到x,对于x-1来说,它对于的dp数组是dp[i - 1][j][k][z],同理对于x-2来说,dp数组是dp[i][j - 1][k][z]… 下面示意图是以从x-3走到x为例。
在这里插入图片描述
但是这里有个细节,需要处理边界情况,对于变量i j k z来说,它们表示抽取各自卡的张数,对i来说,取值范围是从0到cnt[1](cnt数组中cnt[1] cnt[2] cnt[3] cnt[4]有意义,各自表示1卡片的张数、2卡片的张数、3卡片的张数、4卡片的张数),对于j来说,取值范围是从0到cnt[2],k z同理,所以i j k z取值是非负的,所以要访问dp[i - 1][j][k][z]时需要保证i大于0,要访问dp[i][j - 1][k][z]时需要保证j大于0,k、z同理。
所以状态转移方程如下所示,本质我们上面的推导是基于最后一步分析的:
在这里插入图片描述
在代码实现中会用4个for循环依次枚举i j k z的所有取值,枚举一种i j k z的取值情况就会将四种可能的状态庄毅方程作比较:dp[i - 1][j][k][z]、dp[i][j - 1][k][z]、dp[i][j][k - 1][z]、dp[i][j][k][z - 1],取出最大值作为dp[i][j][k][z]的取值。
4、初始化
题目规定乌龟棋子自动获得起点格子的分数,所以dp[0][0][0][0] = a[1]
5、输出结果
dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]]

代码

#include<iostream>#include<algorithm>usingnamespace std;constint N =360;constint M =50;int n, m;//存储棋盘 存储四张卡片各自数目 dp数组int a[N], cnt[5], dp[M][M][M][M];intmain(){//处理输入 cin >> n >> m;for(int i =1; i <= n; i++){ cin >> a[i];}while(m--){int x =0; cin >> x; cnt[x]++;}//初始化 dp[0][0][0][0]= a[1];//按顺序填表for(int i =0; i <= cnt[1]; i++){for(int j =0; j <= cnt[2]; j++){for(int k =0; k <= cnt[3]; k++){for(int z =0; z <= cnt[4]; z++){//x为当前访问格子下标(数组a下标)int x =1+ i +2* j +3* k +4* z;int& t = dp[i][j][k][z];if(i >0) t =max(t, dp[i -1][j][k][z]+ a[x]);if(j >0) t =max(t, dp[i][j -1][k][z]+ a[x]);if(k >0) t =max(t, dp[i][j][k -1][z]+ a[x]);if(z >0) t =max(t, dp[i][j][k][z -1]+ a[x]);}}}}//输出结果 cout << dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]]<< endl;return0;}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

Read more

FPGA入门:CAN总线原理与Verilog代码详解

FPGA入门:CAN总线原理与Verilog代码详解

目录 一、CAN 总线核心原理 1. 物理层特性 2. 协议层核心概念 (1)位时序 (2)帧结构(标准数据帧) (3)关键机制 二、FPGA 实现 CAN 的核心模块 三、Verilog 代码实现(以 50MHz 时钟、1Mbps 波特率为例) 1. 全局参数定义 2. 位时序模块(CAN Bit Timing Generator) 3. CRC 计算模块(CAN CRC Generator) 4. 发送模块(CAN Transmitter) 5. 接收模块(CAN Receiver)

By Ne0inhk
汇川机器人软件RobotLab常规操作

汇川机器人软件RobotLab常规操作

一.权限管理注意事项 1.1 软件登录权限管理 连接上软件后,修改轴参数、点位数据需要权限。点击人物图标,登录对应的权限,管理员权限登录密码6个0。 1.2机器人控制权限管理 点击“锁”,打开机器人控制权配置页面。 选择“InoRoboLabt”,机器人受编程软件控制,使用软件可手动移动点位、示教位置信息。 选择“远程IO单元”,机器人受外部设备控制如PLC、上位机,机器人进入自动模式,收到交互信号就按照程序执行。 选择“远程以太网客户端”,机器人受远程客户短控制,用于查找问题、远程调试。 二、 使用过渡点注意事项 程序中点到点直线运动会有机构干涉或有安全风险时,使用过渡点在运动规避风险。 使用过渡点时,注意指令的工具坐标系,选择正确的Wobj工具好,否则运动出错有撞机风险。 如下图所示为例,wobj0为A工位,wobj1为B工位,注意在“轴控制面板”中选择对应工具坐标号 三、使用全局点位移动注意事项 双击左侧“P.

By Ne0inhk
Formality:原语(primitive)的概念

Formality:原语(primitive)的概念

相关阅读 Formalityhttps://blog.ZEEKLOG.net/weixin_45791458/category_12841971.html?spm=1001.2014.3001.5482         原语(primitive)一般指的是语言内置的基本构件,它们代表了基本的逻辑门和构件,通常用于建模电路的基本功能,例如Verilog中的门级建模会使用and、or等关键词表示单元门。Formality也存在原语的概念,这一般出现在对门级网表进行建模时,本文将对此进行详细解释。         假设以例1所示的RTL代码作为参考设计(可以看出添加了// synopsys sync_set_reset综合指令让Design Compiler将其实现为带同步复位端的D触发器),例2所示的综合后网表作为实现设计,其中data_out_reg原语是一个带同步复位端的D触发器(FDS2)。 // 例1 module ref( input clk, input reset, input data_in, output reg data_

By Ne0inhk
DAY4 基于 OpenClaw + 飞书开放平台实现 AI 新闻推送机器人

DAY4 基于 OpenClaw + 飞书开放平台实现 AI 新闻推送机器人

DAY4 基于 OpenClaw + 飞书开放平台实现 AI 新闻推送机器人 目录 DAY4 基于 OpenClaw + 飞书开放平台实现 AI 新闻推送机器人 前  言 1 环境准备 1.1 华为云开发环境 1.2 ModelArts 代金券与模型服务 1.3 启动 OpenClaw 网关 2 飞书开放平台配置 2.1 创建企业自建应用 2.2 添加机器人能力 2.3 配置应用权限 2.4 发布应用版本 3 OpenClaw 与飞书集成 3.1 配置 OpenClaw

By Ne0inhk