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

算法基础篇:(二十一)数据结构之单调栈:从原理到实战,玩转高效解题

算法基础篇:(二十一)数据结构之单调栈:从原理到实战,玩转高效解题

目录 前言 一、什么是单调栈?先打破 “栈” 的常规认知 1.1 单调栈的核心特性 1.2 如何实现一个单调栈? 实现单调递增栈 实现单调递减栈 1.3 核心操作解析:为什么要 “弹出元素”? 二、单调栈能解决什么问题?四大核心场景全覆盖 2.1 场景 1:找左侧最近的 “更大元素” 问题描述 解题思路 代码实现 测试用例验证 2.2 场景 2:找左侧最近的 “更小元素” 问题描述 解题思路 代码实现 测试用例验证 2.3 场景 3:找右侧最近的 “更大元素” 问题描述

By Ne0inhk
【数据结构初阶】排序算法(中)快速排序专题

【数据结构初阶】排序算法(中)快速排序专题

文章目录 * 1. 快排主框架 * 2. 快排的不同实现 * 2. 1 hoare版本 * 2. 2 挖坑法 * 2. 3 lomuto前后指针法 * 2. 4 快排的非递归版本 * 3. 快排优化 * 3. 1 快排性能的关键点分析: * 3. 1 三路划分 * 3. 2 introsort自省排序 1. 快排主框架 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。 其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。 简单地说,就是将数组分成左右两个部分,左部分都大于(或小于)中间的基准值,右部分都小于(或大于)中间的基准值,然后不断重复上述过程,直到数组完全有序。 //快排主框架voidQuickSort(int*

By Ne0inhk
《算法闯关指南:动态规划算法--斐波拉契数列模型》--01.第N个泰波拉契数,02.三步问题

《算法闯关指南:动态规划算法--斐波拉契数列模型》--01.第N个泰波拉契数,02.三步问题

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 01.第N个泰波拉契数 * 解法(动态规划): * 算法流程: * C++算法代码: * 算法总结&&笔记展示: * 02.三步问题 * 解法(动态规划): * 算法思路: * C++算法代码: * 算法总结&&笔记展示: * 结尾: 前言: 聚焦算法题实战,系统讲解三大核心板块:优选算法:剖析动态规划、二分法等高效策略,学会寻找“最优解”。 递归与回溯:掌握问题分解与状态回退,攻克组合、排列等难题。 贪心算法:理解“

By Ne0inhk
Flutter 三方库 matcher 的鸿蒙化适配指南 - 实现具备语义化断言与自定义匹配算法的测试契约框架、支持端侧质量验证的强力抽象实战

Flutter 三方库 matcher 的鸿蒙化适配指南 - 实现具备语义化断言与自定义匹配算法的测试契约框架、支持端侧质量验证的强力抽象实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 matcher 的鸿蒙化适配指南 - 实现具备语义化断言与自定义匹配算法的测试契约框架、支持端侧质量验证的强力抽象实战 前言 在进行 Flutter for OpenHarmony 开发时,当编写单元测试时,我们经常使用 expect(actual, matcher) 这种语法。你是否想过,如何让断言读起来像自然语言一样?或者,如何自定义一套专门针对鸿蒙原生组件状态的对比逻辑?matcher 是 Dart 官方维护的断言库扩展,它定义了测试中所有“匹配逻辑”的底层协议。本文将探讨如何在鸿蒙端构建极致、严谨的质量契约体系。 一、原直观解析 / 概念介绍 1.1 基础原理 该库建立在“谓词逻辑(Predicate Logic)”之上。它通过将复杂的 Object

By Ne0inhk