1. 问题
经常以数组序列形式出现,问的是最xxx?
之前一篇已经提到过,动态规划划分子问题是首要,一定要定义一个比较好的子问题表达式,这样才可以较容易求得状态转移方程。
这类动态规划问题如下:
2. 定义子问题
这类问题的子问题一般都是定义:f(n)为以an为结尾的最XXX。
然后求状态转移方程,有的比较简单的就单纯的和前面一个子问题有关,即f(i) = g(f(i-1)),例如 最大子序列和,连续子数组的最大和;有的是和前面的某个特定的子问题有关,例如 最长上升子序列;还有一个比较特殊,得使用两个类似的子问题,例如乘积最大子数组。
3. 题解
- 最大子序列和,连续子数组的最大和
这个比较简单了,定义子问题:f(n)表示以nums[n](an)为结尾的最大子序列和;
那么根据我们的子问题的定义,当遍历到ai时,最大子序列要以ai为结尾,则其要么是f(i-1) + ai,要么是ai,然后根据最大,那么就得到了状态转移方程: f ( n ) = m a x { f ( n − 1 ) + a n , a n } ; f(n) = max\{f(n-1) + a_n, a_n\}; f(n)=max{f(n−1)+an,an};
注意到这个可以用原数组进行子问题的结果缓存,但是为了不改变参数,我们还是最好自己申请一个数组空间。当然注意到f(n)只与前面一个子问题有关,因此我们可以只用一个变量保存f(n-1)即可。
class Solution {
public int maxSubArray(int[] nums) {
int[] maxSub = new int[nums.length];
int max = 0;
for (int i = 0; i < nums.length; i++){
if (i == 0) {
maxSub[i] = nums[i];
max = maxSub[i];
}else {
maxSub[i] = Math.max(maxSub[i-1]+nums[i], nums[i]);
}
max = Math.max(max, maxSub[i]);
}
return max;
}
}
-
最长上升子序列
定义子问题:f(n)表示以an结尾的最长上升子序列;
根据子问题求状态转移方程:当遍历到ai时,根据子问题的定义,当前最长上升序列一定要包含ai,并且要是上升的,并且要是最长的,那么如果我们按照上面的那道题的状态转移方程的写法行吗? f ( n ) = { f ( n − 1 ) + 1 a n > a n − 1 ; 1 otherwise. f(n)= \begin{cases} f(n-1) + 1 & {a_n > a_{n-1};} \\ 1 & \text{otherwise.} \end{cases} f(n)={f(n−1)+11an>an−1;otherwise.
注意这里强调了最长,那就是要在所有的可以将an添加到末尾的上升子序列中最长的那个挑出来,然后将其加入从而得到最长的以an为结尾的上升子序列。因此实际的状态转移方程为:
f ( n ) = { m a x { f ( i ) + 1 } a n > a i 并 且 i < n ; 1 otherwise. f(n)= \begin{cases} max\{f(i) + 1\} & {a_n > a_i 并且i<n ;} \\ 1 & \text{otherwise.} \end{cases} f(n)={max{f(i)+1}1an>ai并且i<n;otherwise. -
乘积最大子数组
定义子问题:f(n)为以an结尾的乘积最大子数组的积
状态转移方程: f ( n ) = { f ( n − 1 ) ∗ a n f ( n − 1 ) ∗ a n > 0 ; a n otherwise. f(n)= \begin{cases} f(n-1) * a_n & {f(n-1) * a_n > 0;} \\ a_n & \text{otherwise.} \end{cases} f(n)={f(n−1)∗ananf(n−1)∗an>0;otherwise.
这样的对吗?假如ai是负数,这时自然希望f(i-1)越负越好,但是此前f(n)的定义不是这么定义的,那怎么办呢?
定义两个子问题:f(n)保持原定义,增加一个g(n)表示以an结尾的乘积最小(负)子数组的积;
然后,状态转移方程:
f ( n ) = m a x { f ( n − 1 ) ∗ a n , g ( n − 1 ) ∗ a n , a n } f(n)=max\{f(n-1) * a_n, g(n-1)*a_n, a_n\} f(n)=max{f(n−1)∗an,g(n−1)∗an,an}
g ( n ) = m i n { g ( n − 1 ) ∗ a n , f ( n − 1 ) ∗ a n , a n } g(n)=min\{g(n-1) * a_n,f(n-1)*a_n ,a_n\} g(n)=min{g(n−1)∗an,f(n−1)∗an,an}
解释一下:对于f(n)来说,它是以an结尾的最大,当an < 0 时,我们想前面积累的越负越好,因此应该使用g(n-1)*an,但是g(n-1)有可能是正数啊(例如an前面全是正数的时候),所以还应与an比较谁更大(当g(n-1)大于0时自然是an更大了);当an > 0 时,我们想前面积累的越正越好,因此应该比较f(n-1)*an与an的大小,取其大者,为什么要与an比较?因为f(n-1)可能为负值啊。例如序列[1,2,3,4,-5,6],当遇到第一个负数-5时,f(4)=-5。
g(n)同上。
上代码了:
class Solution {
public int maxProduct(int[] nums) {
//
int[] posi = new int[nums.length];
int[] neg = new int[nums.length];
int max = nums[0];
for (int i = 0; i < nums.length; i++) {
int n = nums[i];
if (i == 0) {
posi[i] = n;
neg[i] = n;
} else {
posi[i] = Math.max(Math.max(posi[i-1]*n, n), neg[i-1]*n);
neg[i] = Math.min(Math.min(posi[i-1]*n, n), neg[i-1]*n);
}
max = Math.max(max, posi[i]);
}
return max;
}
}
本文深入探讨了如何利用动态规划解决一系列以数组序列为背景的问题,包括最大子序列和、最长上升子序列及乘积最大子数组等。通过定义子问题、建立状态转移方程,给出详细的解题思路和代码实现。

被折叠的 条评论
为什么被折叠?



