合計が指定された数値に最も近い配列の3つの要素を見つける


155

整数の配列が与えられた場合、A 1、A 2、...、A n負の値と正の値を含む、および別の整数Sが与えられたとします。次に、配列内で3つの異なる整数を見つける必要があります。その合計は、指定された整数Sに最も近くなります。 。複数のソリューションが存在する場合、それらのいずれも問題ありません。

すべての整数がint32_tの範囲内にあると想定でき、合計の計算時に算術オーバーフローは発生しません。Sは特別なものではなく、ランダムに選ばれた数です。

3つの整数を見つけるためのブルートフォース検索以外の効率的なアルゴリズムはありますか?


1
数値に等しい(そして最も近くない)合計を探している場合、これは3SUMの問題です。
Bernhard Barker

回答:


186

3つの整数を見つけるためのブルートフォース検索以外の効率的なアルゴリズムはありますか?

うん。これをO(n 2)時間で解決できます!まず、問題Pを「目標値」の必要性を排除するわずかに異なる方法で同等に表現できることを考慮してください。

元の問題P整数の配列とターゲット値Aが与えられたn場合SAその合計から3タプルは存在しSますか?

修正された問題P'整数の配列Aが与えられたn場合、Aその合計からゼロまでの3タプルは存在しますか?

問題のこのバージョンから移動できることに注意してください P'P各要素からS / 3を引くからA、現在は目標値は必要ありません。

明らかに、考えられるすべての3タプルを単純にテストすると、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.
}

このアルゴリズムは、3つのポインタを置くことによって動作ijおよびk配列内の様々なポイントで。i最初から始まり、ゆっくりと最後まで進んでいきます。k最後の要素を指します。jどこiから始まったかを示します。それぞれのインデックスの要素を繰り返し合計し、次のいずれかが発生するたびに試みます。

  • 金額は正確です!答えを見つけました。
  • 合計が小さすぎます。移動しj、次の最大数を選択するために、近い終わりまで。
  • 合計が大きすぎます。移動しk、次の最小数を選択するために、近い先頭に。

それぞれについてi、のポインタjとは、k徐々にお互いに近づくだろう。結局、彼らはお互いを通過し、その時点で私たちはそのために何もする必要はありません。i同じ要素を異なる順序で合計する。その後、次を試してi繰り返します。

最終的には、有用な可能性を使い果たすか、解決策を見つけるでしょう。外側のループをO(n)回実行し、内側のループをO(n)回実行しているため、これはO(n 2)であることがわかります。各整数をビットベクトルとして表現し、高速フーリエ変換を実行することにより、本当に奇妙な場合は、これを準二次的に行うことは可能ですが、それはこの回答の範囲を超えています。


注:これはインタビューの質問なので、ここで少し浮気しました。このアルゴリズムでは、同じ要素を複数回選択できます。つまり、(-1、-1、2)は(0、0、0)と同様に有効なソリューションです。また、タイトルが言及しているように、最も近い答えではなく、正確な答えのみを検索します。読者への演習として、個別の要素のみで機能させる方法(ただし、これは非常に単純な変更です)と正確な回答(これも単純な変更です)を説明します。


8
アルゴリズムはわずか3タプル見つけることができるようだ等しくない、Sへの最も近い S.へ
ZelluX

7
ZelluX:メモで述べたように、インタビューの問題なので、あまり多くを与えたくありませんでした。うまくいけば、最も近い答えが得られるように変更する方法を確認できます。(ヒント:1つの方法は、これまでで最も近い回答を追跡し、より適切な回答を見つけた場合は上書きすることです。)
John Feminella

12
問題のステートメントを変更しない場合は、代わりにai + Sになる和とakを検索します。
ブール値

