动态规划!?这些题刷完,不可能学不会

前言

今天是我的编程千人社群 —— 【英雄算法联盟】创办的第 1137 天,目前人数 5600+ 人,星球目前主营业务为 零基础答疑、C、C++、Python 三个语言的编程教学、数据结构和算法的集训、游戏开发、面试求职、简历辅导、职场答疑、大厂内推、人脉触达、认知提升。

星友提问:动态规划怎么学?

提问链接:https://t.zsxq.com/eoCMx
在这里插入图片描述

在这里插入图片描述
刷题之前务必先看相关的动态规划教程:基础算法 && 进阶算法
题集链接:https://t.zsxq.com/JhTlu

一、动态规划基础认知

  动态规划核心就是把计算过的问题存储在数组或者哈希表中,下次进行访问的时候,可以 O(1) 的获取,从而加速计算过程。
  动态规划处理的是有向无环图问题。

二、线性 DP 相关题型

1. 递推(26题)

  递推是动态规划中一种较为基础且直观的思想方法。它从已知的初始条件出发,通过一定的规则逐步推导出后续的状态。在这 26 道递推相关题目中,往往需要我们仔细观察题目中的数字或状态变化规律,建立起相邻状态之间的联系。例如,在经典的斐波那契数列问题中,我们通过 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n - 1)+f(n - 2) f(n)=f(n1)+f(n2) n > 2 n>2 n>2 f ( 1 ) = 1 f(1)=1 f(1)=1 f ( 2 ) = 1 f(2)=1 f(2)=1)的递推公式,就能轻松计算出数列中任意一项的值。这种自底向上的求解方式,是递推类题目最显著的特征。

2. 最大子段和(20 题)

  最大子段和问题聚焦于在一个给定的数列中,找出连续的子段,使其元素之和达到最大。在这 20 道题目中,常见的解法是利用动态规划的思想,定义状态 d p [ i ] dp[i] dp[i]表示以第 i i i个元素结尾的最大子段和。状态转移方程为 d p [ i ] = m a x ( d p [ i − 1 ] + a r r [ i ] , a r r [ i ] ) dp[i]=max(dp[i - 1]+arr[i], arr[i]) dp[i]=max(dp[i1]+arr[i],arr[i]),即要么将当前元素加入到前一个位置的最大子段和中,要么以当前元素作为新的子段起点。通过遍历整个数列,不断更新 d p dp dp数组,最终得到的最大值就是我们所求的最大子段和。

3. 最长单调子序列(32 题)

  最长单调子序列问题要求在给定序列中找出最长的单调递增或单调递减子序列。对于这 32 题,我们可以定义状态 d p [ i ] dp[i] dp[i]表示以第 i i i个元素结尾的最长单调子序列长度。在寻找状态转移关系时,需要遍历 i i i之前的所有元素 j j j,若满足单调条件(如递增时 a r r [ j ] < a r r [ i ] arr[j]<arr[i] arr[j]<arr[i]),则更新 d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) dp[i]=max(dp[i], dp[j]+1) dp[i]=max(dp[i],dp[j]+1) 。通过这种方式,我们能够系统地计算出每个位置对应的最长单调子序列长度,进而找出整个序列中的最长单调子序列。
  当然,当数据量非常大时候,需要通过二分进行优化。

4. 记忆化搜索(25 题)

  记忆化搜索是动态规划的一种实现方式,它将递归与记忆化相结合。在这 25 道相关题目中,通过在递归函数中记录已经计算过的子问题结果,避免了重复计算。例如,在计算斐波那契数列时,我们可以使用记忆化搜索的方式,定义一个数组记录已经计算出的 f ( n ) f(n) f(n)的值,当再次需要计算相同的 n n n时,直接从数组中取值返回,大大提高了计算效率。这种方式在处理一些具有复杂递归结构的问题时,能够有效减少时间复杂度。

5. 二维线性 DP(54 题)

  二维线性 DP 是在一维线性 DP 基础上的拓展,其状态通常由两个维度来表示。在这 54 道题目中,状态转移方程也会涉及到两个维度的变化。比如在矩阵路径问题中,我们需要计算从矩阵左上角到右下角的最短路径长度。定义状态 d p [ i ] [ j ] dp[i][j] dp[i][j]表示到达矩阵中第 i i i行第 j j j列位置的最短路径长度,状态转移方程可能为 d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + m a t r i x [ i ] [ j ] dp[i][j]=min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j] dp[i][j]=min(dp[i1][j],dp[i][j1])+matrix[i][j](假设只能向下或向右移动)。通过对二维数组的遍历和状态更新,最终得到目标位置的最优解。

