DP做题规律
问题分解的过程中,如果此时的状态与之前的状态相关,那么DP可以试一试。
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
简单的DP
-
一维
斐波那契数
爬楼梯
dp[i]=dp[i-1]+dp[i-2] -
二维
(1) 不同路径
(2) 整数拆分详解 递推公式难
dp[i] 表示将i拆分所得最大的乘积,那么dp[i]可以从dp[i-j]*j 与 j*(i-j)得到; -
dp[i-j]一定是将i-j拆分为两个或两个以上的数字和,因此这一项考虑的是将i拆分大于两个数字和的情况
-
j*(i-j)考虑的是将i拆分为两个数字和的情况
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
dp[2] = 1;
for (int i = 3; i <= n ; i++) {
for (int j = 1; j < i - 1; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};
背包
01背包
- 数组含义:
- dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,最大价值总和
- 遍历方法:
- 不放物品i:dp[i - 1][j]
- 放物品i:dp[i - 1][j - weight[i]] + value[i]
//从后往前
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
完全背包
物品无限
// 从前往后
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
多重背包
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
题型:
找子集,得到目标和 问题转化 数组含义
- 分割等和子集 W V相同
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等 - 最后一块石头的重量 II W V相同 问题转化 数组含义
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎; 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
本质上寻找将集合分为两个子集,使值尽可能接近
找组合,得到目标和
- 目标和 问题转换 递推公式 初始化
left - right = target。
left + right=sum
left - (sum - left) = target ->left = (target + sum)/2
问题转化为装满left有多少方案
注:转化为这样后不需要在考虑-号的事情了!!!简化了问题
多维度背包
- 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
完全背包 dp求组合、排列个数
- 零钱兑换 II
外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
这种遍历顺序中dp[j]里计算的是组合数
如果把两个for交换顺序,代码如下:
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
此时dp[j]里算出来的就是排列数!
打家劫舍 递推公式加限制
给递推公式加一些条件
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
买卖股票 多状态dp 状态转移方法
- 买卖股票的最佳时机 只能买卖一次
给dp数组加入一些状态
dp[i][0] 第i天持有股票
dp[i][1] 第i天未持有股票
dp[i][0] 递推
第i-1天持有 dp[i-1][0]
第i-1天未持有 -prices[i]
dp[i][1] 递推
第i-1天持有 prices[i]+dp[i-1][0]
第i-1天未持有 dp[i-1][1]
dp[i][0] = max(dp[i - 1][0], -prices[i]);
dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
- 买卖股票的最佳时机II 买卖多次
dp[i][0] 第i天持有股票
dp[i][1] 第i天未持有股票
dp[i][0] 递推
第i-1天持有 dp[i-1][0]
第i-1天未持有 dp[i-1][1]-prices[i]
dp[i][1] 递推
第i-1天持有 prices[i]+dp[i-1][0]
第i-1天未持有 dp[i-1][1]
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
- 买卖股票的最佳时机III 可以完成两笔交易。
五个状态,不是某一天一定要买入卖出,表示一个状态,这种状态是可以延续的!
没有操作
第一次买入
第一次卖出
第二次买入
第二次卖出
dp[i][1]递推
第i天买入股票 dp[i][1] = dp[i-1][0] - prices[i]
第i天没有操作 dp[i][1] = dp[i - 1][1]
dp[i][2]递推
第i天卖出股票 dp[i][2] = dp[i-1][1] + prices[i]
第i天没有操作 dp[i][2] = dp[i - 1][2]
dp[i][3]递推
第i天买入股票了 dp[i][3] = dp[i-1][2] - prices[i]
第i天没有操作 dp[i][3] = dp[i - 1][3]
dp[i][4]递推
第i天卖出股票 dp[i][4] = dp[i-1][3] + prices[i]
第i天没有操作 dp[i][4] = dp[i - 1][4]
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
求子序列长度 dp数组的定义以及递推方法
-
最长递增子序列
dp[i]表示以包含nums[i]结尾的序列最长上升子序列的长度
dp[i]是之前所有的进行比较 取最大的 dp[j] + 1
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); -
最长连续递增序列 限制了递推方式
if (nums[i + 1] > nums[i]) dp[i + 1] = dp[i] + 1; -
最长重复子数组 多维DP
dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]
递推公式
if (A[i - 1] == B[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; // 连续 -
最长公共子序列 不需要连续
if (text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);