原题来自LeetCode,由“待字闺中”微信公众号转发。这里是我自己看了原贴之后的思考和解答。
题目如下:给定一个整型数组,至少有一个元素,请计算子数组最大乘积是多少?子数组必须是原数组中连续的一串数字构成的数组。整数可正可负。
例如:给定数组 [2, 3, -2, 4]。经过计算,得到最大乘积为6。子数组为[2,3]。
根据原贴的解题思路,这道题可以用动态规划来解,但难度在哪里呢?负负得正,就可恶在这里。那这个题目怎么办?O(n^2)么?是否要计算保存之前的所有状态?不必如此,其实就多了数组中的元素为负情况的考虑。因为负负得正,我们除了考虑当前的最大值之外,还需要考虑当前的最小值。
原因是,如果最小值是负的,而且当前是负的,那么乘积之后是正数,很可能就大于当前最大的。
所以遍历一遍,计算以每个元素结尾的最大值最小值,最后找到全局的最大值。就是我们要的结果了。
根据原贴的代码,我自己重新实现了一遍,如下:
class DynProg01
{
public static void main(String[] args)
{
System.out.println(new Integer(solve(new int[] { 1, 2, 3 })).toString());
System.out.println(new Integer(solve(new int[] { 1, -2, 3 })).toString());
System.out.println(new Integer(solve(new int[] { 1, -2, 3, -4 })).toString());
System.out.println(new Integer(solve(new int[] { 1, -2, -7, -4 })).toString());
System.out.println(new Integer(solve(new int[] { 1, -2, -5, -4 })).toString());
System.out.println(new Integer(solve(new int[] { 3, 4, -1 })).toString());
}
public static int solve(int[] array)
{
int maxVal = array[0];
int minVal = array[0];
int result = array[0];
for (int i = 1; i < array.length; i++) {
int tmpMax = maxVal * array[i];
int tmpMin = minVal * array[i];
maxVal = max3(tmpMax, tmpMin, array[i]);
minVal = min3(tmpMax, tmpMin, array[i]);
System.out.println("maxVal: " + (new Integer(maxVal)).toString() + "; minVal: " + (new Integer(minVal)).toString());
result = max2(maxVal, result);
}
return result;
}
private static int max3(int a, int b, int c)
{
return max2(max2(a, b), c);
}
public static int max2(int a, int b)
{
if (a < b) return b;
return a;
}
private static int min3(int a, int b, int c)
{
return min2(min2(a, b), c);
}
public static int min2(int a, int b)
{
if (a < b) return a;
return b;
}
}
代码没有经过简化,所以把我的测试数据也都附上了。对于所有这些例子,运行下来得到的结果都是正确的。它为什么正确呢?
上述代码里面,max2和max3函数都是相当平凡的,它们就是从两个或三个元素中选取最大值。关键在于solve函数。它里面的那个maxVal、minVal、tmpMax、tmpMin和result这些变量分别表示什么。
我昨晚上思考了许久,想明白了。对于下标i来说,maxVal和minVal代表的是数组从元素0到元素i-i之间,包含元素i-1的子数组中能得到的最大乘积和最小乘积。result则代表从元素0到元素i-1之间,乘积最大的子数组的乘积。tmpMax和tmpMin则是将maxVal、minVal和元素i相乘得到的临时结果,用于后面的比较来得到新的maxVal和minVal。
现在需要用一下数学归纳法。假设对于n的情况,maxVal和minVal还有result的值都满足了上述条件,对于n+1的情况又如何呢?现在的情况是,新增了元素n。首先,考虑maxVal。maxVal要么就是元素n,要么就是包含元素n在内的子数组的乘积。前者的情况显然在这里得到了考虑,而事实上,在我们题设的整数情况下却不会发生,因为整数的绝对值总是大于等于1,所以之前的乘积的绝对值总是大于等于1,同时考虑maxVal和minVal的结果就会让新的maxVal大于等于元素n。但它对于浮点数的情况是有必要的。对于包含元素n在内的子数组的乘积的情况,maxVal表示的是可能得到的最大乘积,其他子数组的乘积已经不可能比这个更大了,如果考虑当前值为负的情况,minVal就能起到作用,所以新的maxVal是正确的。
同理可证新的minVal是正确的。那么result呢?需要注意result只是从元素0到元素n-1之间的最大乘积子数组的乘积,它未必要包含元素n-1。那很简单,它只需要从它自己和新的maxVal之间取较大值即可。
这样的动态规划,让我联想起了字符串查找用的KMP算法。因为KMP算法也要计算最大匹配的真前缀的位置,也要用到数学归纳法。下面是我当年对KMP算法的理解和解释:
http://www.fandecheng.com/archives/167
上面说过,现在这个动态规划算法也适用于浮点数。我试了[2, 0.5, 3],这个算法也是正确的。
还有,原贴中提到的一道求和题。大致是:给定一个整型数组,请计算加起来的和最大的子数组的和是多少?子数组必须是原数组中连续的一串数字构成的数组。整数可正可负。
这道题更简单些,因为我们只要一路求最大和就可以了。关键问题是遇到负数怎么处理。其实也一样,用上面的maxVal和result的思路。如果元素n是一个负数,maxVal加上它将让maxVal变小,但我们要求的是包含这个元素的子数组中的最大和,所以还是必须加上它的。只要之前的maxVal本身是正的就行。如果之前的maxVal本身就是负的,那我们把它抛弃,只取元素n。而result则将成为和最大的子数组,其结尾不必是元素n。