在数组中找出三个和的总和最接近给定数字的元素


155

给定一个整数数组A 1,A 2,...,A n,包括负数和正数,以及另一个整数S。现在我们需要在数组中找到三个不同的整数,它们的和最接近给定的整数S如果存在多个解决方案,那么任何一个都可以。

您可以假定所有整数都在int32_t范围内,并且计算总和不会发生算术溢出。S没什么特别,只是一个随机选择的数字。

除了蛮力搜索以外,还有没有其他有效的算法可以找到这三个整数?


1
如果您要寻找一个等于数字(而不是最接近的数字)的总和,这将是3SUM问题
伯恩哈德·巴克

Answers:


186

除了蛮力搜索以外,还有没有其他有效的算法可以找到这三个整数?

是的 我们可以在O(n 2)时间内解决这个问题!首先,考虑P可以用稍有不同的方式等效地表述您的问题,从而消除对“目标值”的需求:

原来的问题P给定一个阵列An整数和目标值S,就存在着从一个3元组A求和以S

修改后的问题P'给定一个整数数组AnA该和到零是否存在3元组?

请注意,您可以从这个版本的问题,去P'P通过从每个元素减去你的S / 3 A,但现在你不需要目标值了。

显然,如果仅测试所有可能的三元组,就可以解决O(n 3)中的问题-这是蛮力基准。有可能做得更好吗?如果我们以更聪明的方式选择元组怎么办?

首先,我们花一些时间对数组进行排序,这使我们付出了O(n log n)的初始代价。现在我们执行以下算法:

for (i in 1..n-2) {
  j = i+1  // Start right after i.
  k = n    // Start at the end of the array.

  while (k >= j) {
    // We got a match! All done.
    if (A[i] + A[j] + A[k] == 0) return (A[i], A[j], A[k])

    // We didn't match. Let's try to get a little closer:
    //   If the sum was too big, decrement k.
    //   If the sum was too small, increment j.
    (A[i] + A[j] + A[k] > 0) ? k-- : j++
  }
  // When the while-loop finishes, j and k have passed each other and there's
  // no more useful combinations that we can try with this i.
}

该算法的工作原理是将三分,ij,和k在阵列中的各个点。i从一开始就开始,然后慢慢地发展到最后。k指向最后一个元素。j指向i起点。我们迭代地尝试对元素在它们各自的索引处进行求和,并且每次发生以下情况之一:

  • 总和是正确的!我们找到了答案。
  • 这个数目太小了。移至j末尾以选择下一个最大的数字。
  • 这个数目太大了。移动k接近开始选择下一个最小的数。

对于每一个i的指针j,并k会逐渐更接近对方。最终,它们将相互传递,并且在那一刻,我们不需要尝试任何其他操作i,因为我们将以相同的顺序对相同的元素求和。在那之后,我们尝试下一个i并重复。

最终,我们将耗尽所有有用的可能性,或者找到解决方案。您可以看到这是O(n 2),因为我们执行了外循环O(n)次,执行了内循环O(n)次。如果您真的很喜欢,可以通过将每个整数表示为一个位向量并执行快速傅里叶变换,来进行次二次级处理,但这超出了此答案的范围。


注意:因为这是一个采访问题,所以我在这里作了一些欺骗:该算法允许多次选择相同的元素。也就是说,(-1,-1,2)和(0,0,0)都是有效的解决方案。正如标题所提到的,它也只能找到确切的答案,而不能找到最接近的答案。作为读者的练习,我将让您找出如何使其仅与不同的元素一起使用(但这是一个非常简单的更改)和确切的答案(这也是一个简单的更改)。


8
看来算法只能找到3元组等于,不至S 最接近于S.
ZelluX

7
ZelluX:正如我在笔记中提到的那样,我不想付出太多,因为这是一个面试问题。希望您能看到如何对其进行修改,以使其获得最接近的答案。(提示:一种方法是跟踪到目前为止最接近的答案,如果找到更好的答案,则将其覆盖。)
John Feminella 2010年

12
如果我们不修改问题说明,该怎么办,我们将搜索aj和ak,得出ai + S。
布尔值