3
@ZelluX:マージソートの仕組みに似ています(それが最初にクリックされた方法です)。その内部ループが実行しようとしていることは、A [j]またはA [k]のいずれも、満足できるソリューションの一部ではないことを証明することです。任意の時点での問題は、「A [j] + A [k] = S-A [i]となるようなj '> = jとk' <= kのペアはありますか?」現在のペア(i、j)を見ると、3つの可能性があります。合計が強すぎる(停止-勝った!)、低すぎる、または高すぎる。それが低すぎる場合、合計A [j] + A [k ']もすべての k' <= kに対して低すぎる必要があります。これは、各合計で最初の項(A [j])が同じになるためです。 ..
j_random_hacker 2012年

1
...そして、2番目の項(A [k '])は、A [k]と同じか、それよりも低くなります。したがって、この場合には、我々はA [j]がに参加できないことを証明されているすべての私たちは同様にそれを破棄してもよいので、 -満たす合計!j = j + 1に設定してやり直すことで、これを行います(代わりに、より小さな副問題を再帰的に解決するという観点から考えると役立つ場合があります)。同様に、A [j] + A [k]の合計が高すぎる場合、A [j '] + A [k]もすべての j'> = j に対して高すぎる必要があることがわかります。少なくともA [j]と同じ大きさでなければならず、すでに高すぎます。これは、k = k-1に設定して最初からやり直すことで、A [k]を安全に破棄できることを意味します。
j_random_hacker

28

確かに、これは読みやすく、エラーが発生しにくいため、より優れたソリューションです。唯一の問題は、1つの要素が複数選択されないようにするために、数行のコードを追加する必要があることです。

別の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)ストレージです。
Charles Munger、

6
ハッシュセットの使用は厳密なO(n ^ 2)ではありません。これは、ハッシュセットがまれに縮退し、結果としてルックアップ時間が線形になる場合があるためです。
Ext3h 2014

@Charles-また、ソート中に元の配列を変更するため、JohnのソリューションにはO(N)スペースが必要です。つまり、関数を使用する前に、呼び出し元が防御コピーを必要とする可能性があります。
gamliela 2016

アルゴリズムにエラーがあると思います。s2すでに選択されている要素である可能性があります。たとえば、配列が0,1,2とである場合、答えK2ありません。私はあなたのアルゴリズム0,1,1が明らかに正しくないものを出力すると思います。
ヤムチャ

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から開始するため、iがjになることはありません。チェックする必要がある実際の条件は、j == kかどうかだけです。ただし、whileループをj <kに設定すると、kは常にjより大きく、jは常にiよりも大きいため、長いifステートメントを使用しなくても問題を解決できます。
lorenzocastillo 2016

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要素の複雑性はO>(n ^(k-1))for k> = 2です。追加の加数ごとに外部ループを追加する必要があります。
Ext3h 2014

5

ソートされた配列があることに注意してください。このソリューションは、合計を検索し、同じ要素を繰り返さないという点でのみ、ジョンのソリューションに似ています。

#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

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)ソリューションは、古典的な動的計画法を使用します手法をます。

要するに:

2次元配列V [4] [S + 1]を作成します。次のように入力します。

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

V 1 [A i ] = 1、任意のi、V 1 [x] = 0、その他すべてのx

V [2] [A i + A j ] = 1、任意のi、j。V [2] [x] =他のすべてのxに対して0

V [3] [3つの要素の合計] = 1。

Aを通じて、反復をそれを埋めるために、私は、各Aのために、私は、右から左に配列を反復処理。


最初のアルゴリズムへのわずかな変更..要素が存在しない場合、バイナリ検索の最後に、左、現在、および右の要素を調べて、最も近い結果を与える要素を確認する必要があります。 。
Anurag

配列が大きすぎてO(s * N)ではありません。このステップはO(N ^ 2):V [2] [Ai + Aj] = 1で、任意のi、jについてです。他のすべてのxの場合、V [2] [x] = 0。
Richard

1

これは、次のようにO(n log(n))で効率的に解決できます。私は、3つの数値の合計が指定された数値と等しいかどうかを判断するソリューションを提供しています。

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真ん中のすべての要素が目的の数よりも厳密に小さいか大きい場合の2つの単純なケースがあります。しかし、バイナリ検索が途中で停止した場合はどうでしょうか?両方のブランチをチェックする必要があります(rightIndex--およびleftIndex++)。ソリューションでは、この状況を無視するだけです。しかし、この問題を克服する方法はないと思います。
Aivean

0

削減:@John Feminella解O(n2)が最もエレガントだと思います。タプルを検索するためのA [n]を減らすこともできます。すべての要素がA [0]-A [k]になるようにA [k]を観察することにより、検索配列が巨大でSUM(s)が本当に小さい場合。

A [0]は最小です:-昇順でソートされた配列。

s = 2A [0] + A [k]:sとA []が与えられると、log(n)時間のバイナリ検索を使用してA [k]を見つけることができます。


0

ここにO(N ^ 2)であるjavaのプログラムがあります

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);
    }
}

素晴らしいアプローチですが、結果の数を3つに制限するという点は理解できませんでした。たとえば、次の入力を考えます:[1,11,3,4,5,6,7,8、2]と合計12は、ソリューションから、[1、11] [4,8] [1,4、 5,2]などはすべて機能します。
Anupam Saini、

0

この問題は、O(n ^ 2)で2合計問題を少し修正して拡張することで解決できます。Aは要素を含むベクトルで、Bは必要な合計です。

int Solution :: 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

セットが使用するスペースが多すぎる場合は、O(n / w)スペースを使用するjava.util.BitSetを簡単に使用できます。


-1

これらの3つの要素を取得するプログラム。最初に配列/リストをソートし、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]の合計が必要な合計よりも小さい場合、3番目の数字が見つかる可能性があるため、別のforループを実行します

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

//この場合、3番目の値を探しています。arr [i]とarr [j]とarr [k]の合計が必要な合計である場合、arr [i]をキーにしてから、arr [j]とarr [k]を追加して、これらを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]);}

これで、目的の合計に追加される3つの値を表すすべてのエントリを持つディクショナリができました。HashMap関数を使用してこれらすべてのエントリを抽出します。これは完全に機能しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.