在程序员的江湖中,有一个流传了很久的故事,那就是:
华为面试题(8分钟写出代码):
有两个数组a,b,大小都为n,数组元素的值任意,无序;
要求:通过交换a,b中的元素,使数组a元素的和与数组b元素的和之间的差最小
华为面试题(8分钟写出代码) (注意发帖时间:2006年)
可怜一个“8分钟”,让无数程序员在8小时、8天、8周、甚至8年仍然未找出满意的答案而掉入了自卑的漩涡无法自拔!
这道题根本没有完美的解决方案。因为严格而言他是一个NP-Hard问题!
其本质属于划分问题:Partition problem
交换方法的侷限性
所有解决思路中,用排序、交换来解的,无一例外都会陷入局部最优。
对于交换算法,有朋友给出了一系列分析和证明:
交换两个数组使两个数组和的差最小
然而,这种方法只能保证每一次交换比上一次更优,就和梯度下降算法一样,只能保证最终达到一个局部最优值。
为何是局部最优而不是全局最优?考虑用上述交换算法解决这个例子:
a =[ 4 7 7 13]
b =[ 5 5 9 10]
此时,sum (a) = 31 > sum(b) = 29。
因此从a中选择一个比b大的数交换,且保证两数组和的差距不会更大。
(注:如果必须满足差距更小才交换,则算法直接停止)
唯一的选择是交换7和5,得到:
a =[ 4 5 7 13]
b =[ 4 7 9 10]
此时,sum (a) = 29 < sum(b) = 31。
是否感受到崩溃?这就是所谓的局部最优。你永远无法使a和b相等。
实际上,这个例子的最优解需要同时交换a中的4,7与b中的5,5:
a =[ 5 5 7 13]
b = [4 7 9 10]
此时sum (a) = 30 == sum(b) = 30。
可见,单一的交换a,b中的元素,永远达不到这个状态。
有人说那算法改成同时交换2个呢?
治标不治本。那如果遇到需要同时交换x个的初始序列,你咋办?
简化情况下的全局最优解
当然,在极度简化的情况下,是可以使用动态规划(Dynamic Program)得到完美解的。
所谓极度简化,是要求n很小,而数组中的每个数字也很小的情况。
下面我以n=1000,数字中每个数字最大不超过k=1000的正整数做说明。
这时候,就是加了一个物品数量限制的揹包问题。即给定1000个数字,选其中500个来使得使得大小为(sum(a)+sum(b))/2的揹包尽可能满。
极端情况下,1000个数字加起来大小为n*k=100万。因此揹包问题的空间辅助度为O(n*k)。
由于存在物品数量限制,需要一个额外的空间维度记录揹包中物品数量(n),因此需要开辟一个二维数组存储子问题解,总的空间复杂度为O(n*n*k)。
所以需要内存为1000*1000*1000 = 1G。
当然,严格来说,揹包大小可以取n*k的一半。揹包中物品数量也可取n的一半。这种常数就不讨论了。
重点是n,k太大,你揹包直接撑爆。如数组数字为整数(k=2^32)
就算你机器nb,撑不爆内存,O(n*n*k)的时间复杂度也能让你爆炸。
总结
总之,这道题数组大小没指定,即n未知;也没有说数组中的数的范围,k也未知。
如果你在面试中遇到这类问题,需要好好的分析数据范围。
如果n,k足够小,使用DP。
如果n,k太大,难以直接得出精确解。
这个时候,你应该考虑如何求得近似解。上述交换算法是一种思路。但是该算法必然陷入局部最优解。
你也可以扩展使用人工智能领域的一些随机优化方法,如模拟退火,蚁羣算法,遗传算法等等。这类方法具有一定的跳出局部最优解的能力。
相关的例子,可参考我之前的一篇博文:
模拟退火算法解旅行商(TSP)问题
又到程序员招聘季节!应试的程序员童鞋一定要引以为戒!
面试是灵活的,题目也是灵活的。
遇到问题,不一定要一开始就思考如何动笔,而是多思考,多和面试官交流。
面试不以给出答案为唯一目标,重要的是是否展现清晰的思路和冷静的解决问题能力。
最后,至于这题DP的代码,微软技术面试心得——《程序之美》中有类似的题。
该数章节2.18《数组分割》例子中,给出了DP的伪代码。此处不再赘述。