3
@ZelluX:这类似于合并排序的工作方式(这是我第一次单击的方式)。内循环试图做的是证明A [j]或A [k]不能成为任何令人满意的解决方案的一部分。任何时候的问题都是:“是否存在任何对j'> = j和k'<= k,使得A [j] + A [k] = S-A [i]?” 查看当前对(i,j),有3种可能性:总和(停止-我们赢了!),太低或太高。如果它太低,那么对于每个 k'<= k,总和A [j] + A [k']也必须太低,因为在每个这样的总和中,第一项(A [j])将相同。 ..
j_random_hacker 2012年

1
...,第二项(A [k'])将等于或低于A [k]。因此,在这种情况下,我们已经证明A [j]不能参与任何令人满意的总和-因此我们不妨丢弃它!我们可以通过设置j = j + 1并重新开始来实现(尽管这可能有助于从递归方式解决较小的子问题的角度进行思考)。同样,如果总和A [j] + A [k]太高,那么我们知道对于每个 j'> = j,A [j'] + A [k]也必须太高,因为A [j']必须至少与A [j]一样大,而我们已经太高了。这意味着我们可以通过设置k = k-1并重新开始来安全地丢弃A [k]。
j_random_hacker 2012年

28

当然,这是一个更好的解决方案,因为它更易于阅读,因此不易出错。唯一的问题是,我们需要添加几行代码,以避免对一个元素进行多次选择。

另一个O(n ^ 2)解决方案(通过使用哈希集)。

// K is the sum that we are looking for
for i 1..n
    int s1 = K - A[i]
    for j 1..i
        int s2 = s1 - A[j]
        if (set.contains(s2))
            print the numbers
    set.add(A[i])

8
缺点是O(N)存储,而不是就地存储。
查尔斯·芒格

6
使用哈希集并不是严格的O(n ^ 2),因为哈希集在极少数情况下会退化,从而导致线性查找时间最多。
Ext3h 2014年

@Charles-John的解决方案也需要O(N)空间,因为您在排序时更改了原始数组。这意味着调用者在使用该功能之前可能需要防御性副本。
gamliela '16

我认为您的算法有误。s2可能是已经选择的元素。例如,如果数组is 0,1,2Kis 2,则不应有答案。我认为您的算法将输出0,1,1,这显然是不正确的。
Yamcha

7

John Feminella的解决方案有一个错误。

在生产线上

if (A[i] + A[j] + A[k] == 0) return (A[i], A[j], A[k])

我们需要检查i,j,k是否都不同。否则,如果我的目标元素是6并且输入数组包含{3,2,1,7,9,0,-4,6}。如果我打印出总计为6的元组,那么我也将得到0,0,6输出。为了避免这种情况,我们需要以这种方式修改条件。

if ((A[i] + A[j] + A[k] == 0) && (i!=j) && (i!=k) && (j!=k)) return (A[i], A[j], A[k])

2
John Feminella解决方案仅是一种用于解决问题的算法,他还指定了他的解决方案不适用于不同的数字条件,因此您必须对上面留给读者的代码进行一些修改。
EmptyData

3
实际上,由于您总是从j = i + 1开始,所以我永远不会成为j。您应该检查的唯一实际条件是j == k。但是,通过将while循环设置为j <k,可以解决问题而无需冗长的if语句,因为k始终大于j,j始终大于i。
lorenzocastillo

2
这似乎不是对问题的答案,而是对约翰·费米内拉的回答的评论。
Bernhard Barker

6

像O(n ^ 2)这样的东西怎么样

for(each ele in the sorted array)
{
    ele = arr[i] - YOUR_NUMBER;
    let front be the pointer to the front of the array;
    let rear be the pointer to the rear element of the array.;

    // till front is not greater than rear.                    
    while(front <= rear)
    {
        if(*front + *rear == ele)
        {
            print "Found triplet "<<*front<<","<<*rear<<","<<ele<<endl;
            break;
        }
        else
        {
            // sum is > ele, so we need to decrease the sum by decrementing rear pointer.
            if((*front + *rear) > ele)
                decrement rear pointer.
            // sum is < ele, so we need to increase the sum by incrementing the front pointer.
            else
                increment front pointer.
        }
    }

这将查找3个元素的总和是否完全等于您的数字。如果要最接近,可以对其进行修改以记住最小的增量(当前三元组数之间的差异),最后打印对应于最小增量的三元组。


如果您想找到k个元素来求和,那么复杂度是多少?您如何处理?
coder_15

使用这种方法,对于k> = 2,k个元素的复杂度为O(n ^(k-1))。您需要为每个附加求和添加一个外循环。
Ext3h 2014年

5

请注意,我们有一个排序数组。此解决方案与John的解决方案相似,仅在于它求和并且不重复相同的元素。

#include <stdio.h>;

int checkForSum (int arr[], int len, int sum) { //arr is sorted
    int i;
    for (i = 0; i < len ; i++) {
        int left = i + 1;
        int right = len - 1;
        while (right > left) {
            printf ("values are %d %d %d\n", arr[i], arr[left], arr[right]);
            if (arr[right] + arr[left] + arr[i] - sum == 0) {
                printf ("final values are %d %d %d\n", arr[i], arr[left], arr[right]);
                return 1;
            }
            if (arr[right] + arr[left] + arr[i] - sum > 0)
                right--;
            else
                left++;
        }
    }
    return -1;
}
int main (int argc, char **argv) {
    int arr[] = {-99, -45, -6, -5, 0, 9, 12, 16, 21, 29};
    int sum = 4;
    printf ("check for sum %d in arr is %d\n", sum, checkForSum(arr, 10, sum));
}

需要计算的绝对a[r] + a[l] + a[i] - sum。试一下 arr = [-1, 2, 1, -4] sum = 1
Dimitry

3

这是C ++代码:

bool FindSumZero(int a[], int n, int& x, int& y, int& z)
{
    if (n < 3)
        return false;

    sort(a, a+n);

    for (int i = 0; i < n-2; ++i)
    {
        int j = i+1;
        int k = n-1;

        while (k >= j)
        {
            int s = a[i]+a[j]+a[k];

            if (s == 0 && i != j && j != k && k != i)
            {
                x = a[i], y = a[j], z = a[k];
                return true;
            }

            if (s > 0)
                --k;
            else
                ++j;
        }
    }

    return false;
}

2

非常简单的N ^ 2 * logN解决方案:对输入数组进行排序,然后遍历所有对A i,A j(N ^ 2次),并针对每一对检查(S-A i -A j)是否在数组中( logN时间)。

另一个O(S * N)解决方案使用经典的动态编程方法。

简而言之:

创建一个二维数组V [4] [S + 1]。填写方式如下:

V [0] [0] = 1,V [0] [x] = 0;

对于任何i,V 1 [A i ] = 1,对于所有其他x ,V 1 [x] = 0

对于任何i,j,V [2] [A i + A j ] = 1。对于所有其他x,V [2] [x] = 0

V [3] [任何3个元素的和] = 1。

要填充它,请遍历A i,对于每个A i,从右到左遍历数组。


对第一个算法稍作更改..如果该元素不存在,则在二分查找之后,我们必须查看左侧,当前和右侧的元素,以查看哪个元素给出了最接近的结果。
阿努拉格2010年

数组太大,不是O(s * N)。对于任何i,j,此步骤均为O(N ^ 2):V [2] [Ai + Aj] = 1。对于所有其他x,V [2] [x] = 0。
理查德

1

可以在O(n log(n))中有效地解决此问题,如下所示。我正在提供一种解决方案,该解决方案可以告诉任何三个数字的总和是否等于给定数字。

import java.util.*;
public class MainClass {
        public static void main(String[] args) {
        int[] a = {-1, 0, 1, 2, 3, 5, -4, 6};
        System.out.println(((Object) isThreeSumEqualsTarget(a, 11)).toString());
}

public static boolean isThreeSumEqualsTarget(int[] array, int targetNumber) {

    //O(n log (n))
    Arrays.sort(array);
    System.out.println(Arrays.toString(array));

    int leftIndex = 0;
    int rightIndex = array.length - 1;

    //O(n)
    while (leftIndex + 1 < rightIndex - 1) {
        //take sum of two corners
        int sum = array[leftIndex] + array[rightIndex];
        //find if the number matches exactly. Or get the closest match.
        //here i am not storing closest matches. You can do it for yourself.
        //O(log (n)) complexity
        int binarySearchClosestIndex = binarySearch(leftIndex + 1, rightIndex - 1, targetNumber - sum, array);
        //if exact match is found, we already got the answer
        if (-1 == binarySearchClosestIndex) {
            System.out.println(("combo is " + array[leftIndex] + ", " + array[rightIndex] + ", " + (targetNumber - sum)));
            return true;
        }
        //if exact match is not found, we have to decide which pointer, left or right to move inwards
        //we are here means , either we are on left end or on right end
        else {

            //we ended up searching towards start of array,i.e. we need a lesser sum , lets move inwards from right
            //we need to have a lower sum, lets decrease right index
            if (binarySearchClosestIndex == leftIndex + 1) {
                rightIndex--;
            } else if (binarySearchClosestIndex == rightIndex - 1) {
                //we need to have a higher sum, lets decrease right index
                leftIndex++;
            }
        }
    }
    return false;
}

public static int binarySearch(int start, int end, int elem, int[] array) {
    int mid = 0;
    while (start <= end) {
        mid = (start + end) >>> 1;
        if (elem < array[mid]) {
            end = mid - 1;
        } else if (elem > array[mid]) {
            start = mid + 1;
        } else {
            //exact match case
            //Suits more for this particular case to return -1
            return -1;
        }
    }
    return mid;
}
}

我认为这不会起作用。当然,您有两种简单的情况来进行前进,leftIndex或者rightIndex中间的所有元素都严格小于或大于所需的数量。但是,当二进制搜索在中间某处停止时,情况又如何呢?您将需要检查两个分支(where rightIndex--leftIndex++)。在您的解决方案中,您只需忽略这种情况。但是我认为没有办法解决这个问题。
艾凡

0

还原:我认为@John Feminella解O(n2)最优雅。我们仍然可以减少搜索元组的A [n]。当我们的搜索数组很大而SUM很小时,通过观察A [k]使得所有元素都在A [0]-A [k]中。

A [0]最小:-升序排序数组。

s = 2A [0] + A [k]:给定s和A [],我们可以在log(n)时间使用二进制搜索找到A [k]。


0

这是java中的程序,它是O(N ^ 2)

import java.util.Stack;


public class GetTripletPair {

    /** Set a value for target sum */
    public static final int TARGET_SUM = 32;

    private Stack<Integer> stack = new Stack<Integer>();

    /** Store the sum of current elements stored in stack */
    private int sumInStack = 0;
    private int count =0 ;


    public void populateSubset(int[] data, int fromIndex, int endIndex) {

        /*
        * Check if sum of elements stored in Stack is equal to the expected
        * target sum.
        * 
        * If so, call print method to print the candidate satisfied result.
        */
        if (sumInStack == TARGET_SUM) {
            print(stack);
        }

        for (int currentIndex = fromIndex; currentIndex < endIndex; currentIndex++) {

            if (sumInStack + data[currentIndex] <= TARGET_SUM) {
                ++count;
                stack.push(data[currentIndex]);
                sumInStack += data[currentIndex];

                /*
                * Make the currentIndex +1, and then use recursion to proceed
                * further.
                */
                populateSubset(data, currentIndex + 1, endIndex);
                --count;
                sumInStack -= (Integer) stack.pop();
            }else{
            return;
        }
        }
    }

    /**
    * Print satisfied result. i.e. 15 = 4+6+5
    */

    private void print(Stack<Integer> stack) {
        StringBuilder sb = new StringBuilder();
        sb.append(TARGET_SUM).append(" = ");
        for (Integer i : stack) {
            sb.append(i).append("+");
        }
        System.out.println(sb.deleteCharAt(sb.length() - 1).toString());
    }

    private static final int[] DATA = {4,13,14,15,17};

    public static void main(String[] args) {
        GetAllSubsetByStack get = new GetAllSubsetByStack();
        get.populateSubset(DATA, 0, DATA.length);
    }
}

好的方法,但是我无法将结果数限制为三元组。例如,考虑输入:[1,11,3,4,5,6,7,8,2]和12,从您的解决方案看来,[1,11] [4,8] [1,4, 5,2]等都可以。
阿努帕姆·塞尼

0

可以通过对2和问题进行细微修改来在O(n ^ 2)中解决该问题.A是包含元素的向量,B是所需的和。

int解决方案:: threeSumClosest(vector&A,int B){

sort(A.begin(),A.end());

int k=0,i,j,closest,val;int diff=INT_MAX;

while(k<A.size()-2)
{
    i=k+1;
    j=A.size()-1;

    while(i<j)
    {
        val=A[i]+A[j]+A[k];
        if(val==B) return B;
        if(abs(B-val)<diff)
        {
            diff=abs(B-val);
            closest=val;
        }
        if(B>val)
        ++i;
        if(B<val) 
        --j;
    }
    ++k;

}
return closest;

0

这是Python3代码

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        result = set()
        nums.sort()
        L = len(nums)     
        for i in range(L):
            if i > 0 and nums[i] == nums[i-1]:
                continue
            for j in range(i+1,L):
                if j > i + 1 and nums[j] == nums[j-1]:
                    continue  
                l = j+1
                r = L -1
                while l <= r:
                    sum = nums[i] + nums[j] + nums[l]
                    result.add(sum)
                    l = l + 1
                    while l<=r and nums[l] == nums[l-1]:
                        l = l + 1
        result = list(result)
        min = result[0]
        for i in range(1,len(result)):
            if abs(target - result[i]) < abs(target - min):
                min = result[i]
        return min

-1

另一个可以早期检查并失败的解决方案:

public boolean solution(int[] input) {
        int length = input.length;

        if (length < 3) {
            return false;
        }

        // x + y + z = 0  => -z = x + y
        final Set<Integer> z = new HashSet<>(length);
        int zeroCounter = 0, sum; // if they're more than 3 zeros we're done

        for (int element : input) {
            if (element < 0) {
                z.add(element);
            }

            if (element == 0) {
                ++zeroCounter;
                if (zeroCounter >= 3) {
                    return true;
                }
            }
        }

        if (z.isEmpty() || z.size() == length || (z.size() + zeroCounter == length)) {
            return false;
        } else {
            for (int x = 0; x < length; ++x) {
                for (int y = x + 1; y < length; ++y) {
                    sum = input[x] + input[y]; // will use it as inverse addition
                    if (sum < 0) {
                        continue;
                    }
                    if (z.contains(sum * -1)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

我在这里添加了一些单元测试:GivenArrayReturnTrueIfThreeElementsSumZeroTest

如果set使用太多空间,我可以轻松地使用将使用O(n / w)space的java.util.BitSet 。


-1

编程以获取这三个元素。我刚刚首先对数组/列表进行了排序,并minCloseness根据每个三元组对其进行了更新。

public int[] threeSumClosest(ArrayList<Integer> A, int B) {
    Collections.sort(A);
    int ansSum = 0;
    int ans[] = new int[3];
    int minCloseness = Integer.MAX_VALUE;
    for (int i = 0; i < A.size()-2; i++){
        int j = i+1;
        int k = A.size()-1;
        while (j < k){
            int sum = A.get(i) + A.get(j) + A.get(k);
            if (sum < B){
                j++;
            }else{
                k--;
            }
            if (minCloseness >  Math.abs(sum - B)){
                minCloseness = Math.abs(sum - B);
                ans[0] = A.get(i); ans[1] = A.get(j); ans[2] = A.get(k);
            }
        }
    }
    return ans;
}

-2

我是在n ^ 3中完成的,下面是我的伪代码;

//创建一个键为Integer且值为ArrayList的hashMap //使用for循环在列表中迭代,对于列表中的每个值,从下一个值开始再次迭代;

for (int i=0; i<=arr.length-1 ; i++){
    for (int j=i+1; j<=arr.length-1;j++){

//如果arr [i]和arr [j]的总和小于期望的总和,则有可能找到第三个数字,因此另一个for循环

      if (arr[i]+arr[j] < sum){
        for (int k= j+1; k<=arr.length-1;k++)

//在这种情况下,我们现在正在寻找第三个值;如果arr [i]和arr [j]和arr [k]的总和是期望的总和,则通过将arr [i]作为键,然后将arr [j]和arr [k]添加到HashMap中,将它们添加到HashMap中。该键的值中的ArrayList

          if (arr[i]+arr[j]+arr[k] ==  sum){              
              map.put(arr[i],new ArrayList<Integer>());
              map.get(arr[i]).add(arr[j]);
              map.get(arr[i]).add(arr[k]);}

之后,您现在有了一个字典,其中包含代表三个值的所有条目的总和。使用HashMap函数提取所有这些条目。这工作得很好。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.