2.18 数组分割
有一个没有排序、元素个数为2n的正整数数组,要求:如何能把这个数组分割为元素个数为n的两个数组,并使两个子数组的和最接近?
分析与解法
从题目中可以分析出,题目的本质就是要从2n个整数中找出n个,使得它们的和尽可能地靠近所有整数之和的一半。
【解法一】
看到这个题目后,一个直观的想法是:
先将数组的所有元素排序为a1<a2<…<a2N,
将它们划分为两个子数组S1=[a1,a3,a5,…,a2N-1]和S2=[a2,a4,a6,…,a2N],
从S1和S2中找出一对数进行交换,使得SUM(S1)和SUM(S2)之间的值尽可能的小,直到找不到可对换的。这种想法的缺陷是得到的S1和S2并不是最优的。
【解法二】
假设2n个整数之和为SUM。从2n个整数中找出n个元素的和,不管如何接近SUM/2,同样都会存在大于SUM/2或者小于SUM/2的情况。在求解这个问题时,大于或小于SUM/2没有本质的区别。因此,可以只考虑小于等于SUM/2的情况。
比较直观地来说,可以用动态规划来解决这个问题。具体分析如下:
可以把任务分成2N步,第k步的定义是前k个元素中任意i个元素的和,所有可能的取值之集合为Sk(只考虑取值小于等于SUM/2的情况)。
然后将第k步拆分成两个小步。即首先得到前k-1个元素中,任意i个元素,总共能有多少种取值,设这个取值集合为Sk-1={vi}。第二步就是令Sk=Sk-1∪{vi+arr[k]},即可完成第k步。
伪代码实现如下:
代码清单2-36
从有效性来分析,整个代码是一个三重循环,目的就是执行insert。这个代码实际执行insert的次数至多是4N次(枚举所有元素在与不在的情况),因此可以认为复杂度是O(4N)。
既然算法的时间复杂度是N的指数级,因此在N很大时,效率很低。我们不得不考虑设计一种时间复杂度是N的多项式函数的方法。考虑的出发点是,是否有另一种拆分第k步的方法。
【解法三】
解法二的拆分方法需要遍历Sk-1={vi}的元素,由于Sk-1={vi}的元素个数随着k的增大而增大,所以导致了解法二的效率低下。能不能设计一个算法使得第k步所花费的时间与k无关呢?
我们不妨倒过来想,原来是给定Sk-1={vi},求Sk。那我们能不能给定Sk的可能值v和arr[k],去寻找v-arr[k]是否在Sk-1={vi}中呢?由于Sk可能值的集合的大小与k无关,所以这样设计的动态规划算法其第k步的时间复杂度与k无关。
代码如下:
代码清单2-37
利用如上的算法,时间复杂度将为O(N2* Sum)。
讨论
虽然解法三对于N是多项式时间算法,但当SUM很大而N很小时,比如对于arr={109,108,108+1,109+1},这个解法是很不合适的,所以我们应该根据具体的问题选用合适的方法。
扩展问题
如果数组中有负数,怎么办?