中午吃法, 同事给我出了一道题。
practice1. 给定一个拥有N个数字的有序数组,如何找出他的最大子集的乘积最大.比如给的数组是: [1,2,-3,4,5]. 那么他的最大子集是[4,5].因为4*5=20.是最大的。
备注: 我没有看标准答案,只是根据自己的想法尝试解答。肯定不是最好的解决方法,也不高效。不过,是自己想出来的,特此记录一下。
问题分析
- 可以把数组描绘在一个二位坐标系里面,横轴表示元素在数组中的下标(index),纵轴表示元素的值(value).
- 用一条曲线将坐标系里面的所有点链接起来。形成一个高低起伏的丘陵形地貌,以元素值0作为一条分界线,将丘陵进行切割,切割后有山峰,有峡谷。
- 对于山峰,山峰上的所有元素都是正值。是构成最大子集的“正能量”子集。
- 对于峡谷,峡谷里的所有元素都是负值。根据奇数个负数的乘积是负数,偶数个负数的 乘积是正数的原理。需要根据情况分别对待:
- 对于 0 点,是不能包含在所求结果里面的。因为有了他,一切归零。
- 如果这段峡谷有偶数个负元素,可以把他归类为山峰,当作山峰处理。
- 如果这段峡谷有奇数个负元素,可以统计这个数组的所有负数个数,然后记录拥有奇数哥峡谷的个数,只要取偶数个峡谷的个数,如果没有剩余的峡谷,那么所取的连续的峡谷的第一个负数峡谷和最后一个正数峡谷之间的地带,就是所要的结果,如果还有剩余的峡谷,把剩余峡谷靠头端或者尾端的元素去掉,就变成“正能量”峡谷了。
解法优化 通过上面的分析,解法思路如下:
- 遍历数组当中的所有元素。记录所有元素为0的位置index. 这些 0 点将把数组分割成子数组。
- 对于每个子数组,统计负数元素的个数。如果负数元素个数为偶数个,可以忽略不记,如果负数元素为奇数个,找出这个子数组开头和结尾的负数元素所在的位置,通过比较大小来决定切去拿一部分,剩下的就是待选的最大子集。比如开头部分是[2,3,-2] ,开头部分乘积是-12,结尾部分是[-1,9]的乘积是-9. 所以选择切去绝对值较小的尾部。
- 根据最大子集计算的结果,找出他们当中最大的值。就是所求的结果。
花了一些时间编写出代码,取 Integer.MAX_VALUE/100 (大约2千万多)个大小为100以内的随机元素组成的数组,计算会用19秒钟。如果取2亿个数,在生成随机数的时候就要报错OutOfMemoryError.
代码如下:
SubArray 是一个对象,记录子组信息, 包括开始索引,结束索引,所有负数的索引。对外提供计算自己的乘积的方法。
package com.huyouxiao.array.subset;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class SubArray {
public Integer startIndex;
public Integer endIndex;
public List<Integer> negativeIndex = new ArrayList<>();
public BigDecimal getCount(Integer[] array) {
int size = negativeIndex.size();
if(size % 2 == 0) {
return this.getMultiplyResult(array, startIndex, endIndex);
} else if(size > 1) {
// remove last negative
BigDecimal first = this.getMultiplyResult(array, startIndex, negativeIndex.get(0));
BigDecimal last = this.getMultiplyResult(array, negativeIndex.get(negativeIndex.size()-1), endIndex);
if(first.compareTo(last)>0) {
endIndex = negativeIndex.get(negativeIndex.size()-1)-1;
return this.getMultiplyResult(array, startIndex, endIndex);
} else {
// remove first negative
startIndex = negativeIndex.get(0)+1;
return this.getMultiplyResult(array, st