昨天,在CSDN网摘里看到一道面试题。题目大概是这样:
有两个数组a/b,大小都为n,数组元素的值任意整型数,无序;
要求:通过交换a,b中的元素,使数组a元素的和与数组b元素的和之间的差最小。
也可见这位朋友的博客: http://blog.csdn.net/kittyjie/archive/2009/07/28/4386742.aspx 说实话,他的实现方式我没有细看(我通常喜欢自己先想一下,实在想不出来,才参考别人的解决方案)。后来浏览后面的评论,有位仁兄说用的是贪心策略。
不扯太多,直奔主题,说说自己的想法。
我觉得这真不是太复杂(也许是我想简单了)。首先,我是这么理解—— “使数组a元素的和与数组b元素的和之间的差最小”的,
如果两个数组的和能相等
最好,如果不能相等那就是使和大的那个数组的和“尽量小”,而使和小的那个数组的和“尽量大”。这样他们的差值才会小。
假设有四个数:1,3,5,1000。
怎样将他们分成两组,并且使两组数和的差值最小?很明显将1、1000分在一组,3、5分在一组。
事实上,你可以发现只要是2的偶数倍的一个有序数列都可以这么做。也就是如果有序数列为1,4,8,20,37,48,51,99,那么1,99,8,48放在一个数组里,而4,51,20,37放在一个数组里。这样这两个数组的和的差总是最小的。(但是我不知道应该怎么证明,我感觉如果是一个有序队列的话,1,、1000之和的平均值与3、5的平均值最接近,求高手给出理论证明)。
当然,当一个序列的个数是2的奇数倍的时候,有个注意点就是:怎么选择最后一对怎么分配。比如一个序列:1,4,30,31,50,51.
让1、51进数组a,4、50进数组b,那么30和31怎么分配呢。在没分配30和31之前,我们能保证数组a的和与数组b的和之差最小。但是,30却不能简单地分配给数组a,31分配给数组b。记住原则是,让和大的数组的和“尽量小”,和小的数组的和"尽量大"。原来数组a的和为52,数组b的和为54.为了满足这个原则,那么应该让31进数组a,30进数组b。
你可以看到,这里我都是列举了正数为例。其实,负数也是这样,只要是一个有序序列,都可以按照这个最大最小成对的做法来分配,就可以使得两数组之和的差值变得最小。
总结一下,程序实现应该如何做:
1,将两个数组放大一个长度为2n的数组中进行排序;
2,看n是否为2的偶数倍,如果是则可以直接按照这样的类似前后成对的分配原则,最大、最小是第一组进第一个数组,第二大和第二小是第二组进第二个数组,第三大和第三小为第三组进第一个数组......依次类推。
如何n不为2的偶数倍,那么在大部分情况下,和n是2的偶数倍的做法一样(但是在做的时候有必要累加两数组的和)。只是到最后也就是最中间一组的时候,我们要看看那个数大,然后将它放到累计的数组和小的数组中去。而另一个数则放到另一个数组中。
如果,你觉得这种方法有漏洞或者不妥,欢迎拍砖,谢谢。
我觉得有的时候,看到问题不应该先复杂化,或者去套已有的算法,什么动态规划、背包问题、贪心算法等等。化繁为简才是最好的办法。
--------------------请从这里开始看,上面的方法有问 题(欢迎拍砖) -------------------
就像两位评论说的,确实我上面说的这个方法存在这漏洞。不过,我觉得最重要的还是不断滴思考,不要去在意那些错误,那些失败,只有这样才能不断地进步。
下面,总结一下上面的算法为什么有问题,并且再次谈一下我对这个问题的求解。
上面一直在谈差为最小值的问题。却没有去想一个最简单的道理,什么情况下是最小值?很明显,那就是当两个数组的和相等时,他们的差值才是最小的。对,这是一个特殊的情况,而我的思路一直都被两个数组的和不相等牵制着,并且我同时思考的时候没有想到有序序列某些特定的分布情况,那就是也许中间的几个元素的和跟前后几个元素的和相等或接近的情况,比如:1、3、30、50、80、156这样的情况。
所以最后发现,上述方法是不可行的。但是,至少有一句话是正确的,要想让两个数组的和的差值最小,就是让和大的尽量小,和小的数组的和尽量大。但是还需要在前面加上一个前提:在两个数组的和不可能出现相等的情况下。
下面,我说一下,我新的想法(同时非常欢迎再次拍砖,我一定虚心接受各位大牛的指教):
1、还是先将两个数组合并并进行排序;
2、计算出有序列的元素之和。(其实1、2两步没有先后顺序),这个和的一半就是两个数组元素之和的参考值【也就是说最好的情况下,如果这个序列中n个元素相加等于这个值,就直接得出了最终解,而如果没有这样的情况出现,我们也希望存在n个元素之和越接近参考值越好】。
3、就拿上面的一个序列(1、3、30、50、80、156)为例。我们得出参考值为160,现在我们的目标就是在这个2n(这里n=3)的序列中找到n个元素使得他们等于或者尽量接近160。我们先依次取前面的n个元素(1,3,30)并将其相加得出一个和,将和加入一个列表中,当然这个序列也要记录下来,为避免麻烦,我们无需一个元素放在一个单元里,我们可以将其连接成一个字符串也加入一个列表中(因为最后我们要用这个序列,所以有必要先保存它)。然后我们再去序列(1、3、50)、(1、3、80)、(1、3、156)、(3、30、50)....一共有多少个取法很明显这是个高中的排列组合问题——从2n中取n个数,无顺序,如上序列有六个数则有20种取法,也就是有20个n个元素相加的和(当然是有可能重复的)。最后我们将这些和的值与上面的参考值(也就是上面说的序列的和的一半)进行比较,最接近参考值的那个和,所对应的相加序列就是其中的一个数组,我们已经将这些相加的元素拼接成字符串。然后再将他们拆分出来,将剩下的n个元素放入另一个数组中即可。当然你也可以在每次得到n个元素相加的和的同时与参考值比较,如果和与参考值相等就直接退出。