我们先考虑比较简单的情况,所有的数字都是正数。那么显然sums里面的最小值一定是0. 并且sums的第二小的值x一定是这个数组中的最小元素。有了这个x,我们就可以把sums拆解成两部分:一部分是原数组元素(假设有n个)扣除x之后的所有子集和,共有2^(n-1)个;另一部分是前者的每个元素加上x,同样也有2^(n-1)个。这样两部分的并集就是原数组所有元素的subset sums。我们利用这个方法可以不停地剥离出最小元素,继而递归处理sums砍半之后的第一部分(因为没有了x的影响)。直至把原数组的所有元素都重构出来。
那么如何做到上述的拆解呢?我们先找sums里面的最大值a,它必然是所有元素的和,属于第二部分。相对应的,第一部分里必然会有一个a-x。于是我们可以将a和a-x都从sums里删除,同时将a-x放入待递归处理的sums2. 之后不断重复之前的操作,从大到小遍历sums里剩余的值,共进行2^n/2
轮。最后我们得到的sums2就是sums的一半,sum2所包含的所有子集和都已经剔除了x的影响。之后递归处理sums2即可。
OK,我们接下来考虑数字元素有正有负的情况。我们有没有机会确认其中一个元素呢?我们可以想到sums里面的最大值a,此时对应的应该是所有正数元素之和。那么sums里面的次大值b,对应的是什么呢?其实有两种可能,一种从所有正数元素之和里刨掉最小的那个,也有可能是把所有正数元素之和再加上最接近0的一个负数。不管哪种情况,我们令x=a-b,都有可能对应着原数组里的一个元素。如果是前者,那么x本身就是。如果是后者,那么-x就是。于是这里就出现了递归的分叉,我们可以将x作为一个原始元素将sums做分解,也可以将-x作为一个原始元素将sums做分解。对于分解成功的方案,我们就可以继续递归处理从sums砍半得到的sums2.
注意,当我们根据-x做sums的分解时,因为(-x)是负数,方法略有不同。此时sums里面的最小值a才是包含了(-x)的子集和,对应的a+x才是剔除了(-x)的子集和。所以我们需要从小到大遍历sums的元素。
递归的边界条件是当n=1时,sums里有两个元素。其中一个元素必须是0,另一个元素就是返回值(原始元素)。
根据前面的分析,我们会遇到这样一个问题。已知一系列数字{nums[i]},和该系列数字与某正数x的和{nums[i]+x},将这两者混合在一起记做arr之后,如何将原先的{nums[i]}解析出来?
第一种方法是用multiset,每一个回合可以实时取出arr里面的最大值,它必然对应了某个nums[j]+x,于是我们就可以知道了nums[j]是谁。这样我们就可以把nums[j]和nums[j]+x都从multiset里面直接删除。然后递归处理。
另一种方法是用双指针。将arr排序之后从高到低遍历。对于任何一个未访问过的元素arr[i],必然对应着另一个arr[j]+x。于是我们就可以把arr[i]和arr[j]标记出来。然后再剩下的arr元素里递归处理。注意到,i与j都一定是单调递减遍历的。这个技巧和2007和2122是一样的。