Dynamic Programming Problem Notes
      
        
          
          2024-03-21 23:24:45
        
        
              
                
                
                
                  
                    # Algorithm
                  
                
                
              
          
              
          
      
      动态规划问题
判断是否能使用dp:
- 一个状态是由前面的某个状态或某些状态得到的,且不用关心前面的这些状态是如何得来的。
- 确定dp[i][j][k]..[l],就是确定i,j,k,l这些状态是否能通过前面的i,j,k,l的来得到。因此可以定义出dp函数的定义、应该是几纬和动态转移方程。
- 另一种定义状态的方式是看你要当前的答案需要关注哪些状态,哪些状态会影响到当前的答案(如股票问题的dp状态方程定义)
- 通常是求最值、方法数
记忆化搜索
模版
| 1 | int[][] memo = new int[n]; | 
转移为递推须知
- 快速边界修改:- 转换递推后如果出现memo[i][j-1]的情况,可以把整个第二维度(j)+1,注意第一维度(i)不用变化。相当于是把整个数组往后挪了一位。
- 出现memo[m-1][n-1]我们都统一+1,变成memo[i][j],memo[i][j-1],memo[m-1][n]
- 这种方法不需要修改for loop的index,也不需要做特殊判断
- 如果是数字的,也要修改!!比如在递归里面我们是判断if(i<0){return 0},正常会写成memo[-1],但因为我们将数组扩了1位,因此这个地方需要变成memo[0]
- 一些特殊情况: 在初始化的时候,我们要注意看for loop里的值是从哪里开始循环的,如果本来就是i=0开始的,其实就不需要写成i+1了
- 简单来说就是把数组退后了一位,其他所有地方都要看情况修改。还有一种情况就是memo[i+1][j+1]的话,可以把数组扩大1位,但是不用修改i的值,因为我们不需要挪动数组。
 
- 转换递推后如果出现
| 1 | /** 记忆化例子 **/ | 
- 转换成一维也有需要注意的地方- 当memo[i][0]变成了memo[0]时,我们需要在下面dp的i的for循环下进行更新,因为这样才能保证每一步都有正确的值(否则会被上一次的更新覆盖掉)
 
- 当
一维例子:
| 1 | /** 原来的二维 **/ | 
背包问题
从一个数组中,或者从一个固定范围的元素中,取出一些没有必要连续的元素,让他们的和【恰好】【至多】【至少】为k
0/1背包
背包容量为capacity,给一堆物品,每个物品的重量为w[i],问你恰好装满整个背包的话能有多少【方案数】
| 1 | # 本质上是选/不选的问题,且每个物品只能被选一次 | 
注意【恰好】可以改成至多、至少,这些直接在递归入口进行逻辑判断
完全背包
背包容量为capacity,给一堆物品,每个物品的重量为w[i],问你恰好装满整个背包的话能有多少【方案数】(每种物品可以选无数次)
| 1 | # 本质上是选/不选的问题,且每个物品每一轮都可以被选 | 
最长公共子序列(LCS)
简称LCS
| 1 | # 本质上是选/不选的问题 | 
类似习题
- 1458. Max Dot Product of Two Subsequences- LCS问题中,我们寻找的是长度,每个匹配项贡献的是固定值(即+1),因此不需要考虑“只选择当前i和j”的情况,因为长度的增加总是基于两个序列中相对应位置的元素匹配。
- 最大点积子序列问题中,我们寻找的是最大化数值(点积),这个值是变化的,取决于具体的元素值。因此,考虑“只选当前的i和j”是有意义的,因为它们单独的点积可能就是最大的,不需要依赖于它们之前的元素。
 
- LCS问题中,我们寻找的是长度,每个匹配项贡献的是固定值(即
编辑距离
| 1 | # 删除、插入、替换的操作次数 | 
最长递增子序列(LIS)
选出来的递增子序列必须是严格递增的,通常有两种方法
- dp
| 1 | int[] dp = new int[n]; | 
- 贪心+二分- 在java中我们维护一个数组arr,数组需要保证严格递增,假设j为数组arr目前的最后一个元素,如果nums[i]>arr[j],则直接加入数组中,相反使用二分法找到第一个大于nums[i]的数,并将其替换。
- 这个方法依赖于贪心策略:使得长度为i+1的数组中的最后一个元素最小,这样才有可能构造更长的子序列
- 最后答案为j+1=>这个j+1是代表整个数组中的最长递增子序列,不代表是nums[i]为最后一个元素的最长递增子序列!
- 如果要更新i结尾元素的最长递增子序列,要根据nums[i]所被替换或插入的index来
 
- 在java中我们维护一个数组
这类题型可以有很多变形,最常见的就是值递增,或通过排序后满足某种特定的“有一致性、传递性的条件”来求最大长度等等
俄罗斯套娃问题
- 假设m为宽度,n为高度,按宽度排序,宽度相同时按高度下降排序,这样做的好处是我们只比较高度n时,能够避免将相同宽度的信封算进去。
- 相似问题- 1626. Best Team With No Conflicts- 虽然这道题没有很明显的递增,但是我们也是有条件可以看出来递增的:当一名年龄较小球员的分数严格大于一名年龄较大的球员,则存在矛盾。 所以,在按年龄排序的情况下,score要递增;在按score排序的情况下,年龄要递增。这就是我们判断他是否“递增”的条件。
 
 
- 1626. Best Team With No Conflicts
股票系列问题
简单来说就是状态机DP
- 持有/未持有
- 第i天
- 第k次交易
| 1 | /** 这是没有冷冻期的情况 **/ | 
- 在Leetcode 121中,我们被要求最多只能交易一次,这实际上是和最多交易k次的模版一样的
区间DP
本质上也是选与不选的问题,通常与palindrome、分割区间有关
分割区间的小tip
- 1547. Minimum Cost to Cut a Stick- 可以将分割点数组直接看做一个完整的stick,即排序后新建一个数组,从而简化dp
 
