DP问题学习笔记整合
DP问题学习笔记
1. 相关概念
-
dp两大性质
- 最优子结构问题: 如果问题的解是最优的,那么子问题的解也是最优的
- 利用备忘录思想解决重叠子问题计算问题
-
dp问题coding的常见步骤
- 找出最优解的性质,利用树, 图等数据解构刻画其结构特征
- 递归的定义的最优值
- 自底向上分析,找出状态转移方程, 计算子问题的最优解
- 根据子问题的最优解构造最优解
-
dp解题步骤:
第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?
第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]…..dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。
第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值
经典例题
一维dp
最大子段和
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
-
采用dp的思路, 我们把这个数组分成不同的段, dp[i] 表示以nums[i]结尾的那个段的最大字段和, 所以就存在nums[i]这个元素是加入前一段中, 还是自成一段这就是需要思考的问题, 只用求dp[i-1] + nums[i] 和 nums[i] 的最大值即可
-
通过上面的分析, 很容易写出状态转移方程
// dp初始化
dp[0] = nums[0]
dp[i] = Max(dp[i-1] + nums[i], nums[i])
-
这个题很有个很坑的地方就是, dp中最后一个元素并不是最终要求的结果, 这个我们平时做的题有很大的出入, dp[i]的含义是以nums[i]结尾的那个段的最大字段和, 那么dp中最后一个元素表示的是以nums中最后一个元素结尾的那个段的最大字段和, 最大的字段和不一定以nums中最后一个元素结尾,所以要最终要求的目标是dp数组中的最大值.
public int maxSubArray(int[] nums) { if(nums == null || nums.length <= 0) throw new IllegalArgumentException(); int[] dp = new int[nums.length]; dp[0] = nums[0]; for(int i = 1; i < nums.length; ++i){ dp[i] = Math.max(nums[i], nums[i] + dp[i-1]); } // return dp[nums.length-1]; 神坑 int maxValue = Integer.MIN_VALUE; for(int j = 0; j < dp.length; ++j){ if(dp[j] > maxValue)maxValue = dp[j]; } return maxValue;
-
优化 观察dp方程里面, dp[i]依赖dp[i-1]和nums[i],所以可以用一个变量来表示dp[i-1], 同时用一个变量来表示最大子段和, 最后也就不用再遍历dp数组了
public int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
throw new IllegalArgumentException();
}
int numsLen = nums.length;
int preMaxSum = 0; // 表示dp[i-1]
int maxSums = Integer.MIN_VALUE; // 表示最大子段和
for (int i = 1; i <= numsLen; i++) {
preMaxSum = Math.max(preMaxSum + nums[i-1],nums[i-1]);
maxSums = Math.max(maxSums, preMaxSum);
}
return maxSums;
}
- 继续想 如果想让你求出最大子段和是哪些序列? 如何做? 可以使用一个数组location记录以某个元素的最大子段和序列的个数, 看代码即可
public int[] maxSubArrayResult(int[] nums) {
if (nums == null || nums.length == 0) {
throw new IllegalArgumentException();
}
int numsLen = nums.length;
int preMaxSum = 0;
int maxSums = nums[0];
int maxSumsIndex = 1;
int[] location = new int[numsLen + 1];
for (int i = 1; i <= numsLen; i++) {
int tmp = preMaxSum + nums[i-1];
if (tmp > nums[i-1]) {
location[i] = location[i-1] + 1;
preMaxSum = tmp;
} else {
location[i] = 1;
preMaxSum = nums[i-1];
}
if (maxSums < preMaxSum) {
maxSums = tmp;
maxSumsIndex = i;
}
}
int resultLen = location[maxSumsIndex];
int start = maxSumsIndex - resultLen + 1;
int[] result = new int[resultLen];
System.arraycopy(nums, start - 1, result, 0, resultLen);
return result;
}
-
采用分治的思路, 我们把数组从中间分开, 最大子序列的位置就存在以下三种情况
- 最大子序列在左半边, 采用递归解决
- 最大子序列在右半边, 采用递归解决
- 最大子序列横跨左右半边, 左边的最大值加上右边的最大值
-
时间复杂度分析
T(n) = 2 F(n/2) + n
时间复杂度O(nlgn)
public int maxSubArray(int[] nums) {
if(nums == null || nums.length <= 0) throw new IllegalArgumentException();
return helper(nums, 0, nums.length-1);
}
private int helper(int [] nums, int start, int end){
if(nums == null || nums.length <= 0) throw new IllegalArgumentException();
if(start == end)return nums[start];
int middle = start + (end - start) / 2;
int leftSums = helper(nums,start, middle);
int rightSums = helper(nums,middle+1, end);
// 横跨左右两边
int leftRightSums;
// 左边的最大值
int lsums = Integer.MIN_VALUE, temp = 0;
for(int i = middle; i >= start; i--){
temp += nums[i];
if(temp > lsums)lsums = temp;
}
// 右边的最大值
int rsums = Integer.MIN_VALUE;
temp = 0;
for(int j = middle+1; j <= end; j++){
temp += nums[j];
if(temp > rsums) rsums = temp;
}
leftRightSums = rsums + lsums;
return Math.max(Math.max(leftSums, rightSums), leftRightSums);
}
- 打家劫舍
最大乘积和
- 题目描述给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
一不注意就做成了最大子段和
[-2, 3, -4]
maxDp[-2, 3, 12]
当nums[i]为正数时, nums[i] * maxDp[i-1]有可能是最大
当nums[i]为负数时, nums[i] * maxDp[i-1]要变成最小的了, 要想变成最大的, 就需要维护一个minDp
这种题目的话, 一般要利用最大和最小两个特性来解决
代码
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) {
throw new IllegalArgumentException();
}
int len = nums.length;
int returnVal = nums[0];
int preMaxMulti = 1; // 最大乘积
int preMinMulti = 1; // 最小乘积
int tmp;
for (int i = 1; i <= len; i++) {
if (nums[i-1] < 0) { // 当前元素为负数时, 则前一个元素的最小乘积乘以一个负数才可能会最大
tmp = preMaxMulti;
preMaxMulti = Math.max(nums[i-1], nums[i-1] * preMinMulti);
preMinMulti = Math.min(nums[i-1], nums[i-1] * tmp);
}
if(nums[i-1] >= 0) { // // 当前元素为正数时, 则前一个元素的最大乘积乘以一个正数才可能会最大
preMaxMulti = Math.max(nums[i-1], nums[i-1] * preMaxMulti);
preMinMulti = Math.min(nums[i-1], nums[i-1] * preMinMulti);
}
returnVal = Math.max(preMaxMulti, returnVal);
}
return returnVal;
}