动态规划 线性 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

为什么我的OpenClaw安装后无法启动?Gateway服务故障排查全攻略

为什么我的OpenClaw安装后无法启动?Gateway服务故障排查全攻略

为什么我的OpenClaw安装后无法启动?Gateway服务故障排查全攻略 1. 引言 OpenClaw是一款功能强大的自动化工具,但其安装和运行依赖于多个服务组件,其中Gateway服务是核心组件之一。如果Gateway服务无法启动,整个OpenClaw系统将无法正常运行。本文将详细介绍OpenClaw安装后无法启动的常见原因及故障排查方法,帮助你快速定位并解决问题。 2. Gateway服务简介 Gateway服务是OpenClaw的核心组件,负责: * 处理所有API请求 * 管理服务间的通信 * 提供认证和授权 * 处理负载均衡 * 监控系统状态 因此,Gateway服务的正常运行对于OpenClaw至关重要。 3. 常见故障原因 3.1 端口冲突 症状:Gateway服务启动失败,提示端口被占用 原因: * 其他应用正在使用Gateway服务的默认端口(通常为3000) * 之前的OpenClaw进程未完全关闭 解决方案: 1. 查看端口占用情况:

By Ne0inhk
【MySQL飞升篇】分库分表避坑指南:垂直分库vs水平分表,分片键选对才不踩雷

【MySQL飞升篇】分库分表避坑指南:垂直分库vs水平分表,分片键选对才不踩雷

🍃 予枫:个人主页 📚 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》 💻 Debug 这个世界,Return 更好的自己! 引言 当业务数据量突破千万、亿级门槛,单库单表的性能瓶颈会如期而至——查询卡顿、写入超时、扩容困难,每一个问题都足以让后端开发者头大。分库分表(Sharding)作为核心解决方案,却常常让人陷入纠结:垂直分库和水平分表该怎么选?分片键选错会有什么后果?分表后分布式ID、跨库分页、跨库JOIN这些难题又该如何破解?本文从核心概念到实战难题,带你吃透分库分表全流程策略。 文章目录 * 引言 * 一、分库分表核心认知:为什么必须做? * 1.1 单库单表的性能瓶颈根源 * 1.2 分库分表的两大核心方向 * 二、核心拆分策略:垂直分库 vs 水平分表实战 * 2.1 垂直分库:按业务“瘦身”

By Ne0inhk

Flutter 三方库 theme_tailor_annotation 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、多终端一致的主题架构实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 theme_tailor_annotation 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、多终端一致的主题架构实战 在鸿蒙(OpenHarmony)生态的开发中,面对手机、平板、折叠屏及智慧屏等多种屏幕形态,维护一套既美观又严谨的主题系统(Theme System)是一大挑战。传统的 ThemeData 扩展往往冗长且易错。theme_tailor_annotation 为鸿蒙开发者提供了一种基于注解和代码生成的极致主题定义方案。本文将带您领略其架构之美。 前言 什么是 Theme Tailor?它是一套强大的代码生成工具。theme_tailor_annotation 定义了其核心注解(Annotations)。通过这种方式,开发者只需定义一个简单的类,库就会自动生成处理深浅色模式切换、多终端缩放比例及组件级动态样式的样板代码(Boilerplate)。在追求高颜值、高性能的鸿蒙应用实践中,

By Ne0inhk
【案例实战】鸿蒙分布式智能办公应用的架构设计与性能优化

【案例实战】鸿蒙分布式智能办公应用的架构设计与性能优化

目录 一、项目背景与挑战 项目概述 1.1 面临的技术挑战 二、分布式架构设计 2.1 整体架构概览 2.2 组件化设计 2.3 分布式通信机制 三、性能优化实战 3.1 UI渲染优化 3.1.1 虚拟列表实现 3.1.2 懒加载和预加载策略 3.2 内存管理优化 3.2.1 内存泄漏检测与修复 3.2.2 对象池与资源复用 3.3 启动性能优化 四、鸿蒙开放能力接入 4.1 云开发能力集成

By Ne0inhk