6/27, subarray 划分类,股票
所谓 “划分类” DP,是指给定原数组之后,将其划分为 k 个子数组,使其 sum / product 最大 / 最小的 DP 类问题。
而这类问题的 optimal substructure 是,对于给定区间的globalMax,其最优解一定由所属区间内的 localMax 区间组成,可能不连续。 以“必须以当前结尾” 来保证 local 子问题之间的连续性;用 global 来记录最优解之间可能的不连续性。
划分类DP 与 区间类DP 主要区别在于,我们只取其中 k 段,而中间部分可以留空;而区间类 DP 子问题之间是连贯而不留空的。区间类 DP 是记忆化的 divide & conquer, 划分类 DP 则是不连续 subarray 的组合,不同于区间类 DP 每一个区间都要考虑与计算,划分类DP 很多元素和子区间是可以忽略的,有贪心法的思想在里面,因此也依赖于正确的 local / global 结构来保证算法的正确性。
Kadane's Algorithm 相比 prefix sum 的特点是,必须要以 nums[0] 做初始化,从 index = 1 开始搜,优点是在处理 prefix product 的时候更容易处理好“-5” 和 “0” 的情况。
这类靠 prefix sum/product 的 subarray 问题,要注意好对于每一个子问题的 local / global 最优解结构,其中 local 解都是“以当前结尾 && 连续”,而 global 未必。
Prefix sum 数组的 local / global 通用模板,求 min / max 皆可,使用时需要注意初始条件以及顺序;
int[] leftMax = new int[n];
int prefixSum = 0;
// 代表从起始位置开始,值最小的连续和数组
int localMin = 0;
int globalMax = Integer.MIN_VALUE;
for(int i = 0; i < n; i++){
prefixSum += nums[i];
globalMax = Math.max(globalMax, prefixSum - localMin);
localMin = Math.min(localMin, prefixSum);
leftMax[i] = globalMax;
}
比如 CodeForce 上这个帖子的另一种写法:
Kadane's Algorithm 其实是在维护一个 sliding window,不过每个迭代的时候,在考虑以当前元素为新起点之余,又砍去了之前的部分。
input : [-1,5,6,-1,-2,4]
可以看到,在 kadane's algorithm 中,我们保存的这个 prevMin 始终是连续的,起点不一定是哪,但是终点一定是 current,是一个 local 最优解。我们每次迭代,用 local 最优解去更新 global 最优解。
用区间和的思路:
用 Kadane's Algorithm:
L 家面经题。
其实这个做法与其说像 prefix product ,不如说更像 Kadane's Algorithm. 可以看到这个算法可以有效免疫各种切断的情况,只维护到当前位置结尾的 max / min subarray 结果就可以了。
在这里面,min/max 是连续的,local 以 i 结尾的; rst 是 global 非连续的。
这次光用 local 变量更新然后返回 global 最优还不够,我们需要保存对于每一个子问题的 global 最优(可能并不以当前位置结尾),用于后面的左右两边全局查询。
是从 切分数 k 开始循环,还是从数组位置 i 开始循环?
首先思考这个性质:对于切割数为 j 的数组,如果其 size = i < j ,是无法得出正确结果的。换句话说,每次循环真正的正确起点,一定得至少是从 i = j 开始, i 受限于 j.
那么顺着这个思路,如果以 i 作为外循环,内循环 j 一定是在 [0, i] 区间内,并且 j <= k. 因此内循环需要两个条件:
对于任意指定 j ,需要做一个类似于我们之前计算 subarray I & II 时所需要的初始化,确保即使当前位置为负,作为以当前位置结尾的 localMax 依然选中此元素。因此先做一个 localMax[j - 1][j] = Integer.MIN_VALUE;
另一个需要着重注意的是,当 i = j ,即元素数 = 切分数时,即使当前元素为负,我们的 globalMax 也别无选择,必须以 localMax[i][j] 为准,采用当前元素。
这题的另一种循环写法,参照的九章答案。可以看到当循环内计算内容一致时,外循环的顺序是完全可以依据条件调换的,就像 Sparse Matrix Multiplication 一样。
这题和之前求 maximum non-overlap subarrays 的思路基本一致,只不过要求的数组变成了四个:leftMax, leftMin, rightMax 和 rightMin.
于是这题变成了一个非常适合考察计算各种 local / global prefix sum 数组模板熟练度的问题。
k 次交易 = k 个 non-overlapping subarray
然后拼起来就好了。要注意的细节:
股票和 subarray 问题之间稍微有不同: subarray 里面初始化为 0 ,有时候元素为 -5 但是不得取,因此有些位置需要 Integer.MIN_VALUE 的初始化; 然而股票我可以选择不卖,初始的 0 就正好。
同样道理,股票中也不需要处理 subarray 问题中 i = j 时候必须强行拿当前元素,觉得亏我大不了不卖嘛,交易少于 k 也没事。
记得内循环条件是 j * 2 <= i + 1,而不是 j <= i / 2,否则报错,因为 index off-by-one 处理错了。
由这题我们可以发现,实际需要的 dp[] 数量是取决于操作数量的,要以“用当前操作结尾”来定义 localMax. 只不过在之前的问题中,因为买入卖出可以拼接,我们略去了 buy 的数组。
这题扩展开来的话,也可以扩展到 cooldown k 天,所依赖的状态,初始化,循环的起始位置会有变化而已。

这个解法的要点是,每个 state 的 in-degree 代表能到达当前 state 的所有可能操作。
Last updated