概述
- 标准套路
- 明确 状态 和 选择
- 明确dp数组的定义
- 根据选择,思考状态转移的逻辑
- 处理边界情况
- 注意:
- 矩阵压缩时,确定循坏顺序
- 对于不是逐层计算dp数组的题目(一般只需填充一半),需斜着遍历数组(通过引入一个变量 len 来控制)例如:最长回文子串,博弈问题
int[][] dp = new int[array.length][array.length];
for(int len = 0; len < array.length; len++) {
for(int i = 0; i < array.length - len; i++) {
int j = i + len;
if(len == 0) {
}else {
}
}
}
背包问题
- 0-1背包
- 问题描述:一个可装载重量为 w 的背包和 n 个物品,每个物品有价格和重量两个属性,背包能装的最大价值。
- 状态:背包的容量和可选择的物品(装或不装)
- dp数组:dp[i][w](对于前 i 个物品,当前背包容量为 w,可以装的最大价值)
- dp公式:
– 不装: dp[i][w] = dp[i-1][w];
– 装入: dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], dp[i - 1][w]);
- 零钱兑换-完全背包问题(leedcode 518)
- 问题描述:给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
- dp数组:dp[i][j](可以将其压缩为一维数组)(若只使⽤前 i 个物品,当背包容量为 j 时,有 dp[i][j] 种方法可以装满背包)
- dp公式:
– 不装:dp[i][j] = dp[i-1][j];
– 装入:dp[i][j] = dp[i-1][j] + dp[i][j-coins[i-1]];(下标从1开始,所以coins[i-1])- 边界:dp[i][0] = 1(当总金额为0时,组合方式为1)
- 分割等和子集(leedcode 416)
- 问题描述:给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
- 将其转化为0-1背包问题(背包容量sum/2)
- 完全平方和(leedcode 279)
- 最小,需要初始化 i = 1 行
字符串的动态规划
7. 最长公共子序列(leedcode-1143)
- 问题描述:给定两个字符串text1和text2,返回这两个字符串的最长公共子序列的长度。(子序列:由原字符串在不改变字符的相对顺序的情况下删除某些字符后组成的新字符串,并非必须连续)
- dp数组:dp[i][j](text1长度 i 和text2长度 j 的最长公共子序列长度)
思路:
if(s1.charAt(i - 1) == s2.charAt(j - 1))
- dp[i - 1, j - 1] + 1 // i, j同时向前 移动
else max(分别丢弃s1,s2中的字符)
- dp[i - 1, j]
- dp[i, j - 1]
- 编辑距离(leedcode - 72, leedcode - 712 [最小删除和])
- 问题描述:给定两个字符串s1, s2,计算出将s1转换为s2所使用的最少操作数(可以使用三种操作:1. 插入一个字符 2 删除一个字符 3 替换一个字符)
- dp数组:dp[n][m](n-1和m-1的最小编辑距离)
- 初始化:dp[i][0] = i;dp[0][j] = j
思路:
if(s1.charAt(i - 1) == s2.charAt(j - 1))
- dp[i - 1, j - 1] // i, j同时向前 移动
else min(插入,删除,替换)
- dp[i, j - 1] + 1 //插入
- dp[i - 1, j] + 1 //删除
- dp[i - 1, j - 1] + 1 // 替换
- 最长回文子串(leedcode-5)
- 问题描述: 给定一个字符串 s,找到 s 中最长的回文子串。
- dp数组: dp[s.length()][s.length()](记录 i 到 j 之间是否是回文,true/false)
- dp公式:dp[i][j] = dp[i+1][j-1] && s.charAt(i) == s.charAt(j);
- 初始化:dp[i][i] = true; dp[i][i+1] = s.charAt(i) == s.charAt(j)
- KMP
最长上升子序列
- leedcode-300
- 问题描述:给定一个无序的整数数组,找到其中最长上升子序列的长度
- dp数组:dp[n] (Arrays.fill(dp, 1);)
- dp公式:
– nums[j] < nums[i] : dp[i] = Math.max(dp[i], dp[j] + 1);
- 俄罗斯套娃信封问题(leedcode-354)
- 问题描述:给定一些标记了宽度和高度的信封(w,h),当另一个信封的宽度和高度都比这个信封大的时候,这个信封可以放进另一个信封里,请问最多能有多少封信封能组成一组
- 思路:先对宽或高进行排序,然后寻找另一个维度的最长上升子序列(增设条件:前一个维度不能一样)
高楼扔鸡蛋(leedcode-887)
- 问题描述:有一栋 N 层楼,给你 K 个鸡蛋(K至少为1);现确定这栋楼存在楼层使得鸡蛋恰好没碎;最坏情况下,至少要扔几次鸡蛋,才能确定这个楼层。
- dp数组:dp[N+1][K+1](i 层楼 j 个鸡蛋最少实验次数)
- dp公式:dp[i][j] = Math.min(dp[i][j], Math.max(dp[k-1][j-1], dp[i-k][j]) + 1);
- 初始化:0 行 0 列都为 0;dp[1][j] = 1;dp[i][1] = i;
算法思想:
- 对于一个蛋,需 N(层数)次,逐层检查
- 对于无穷个蛋,二分查找
- 对于2个蛋,第一个蛋确定范围,第二个蛋精确查找,保证两次和最小(第一个蛋逐渐缩小范围,通过穷举确定最合适的范围)
博弈问题
- 问题描述:有一排石头堆,用数组piles[]表示,piles[i]表示第 I堆石子有多少个。两个人轮流拿石头,一次拿一堆,但是只能拿走最左边或最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。问先手拿的多还是后手拿的多。
- 状态表示:对于一堆石头来说,有先手和后手拿两个状态,通过定义一个类来表示
- dp数组:
– dp[i][j].fir(i 到 j 之间先手能获得的最高分数)
– dp[i][j].sec(i 到 j 之间后手能获得的最高分数)- dp公式:
– dp[i][j].fir = max(选择最左边的石头堆,选择最右边的石头堆)
– dp[i][j].sec = (根据先手的选择,然后选择另一边)- 初始化:
– dp[i][i].fir = piles[i]
– dp[i][i].sec = 0
正则表达式(leedcode-10)
- 问题描述:给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。( ‘.’ 匹配任意单个字符;’ * ’ 匹配零个或多个前面的那一个元素)
- dp数组:dp[i][j](s 的前 i 个是否能被 p 的前 j 个匹配)
算法思想(从dp[i-1][j-1]入手):
- p[j] == s[i] : dp[i][j] = dp[i-1][j-1]
- p[j] == “.” : dp[i][j] = dp[i-1][j-1]
- p[j] ==" * ":
3.1 dp[i][j - 2] : dp[i][j] = true( * 取0,相当于去掉)
3.2 p[j-1] == s[i] or p[j-1] == “.” :dp[i][j] = dp[i-1][j]
四建键盘
问题描述:
假设你有一个特殊的键盘包含下面的按键:
- key 1: A:在屏幕上打印一个A
- key 2: Ctrl-A:选中整个屏幕
- key 3: Ctrl-C:复制选中区域到缓冲区
- key 4: Ctrl-V:将缓冲区内容输出到上次输入的结束位置,并显示屏幕
现在,你只可以按键 N 次,请问屏幕上最多可以显示几个 A
- dp数组:dp[n+1]
- dp公式:
– 一直按 A:dp[i] = dp[i-1] + 1
– A,A,…C-A,C-C,C-V,C-V,…C-V(用 j 记录开始 c-v 的下标):dp[j - 2] * (i - j + 1)
股票问题
- 选择:买入(buy),卖出(sell),无操作(rest)
- 状态:天数,允许交易的最大次数,当前的持有状态(0没有持有 / 1持有)
- dp数组:dp[i][j][k](第 i 天,最大可交易次数 j,状态 k)
dp公式(选择在 buy 的时候,k - 1):
- dp[i][k][0] = max(dp[i-1][k][0](选择rest), dp[i-1][k][1] + prices[i](选择sell))
– 解释:今天没有持有,要么昨天没有持有,今天选择 rest;要么昨天持有,今天 sell- dp[i][k][1] = max(dp[i-1][k][1](选择rest), dp[i-1][k-1][0] - prices[i](选择buy))
– 解释:今天我持有股票,要么昨天持有,今天选择 rest;要么昨天没有持有,今天 buy
初始化:
- dp[-1][k][0] = 0(:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 )
- dp[-1][k][1] = -infinity(还没开始的时候,是不可能持有股票的,⽤负⽆穷表⽰这种不可能 )
- dp[i][0][0] = 0(因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0)
- dp[i][0][1] = -infinity(不允许交易的情况下,是不可能持有股票的,⽤负⽆穷表⽰这种不可能)
- k = 1(leedcode-121)
- dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) = max(dp[i-1][1][1], -prices[i])
- k 都是 1,可以消去
- 新状态只和相邻的一个状态有关,可以压缩为一个变量即可(dp_i_0,dp_i_1)
- k = + infinity(leedcode-122)
- dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i]) (k 为正⽆穷,那么就可以认为 k 和 k - 1 是⼀样的)
- k 不会改变,可以消去
- k = + infinity with cooldown(leedcode-309)
- dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
- 第 i 天选择 buy 的时候,要从 i-2 的状态转移,⽽不是 i-1
- k = + infinity with fee(leedcode-714)
- dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
- k = 2(leedcode-123)
- 必须对 k 进行穷举
- for (int k = max_k; k >= 1; k–)
- k = any Integer(leedcode-188)
- 出现了⼀个超内存的错误,原来是传⼊的 k 值会⾮常⼤,dp 数组太⼤了
- 有效的限制 k 应该不超 过 n/2,如果超过,就没有约束作⽤了,相当于 k = +infinity
//初始化
//对于其他情况,只用初始化dp[0][0],dp[0][1]
for(int i = 1; i <= k; i++) {
dp[0][i][0] = 0;
dp[0][i][1] = -prices[0];
}