6. 最长公共子序列(29 题)

  最长公共子序列是二维DP的一个经典问题,旨在找出两个给定序列中最长的公共子序列。对于这 29 道题目,我们定义状态 d p [ i ] [ j ] dp[i][j] dp[i][j]表示第一个序列的前 i i i个元素和第二个序列的前 j j j个元素的最长公共子序列长度。若两个序列当前位置的元素相等,即 a r r 1 [ i − 1 ] = a r r 2 [ j − 1 ] arr1[i - 1]=arr2[j - 1] arr1[i1]=arr2[j1],则 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j]=dp[i - 1][j - 1]+1 dp[i][j]=dp[i1][j1]+1;否则 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j]=max(dp[i - 1][j], dp[i][j - 1]) dp[i][j]=max(dp[i1][j],dp[i][j1]) 。通过这种方式,逐步计算出整个二维数组的状态值,最终 d p [ m ] [ n ] dp[m][n] dp[m][n] m m m n n n分别为两个序列的长度)即为最长公共子序列的长度。

三、背包 DP 相关题型

1. 0/1 背包(25 题)

  0/1 背包问题是背包 DP 中的经典题型。在这 25 道题目中,每个物品只有取或不取两种选择(即 0 或 1)。我们定义状态 d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个物品放入容量为 j j j的背包中所能获得的最大价值。状态转移方程为 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j]=max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i]),其中 w e i g h t [ i ] weight[i] weight[i] v a l u e [ i ] value[i] value[i]分别表示第 i i i个物品的重量和价值。该方程的含义是,要么不选第 i i i个物品,价值与前 i − 1 i - 1 i1个物品放入背包时相同;要么选择第 i i i个物品,但需要保证背包容量足够放下该物品,并将其价值加上前 i − 1 i - 1 i1个物品在剩余容量下的最大价值,取两者中的较大值作为当前状态的最优解。

2. 完全背包(30 题)

  完全背包问题与 0/1 背包类似,但每个物品可以无限次选取。在这 30 道题目中,状态定义同样为 d p [ i ] [ j ] dp[i][j] dp[i][j],但状态转移方程有所不同,变为 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j]=max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]) dp[i][j]=max(dp[i1][j],dp[i][jweight[i]]+value[i])。这里 d p [ i ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] dp[i][j - weight[i]] + value[i] dp[i][jweight[i]]+value[i]表示在选取第 i i i个物品后,继续考虑第 i i i个物品在剩余容量下的最大价值,因为物品可以无限选取,所以第二个状态转移项中是 d p [ i ] dp[i] dp[i]而非 d p [ i − 1 ] dp[i - 1] dp[i1] 。通过这种方式,我们能够处理物品可重复选取的背包问题。

3. 多重背包(25 题)

  多重背包问题中,每个物品有一定的数量限制。在这 25 道题目里,我们可以通过将多重背包问题转化为 0/1 背包问题来求解,即将每个数量有限的物品拆分成多个独立的物品,然后按照 0/1 背包的方法进行处理。也可以使用二进制优化的方法,将每个物品按照二进制位进行拆分,减少物品数量,从而降低时间复杂度,高效解决问题。

4. 分组背包(20 题)

  分组背包问题中,物品被分成若干组,每组中只能选取一个物品。在这 20 道题目中,我们定义状态 d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i组物品放入容量为 j j j的背包中所能获得的最大价值。状态转移时,需要遍历第 i i i组中的每个物品,取不选该组物品和选取该组中某个物品时价值的最大值,即 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ k ] ] + v a l u e [ k ] ) dp[i][j]=max(dp[i - 1][j], dp[i - 1][j - weight[k]] + value[k]) dp[i][j]=max(dp[i1][j],dp[i1][jweight[k]]+value[k]) k k k表示第 i i i组中的物品)。通过对组和物品的双重遍历,完成状态更新,得到最终的最优解。

四、树形 DP 相关题型

1. 树形 DP (选与不选模型)(9 题)

  树形 DP (选与不选模型) 主要围绕树结构展开,在这 9 道题目中,对于树的每个节点,都存在选或不选两种决策。我们通常定义状态,例如 d p [ u ] [ 0 ] dp[u][0] dp[u][0]表示不选节点 u u u时以 u u u为根的子树的某种最优值, d p [ u ] [ 1 ] dp[u][1] dp[u][1]表示选节点 u u u时的最优值。状态转移方程会涉及到对节点子树的遍历,根据选与不选的决策,更新当前节点的状态值。比如在树的最大独立集问题中,通过这种选与不选的状态定义和转移,能够计算出树中不相邻节点的最大数量。

