问答题 3.  已知三个升序整数数组a[1], b[m]和c[n],请在三个数组中各找一个元素,使得组成的三元组距离最小。三元组距离的定义是:假设a[i]、b[j]和c[k]是一个三元组,那么距离为:Distance=max(|a[i]-b[j]|,|a[i]-c[k]|,|b[j]-c[k]|),请设计一个求最小三元组距离的最优算法。
【正确答案】最简单的方法就是找出所有可能的组合,从所有的组合中找出最小的距离,但是显然这种方法的效率比较低下。通过分析发现,当ai≤bi≤ci时,此时它们的距离肯定为Di=ci-ai。此时就没必要求bi-ai与ci-ai的值了,从而可以省去很多没必要的步骤,下面分别详细介绍这两种方法。
   方法一:蛮力法
   最容易想到的方法就是分别遍历三个数组中的元素,对遍历到的元素分别求出它们的距离,然后从这些值里面查找最小值,实现代码如下:
   def maxs(a,b,c):
   maxs=b if a<b else a
   maxs=c if maxs<c else maxs
   return maxs
   
   def minDistance(a,b,c):
   aLen=len(a)
   bLen=len(b)
   cLen=len(c)
   minDist=maxs(abs(a[0]-b[0]),abs(a[0]-c[0]),abs(b[0]-c[0]))
   dist=0
   i=0
   while i<aLen:
   j=0
   while j<bLen:
   k=0
   while k<cLen:
   #求距离
   dist=maxs(abs(a[i]-b[j]),abs(a[i]-c[k]).abs(b[j]-c[k]))
   #找到最小距离
   if minDist>dist:
   minDist=dist
   k+=1
   j+=1
   i+=1
   return minDist
   
   if __name__=="__main__":
   a=[3,4,5,7,15]
   b=[10, 12, 14, 16, 17]
   c=[20, 21, 23, 24, 37, 30]
   print "最小离为: "+str(minDistance(a, b, c))
   程序的运行结果为:
   最小距离为:5
   算法性能分析:
   这种方法的时间复杂度为O(l*m*n),显然这种方法没有用到数组升序这一特性,因此,该方法肯定不是最好的方法。
   方法二:最小距离法
   假设当前遍历到这三个数组中的元素分别为ai,bi,ci,并且ai≤bi≤ci,此时它们的距离肯定为Di=ci-ai,那么接下来可以分如下三种情况讨论:
   (1)如果接下来求ai,bi,ci+1的距离,由于ci+1≥ci,此时它们的距离必定为Di+1=ci+1-ai,显然Di+1≥Di,因此,Di+1不可能为最小距离。
   (2)如果接下来求ai,bi+1,ci的距离,由于bi+1≥bi,如果bi+1≤ci,此时它们的距离仍然为Di+1=ci-ai;如果bi+1>ci,那么此时它们的距离为Di+1=bi+1-ai,显然Di+1≥Di,因此,Di+1不可能为最小距离。
   (3)如果接下来求ai+1,bi,ci的距离,如果ai+1<ci-|ci-ai|,此时它们的距离Di+1=max(ci-ai+1,ci-bi),显然Di+1<Di,因此,Di+1有可能是最小距离。
   综上所述,在求最小距离的时候只需要考虑第3种情况即可。具体实现思路为:从三个数组的第一个元素开始,首先求出它们的距离minDist,接着找出这三个数中最小数所在的数组,只对这个数组的下标往后移一个位置,接着求三个数组中当前遍历元素的距离,如果比minDist小,则把当前距离赋值给minDist,依此类推,直到遍历完其中一个数组为止。
   例如给定数组:a=[3,4,5,7,15]; b=[10,12,14,16,17]; c=[20,21,23,24,37,30];
   1)首先从三个数组中找出第一个元素3,10,20,显然它们的距离为20-3=17;
   2)由于3最小,因此,数组a往后移一个位置,求4,10,20的距离为16,由于16<17,因此,当前数组的最小距离为16;
   3)同理,对数组a后移一个位置,依次类推直到遍历到15的时候,当前遍历到三个数组中的值分别为15,10,20,最小距离为10;
   4)由于10最小,因此,数组b往后移动一个位置遍历12,此时三个数组遍历到的数字分别为15,12,20,距离为8,当前最小距离是8;
   5)由于8最小,数组b往后移动一个位置为14,依然是三个数中最小值,往后移动一个位置为16,当前的最小距离变为5,由于15是数组a的最后一个数字,因此,遍历结束,求得最小距离为5。
   实现代码如下:
   def mins(a,b,c):
   mins=a if a<b else b
   mins=mins if mins<c else c
   return mins
   
   def maxs(a,b,c):
   maxs=b if a<b else a
   maxs=c if maxs<c else maxs
   return maxs
   
   def minDistance(a,b,c):
   aLen=len(a)
   bLen=len(b)
   cLen=len(c)
   curDist=0
   minsd=0
   minDist=2**32
   i=0#数组a的下标
   j=0#数组b的下标
   k=0#数组c的下标
   while True:
   curDist=maxs(abs(a[i]-b[j]),abs(a[i]-c[k]),abs(b[j]-c[k]))
   if curDist<minDist:
   minDist=curDist
   #找出当前遍历到三个数组中的最小值
   minsd=mins(a[i], b[j], c[k])
   if minsd=a[i]:
   i+=1
   if i>=aLen:
   break
   elif minsd==b[j]:
   j+=1
   if j>=bLen:
   break
   else:
   k+=1
   if k>=cLen:
   break
   return minDist
   
   if __name__=="__main__":
   a=[3,4,5,7,15]
   b=[10, 12, 14, 16, 17]
   c=[20, 21, 23, 24, 37, 30]
   print "最小距离为: "+str(minDistance(a,b,c))
   算法性能分析:
   采用这种算法最多只需要对三个数组分别遍历一遍,因此,时间复杂度为O(1+n1+n)。
   方法三:数学运算法
   采用数学方法对目标函数变形,有两个关键点,第一个关键点:
   max {|x1-x2|,|y1-y2|}=(|x1+y1-x2-y2|+|x1-y1-(x2-y2)|)/2 (公式1)
   我们假设x1=a[i],x2=b[j],x3=c[k],则
   Distance=max(|x1-x2|,|x1-x3|,|x2-x3|)=max(max(|x1-x2|,|x1-x3|),|x2-x3|) (公式2)
   根据公式1,max(|x1-x2|,|x1-x3|)=1/2(|2x1-x2-x3|+|x2-x3|),带入公式2,得到
   Distance=max(1/2(|2x1-x2-x3|+|x2-x3|),|x2-x3|)=1/2*max(|2×1-x2-x3|,|x2-x3|)+1/2*|x2-x3|//把相同部分1/2*|x2-x3|分离出来
   =1/2*max(|2x1-(x2+x3)|,|x2-x3|)+1/2*|x2-x3|//把(x2+x3)看成一个整体,使用公式1
   =1/2*1/2*((|2x1-2x2|+|2x1-2x3|)+1/2*|x2-x3|
   =1/2*|x1-x2|+1/2*|x1-x3|+1/2*|x2-x3|
   =1/2*(|x1-x2|+|x1-x3|+|x2-x3|)//求出等价公式,完毕!
   第二个关键点:如何设计算法找到(|x1-x2|+|x1-x3|+|x2-x3|)的最小值,x1,x2,x3,分别是三个数组中的任意一个数,算法思想与方法二相同,用三个下标分别指向a,b,c中最小的数,计算一次它们最大距离的Distance,然后再移动三个数中较小的数组的下标,再计算一次,每次移动一个,直到其中一个数组结束为止。
   示例代码如下:
   def mins(a,b,c):
   mins=a if a<b elseb
   mins=mins if mins<c else c
   return mins
   
   def minDistance(a,b,c):
   aLen=len(a)
   bLen=len(b)
   cLen=len(c)
   MinSum=0# 最小的绝对值和
   Sum=0#计算三个绝对值的和, 与最小值做比较
   MinOFabc=0 # a[i], b[j], c[k]的最小值
   cnt=0# 循环次数统计, 最多是1+m+n次i=0, j=0, k=0//a,b,c三个数组的下标索引
   i=j=k=0
   MinSum=(abs(a[i]-b[j])+abs(a[i]-c[k])+abs(b[j]-c[k]))/2
   cnt=0
   while cnt<=aLen+bLen+cLen:
   Sum=(abs(a[i]-b[j])+abs(a[i]-c[k])+abs(b[j]-c[k]))/2
   MinSum=MinSum if MinSum<Sum else Sum
   MinOFabc=mins(a[i], b[j], c[k])# 找到a[i],b[j],c[k]的最小值
   #判断哪个是最小值, 做相应的索引移动
   if MinOFabc==a[i]:
   i+=1
   if i>=aLen:
   break # a[i]最小, 移动i
   if MinOFabc==b[j]:
   j+=1
   if j>=bLen:
   break #b[j]最小, 移动j
   if MinOFabc==c[k]:
   k+=1
   if k>=cLen:
   break #c[k]最小, 移动k
   cnt+=1
   return MinSum
   
   if __name__=="__main__":
   a=[3,4,5,7,15]
   b=[10,12,14,16,17]
   c=[20,21,23,24,37,30]
   print "最小距离为: "+str(minDistance(a, b, c))
   程序的运行结果为:
   最小距离为:5
   算法性能分析:
   与方法二类似,这种方法最多需要执行(1+m+n)次循环,因此,时间复杂度为O(1+m+n)。
【答案解析】[考点] 如何求解最小三元组距离