2. 树上分组背包(9 题)

  树上分组背包结合了树结构和分组背包的特点。在这 9 道题目里,树的节点被分成不同的组,每组内的节点选取规则遵循分组背包的逻辑。我们需要在遍历树的过程中,根据分组背包的状态转移方式,计算每个节点在不同分组选择下的最优值,从而解决问题。

3. 树的重心(9 题)

  树的重心问题是树形 DP 中的一个重要应用。在这 9 道题目中,树的重心是指将树中的该节点删除后,剩余各个连通块中点数的最大值最小的节点。我们可以通过树形 DP 的方式,定义状态记录以每个节点为根的子树的节点数量等信息,通过对树的遍历和状态计算,找出树的重心节点。

五、其他重要 DP 题型

1. 数位 DP(67 题)

  数位 DP 主要用于处理与数字的数位相关的计数或最值问题。在这 67 道题目中,我们通常从数字的高位到低位进行状态转移。定义状态时,会考虑当前处理的数位、之前数位的状态以及一些限制条件等因素。例如,在计算给定区间内满足特定条件的数字个数时,通过对每个数位的状态进行记录和更新,逐步得到最终的结果。这种方法能够高效地处理涉及数字范围统计的复杂问题。

2. 状压 DP(22 题)

  状压 DP(状态压缩 DP)适用于状态数量较多,但可以通过二进制等方式进行压缩表示的问题。在这 22 道题目中,我们将状态用一个整数表示,其中每一位代表一种状态信息。例如,在棋盘覆盖问题中,可以用二进制数表示棋盘上每个格子的覆盖状态,通过对状态的枚举和转移,计算出满足条件的方案数。这种状态压缩的方式,大大减少了状态空间,使得原本复杂的问题能够在可接受的时间复杂度内求解。

3. 区间 DP(36 题)

  区间 DP 关注的是在一个区间上进行动态规划求解。在这 36 道题目中,我们定义状态 d p [ i ] [ j ] dp[i][j] dp[i][j]表示区间 [ i , j ] [i, j] [i,j]上的最优解。状态转移时,需要将区间分割成更小的子区间,通过子区间的最优解来计算当前区间的最优解。例如,在石子合并问题中,将若干堆石子合并成一堆,每次合并相邻的两堆,通过区间 DP 的方法,能够计算出合并石子的最小代价。

有关星球的提问的要点

  其实我更喜欢大家问我一些通识性的问题,起码我能很快回答出来,不会头秃hhh,就算是非常基础的 C/C++ 语法、算法问题,我也很乐意回答,星球里面 1v1 的每个提问,我都会仔细去思考它的答案。如果你觉得不好意思,可以【匿名提问】,最怕就是有问题但是不提出来,自己藏在心里。
  大家问我最多的问题,就是刷题到底有什么用?我工作都找不到了,到底还要不要刷题?
  谈谈我对刷题的想法,我暂且称之为刷题观,因为我是 2011 届的毕业生,2010年的时候拿到了ACM区域赛的金牌,也是这块金牌让我们学校第一次获得了进入世界总决赛的机会。我喜欢刷水题,并不是为了训练,单纯享受的那种刷题通过的快感!(过啦!)这是一次很强的正反馈。
  但是刷着刷着会迷茫,刷题的意义在于哪里?遇到难题往往要想一天,想出来了又怎样?现在已经没有比赛了,为什么还要刷题?后来一次偶然的机会,我遇到了一位高人,他的一句话令我茅塞顿开,有时候我们并不用去追求结果,如果一件事情可以给你带来正反馈,并且你可以一直靠着这股激情去持续坚持做这件事情,在做的过程中,收获了愉悦,本身就是一种情绪价值,远比刷短视频来得更有意义。
  然后刷题这件事情,我坚持了十年,去年,我打算把它作为我的终身事业,星球就好比是一个鉴证我逐渐完善我算法课程的工厂,我会把我这十几年的经验完全沉淀在里面,如果你愿意也需要学习这门技术,那么可以加入我们,学习一年,你的技术以及认知会有突飞猛进的增长。
  当然,这期间,遇到的任何问题,都可以通过星球app向我提问,或者在星球找到我的联系方式,有关星球更多的服务内容,可以参考下面的 阅读原文。
  星球详情介绍 && 本月优惠券领取:《英雄算法联盟》内容汇总。更多服务内容可以点击下方 阅读原文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值