Trouver trois éléments dans un tableau dont la somme est la plus proche d'un nombre donné

155

Étant donné un tableau d'entiers, A 1 , A 2 , ..., A n , y compris les négatifs et les positifs, et un autre entier S.Nous devons maintenant trouver trois entiers différents dans le tableau, dont la somme est la plus proche de l'entier S donné S'il existe plus d'une solution, l'une d'entre elles est correcte.

Vous pouvez supposer que tous les entiers sont compris dans la plage int32_t, et aucun débordement arithmétique ne se produira lors du calcul de la somme. S n'a rien de spécial mais un nombre choisi au hasard.

Existe-t-il un algorithme efficace autre que la recherche par force brute pour trouver les trois entiers?

ZelluX
la source
1
Si vous recherchez une somme égale à un nombre (et non la plus proche), ce serait le problème 3SUM .
Bernhard Barker

Réponses:

186

Existe-t-il un algorithme efficace autre que la recherche par force brute pour trouver les trois entiers?

Oui; nous pouvons résoudre cela en temps O (n 2 )! Tout d'abord, considérez que votre problème Ppeut être formulé de manière équivalente d'une manière légèrement différente qui élimine le besoin d'une «valeur cible»:

problème d'origine P: étant donné un tableau Ad' nentiers et une valeur cible S, existe-t-il un 3-tuple de Acette somme à S?

problème modifié P': étant donné un tableau Ad' nentiers, existe-t-il un 3-tuple de Acette somme à zéro?

Notez que vous pouvez passer de cette version du problème P'de Pen soustrayant votre S / 3 de chaque élément A, mais maintenant vous n'avez pas besoin de la valeur cible plus.

Clairement, si nous testons simplement tous les 3 tuples possibles, nous résoudrons le problème en O (n 3 ) - c'est la ligne de base de la force brute. Est-il possible de faire mieux? Et si nous choisissions les tuples d'une manière un peu plus intelligente?

Tout d'abord, nous investissons du temps pour trier le tableau, ce qui nous coûte une pénalité initiale de O (n log n). Maintenant, nous exécutons cet algorithme:

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.
}

Cet algorithme fonctionne en plaçant trois pointeurs, i, jet kà divers points du tableau. icommence par le début et avance lentement jusqu'à la fin. kpointe vers le tout dernier élément. jindique où ia commencé. Nous essayons itérativement de faire la somme des éléments à leurs indices respectifs, et à chaque fois l'un des événements suivants se produit:

  • La somme est exactement la bonne! Nous avons trouvé la réponse.
  • La somme était trop petite. Rapprochez-vous jde la fin pour sélectionner le prochain plus grand nombre.
  • La somme était trop importante. Rapprochez- kvous du début pour sélectionner le plus petit nombre suivant.

Pour chacun i, les pointeurs de jet kse rapprochent progressivement les uns des autres. Finalement, ils se croiseront, et à ce stade, nous n'avons pas besoin d'essayer autre chose pour cela i, puisque nous additionnerions les mêmes éléments, juste dans un ordre différent. Après ce point, nous essayons le suivant iet répétons.

Finalement, soit nous épuiserons les possibilités utiles, soit nous trouverons la solution. Vous pouvez voir que c'est O (n 2 ) puisque nous exécutons la boucle externe O (n) fois et nous exécutons la boucle interne O (n) fois. Il est possible de le faire de manière sous-quadratique si vous avez vraiment envie, en représentant chaque entier comme un vecteur de bits et en effectuant une transformation de Fourier rapide, mais cela dépasse le cadre de cette réponse.


Remarque: Comme il s'agit d'une question d'entretien, j'ai un peu triché ici: cet algorithme permet de sélectionner plusieurs fois le même élément. Autrement dit, (-1, -1, 2) serait une solution valide, comme le ferait (0, 0, 0). Il ne trouve également que les réponses exactes , pas la réponse la plus proche, comme l'indique le titre. En guise d'exercice pour le lecteur, je vais vous laisser comprendre comment le faire fonctionner avec des éléments distincts uniquement (mais c'est un changement très simple) et des réponses exactes (ce qui est également un changement simple).

John Feminella
la source
8
Il semble que l'algorithme ne puisse trouver que 3-tuple égal à S, pas le plus proche de S.
ZelluX
7
ZelluX: Comme je l'ai mentionné dans la note, je ne voulais pas trop en dire car c'est un problème d'entretien. J'espère que vous pourrez voir comment le modifier pour qu'il vous apporte la réponse la plus proche, cependant. (Astuce: une façon est de garder une trace de la réponse la plus proche jusqu'à présent et de l'écraser si vous en trouvez une meilleure.)
John Feminella
12
et si nous ne modifions pas l'énoncé du problème, nous chercherons plutôt aj et ak cette somme à ai + S.
Booléen
3
@ZelluX: C'est similaire au fonctionnement d'un tri par fusion (c'est comme ça que ça a cliqué pour moi). Ce que cette boucle interne essaie de faire est de prouver que A [j] ou A [k] ne peuvent faire partie d' aucune solution satisfaisante. Le problème à tout moment est: "Y a-t-il une paire j '> = j et k' <= k telle que A [j] + A [k] = S - A [i]?" En regardant la paire actuelle (i, j), il y a 3 possibilités: la somme est allumée (arrêtez - nous avons gagné!), Elle est trop faible ou elle est trop élevée. Si elle est trop faible, alors la somme A [j] + A [k '] doit aussi être trop faible pour tout k' <= k, puisque dans chacune de ces sommes, le premier terme (A [j]) sera le même. ..
j_random_hacker
1
... et le deuxième terme (A [k ']) sera identique ou même inférieur à A [k]. Donc, dans ce cas, nous avons prouvé que A [j] ne peut participer à aucune somme satisfaisante - nous pouvons donc tout aussi bien la rejeter! Ce que nous faisons en définissant j = j + 1 et en recommençant (même si cela peut aider à penser en termes de résolution récursive d'un sous-problème plus petit). De même si la somme A [j] + A [k] est trop élevée, alors on sait que A [j '] + A [k] doit aussi être trop élevée pour tout j'> = j, puisque A [j '] doit être au moins aussi grand que A [j] et nous sommes déjà trop élevés. Cela signifie que nous pouvons éliminer A [k] en toute sécurité en définissant k = k-1 et en recommençant.
j_random_hacker
28

c'est certainement une meilleure solution car elle est plus facile à lire et donc moins sujette aux erreurs. Le seul problème est que nous devons ajouter quelques lignes de code pour éviter la sélection multiple d'un élément.

Une autre solution O (n ^ 2) (en utilisant un hashset).

// 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])
Cem
la source
8
L'inconvénient est le stockage O (N), plutôt que de le faire en place.
Charles Munger
6
L'utilisation d'un hashset n'est pas strict O (n ^ 2) car le hash set peut dégénérer en de rares occasions, entraînant des temps de recherche jusqu'à linéaires.
Ext3h du
@Charles - La solution de John a également besoin d'espace O (N), puisque vous modifiez le tableau d'origine pendant le tri. Cela signifie que l'appelant peut avoir besoin d'une copie défensive avant d'utiliser la fonction.
gamliela
Je pense qu'il y a une erreur dans votre algorithme. s2peut être un élément déjà sélectionné. Par exemple, si le tableau est 0,1,2et Kest 2, il ne devrait pas y avoir de réponse. Je pense que votre algorithme produira 0,1,1ce qui est évidemment incorrect.
Yamcha
7

La solution de John Feminella a un bug.

À la ligne

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

Nous devons vérifier si i, j, k sont tous distincts. Sinon, si mon élément cible est 6et si mon tableau d'entrée contient {3,2,1,7,9,0,-4,6}. Si j'imprime les tuples qui totalisent 6, alors j'obtiendrai également 0,0,6en sortie. Pour éviter cela, nous devons modifier la condition de cette manière.

if ((A[i] + A[j] + A[k] == 0) && (i!=j) && (i!=k) && (j!=k)) return (A[i], A[j], A[k])
Rambo7
la source
2
La solution de John Feminella consiste simplement à présenter l'algorithme pour résoudre le problème, il a également spécifié que sa solution ne fonctionnerait pas pour une condition de nombre distinct et vous devez modifier un peu le code ci-dessus qu'il a laissé au lecteur.
EmptyData
3
En fait, je ne serai jamais j puisque vous le démarrez toujours à j = i + 1. La seule condition réelle que vous devriez vérifier est si j == k. Cependant, en définissant la boucle while sur j <k, vous avez résolu les problèmes sans une longue instruction if puisque k sera toujours supérieur à j et j toujours supérieur à i.
lorenzocastillo le
2
Cela ne semble pas être une réponse à la question, mais plutôt un commentaire sur la réponse de John Feminella.
Bernhard Barker
6

Que diriez-vous de quelque chose comme ça, qui est 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.
        }
    }

Cela trouve si la somme de 3 éléments est exactement égale à votre nombre. Si vous voulez le plus proche, vous pouvez le modifier pour mémoriser le plus petit delta (différence entre votre nombre de triplet courant) et à la fin imprimer le triplet correspondant au plus petit delta.

codaddict
la source
si vous voulez trouver k éléments pour obtenir la somme quelle est la complexité? Comment gérez-vous cela?
coder_15
Avec cette approche, la complexité pour k éléments est O (n ^ (k-1)) pour k> = 2. Vous devez ajouter une boucle externe pour chaque sommation supplémentaire.
Ext3h
5

Notez que nous avons un tableau trié. Cette solution est similaire à la solution de John uniquement en ce qu'elle recherche la somme et ne répète pas le même élément.

#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));
}
Pasada
la source
Il est nécessaire de calculer la différence absolue de a[r] + a[l] + a[i] - sum. Essayez arr = [-1, 2, 1, -4] sum = 1.
Dimitry
3

Voici le code 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;
}
Peter Lee
la source
2

Solution N ^ 2 * logN très simple: trier le tableau d'entrée, puis parcourir toutes les paires A i , A j (temps N ^ 2), et pour chaque paire vérifier si (S - A i - A j ) est dans le tableau ( heure logN).

Une autre solution O (S * N) utilise une approche de programmation dynamique classique .

En bref:

Créez un tableau 2D V [4] [S + 1]. Remplissez-le de telle manière que:

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

V 1 [A i ] = 1 pour tout i, V 1 [x] = 0 pour tout autre x

V [2] [A i + A j ] = 1, pour tout i, j. V [2] [x] = 0 pour tous les autres x

V [3] [somme de 3 éléments quelconques] = 1.

Pour le remplir, parcourez A i , pour chaque A i, parcourez le tableau de droite à gauche.

Olexiy
la source
léger changement du premier algorithme .. si l'élément n'existe pas, alors à la fin de la recherche binaire, il faudrait regarder l'élément à gauche, courant et droit pour voir lequel donne le résultat le plus proche .
Anurag
Le tableau est trop grand et ce n'est pas O (s * N). Cette étape est O (N ^ 2): V [2] [Ai + Aj] = 1, pour tout i, j. V [2] [x] = 0 pour tous les autres x.
Richard
1

Cela peut être résolu efficacement en O (n log (n)) comme suit. Je donne une solution qui indique si la somme de trois nombres est égale à un nombre donné.

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;
}
}
Raju Singh
la source
Je ne pense pas que cela fonctionnera. Certes, vous avez deux cas simples pour avancer leftIndexou rightIndexlorsque tous les éléments du milieu sont soit strictement plus petits ou plus grands que le nombre souhaité. Mais qu'en est-il du cas où la recherche binaire s'est arrêtée quelque part au milieu? Vous devrez vérifier les deux branches (où rightIndex--et leftIndex++). Dans votre solution, vous ignorez simplement cette situation. Mais je ne pense pas qu'il existe un moyen de surmonter ce problème.
Aivean
0

Réduction: Je pense que la solution O (n2) de @John Feminella est la plus élégante. Nous pouvons encore réduire le A [n] dans lequel rechercher un tuple. En observant A [k] de telle sorte que tous les éléments soient dans A [0] - A [k], lorsque notre tableau de recherche est énorme et SUM (s) vraiment petit.

Un [0] est au minimum: - Tableau trié croissant.

s = 2A [0] + A [k]: Étant donné s et A [], nous pouvons trouver A [k] en utilisant une recherche binaire en log (n) temps.

d1val
la source
0

Voici le programme en java qui est 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);
    }
}
M Sach
la source
Belle approche, mais je n'ai pas pu obtenir le point où vous limitez le nombre de résultats à un triplet. Par exemple, considérez l'entrée: [1,11,3,4,5,6,7,8, 2] et la somme 12, d'après votre solution, il apparaît que [1, 11] [4,8] [1,4, 5,2] etc. fonctionneraient tous.
Anupam Saini
0

Le problème peut être résolu en O (n ^ 2) en étendant le problème à 2 sommes avec des modifications mineures: A est le vecteur contenant les éléments et B est la somme requise.

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;
Anurag Semwal
la source
0

Voici le code 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
uday reddy
la source
-1

Une autre solution qui vérifie et échoue tôt:

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

J'ai ajouté quelques tests unitaires ici: GivenArrayReturnTrueIfThreeElementsSumZeroTest .

Si l'ensemble utilise trop d'espace, je peux facilement utiliser un java.util.BitSet qui utilisera l' espace O (n / w) .

moxi
la source
-1

Programme pour obtenir ces trois éléments. Je viens de trier le tableau / la liste en premier et les mettre à jour en minClosenessfonction de chaque triplet.

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;
}
Saurav Sahu
la source
-2

J'ai fait ça en n ^ 3, mon pseudocode est ci-dessous;

// Créer un hashMap avec la clé comme Integer et la valeur comme ArrayList // itérer dans la liste en utilisant une boucle for, pour chaque valeur de la liste, itérer à nouveau à partir de la valeur suivante;

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

// si la somme de arr [i] et arr [j] est inférieure à la somme souhaitée, alors il est possible de trouver un troisième chiffre alors faites-en une autre boucle for

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

// dans ce cas, nous recherchons maintenant la troisième valeur; si la somme de arr [i] et arr [j] et arr [k] est la somme souhaitée, ajoutez-les au HashMap en faisant de arr [i] la clé puis en ajoutant arr [j] et arr [k] dans le ArrayList dans la valeur de cette clé

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

après cela, vous avez maintenant un dictionnaire qui contient toutes les entrées représentant les trois valeurs s'ajoutant à la somme souhaitée. Extrayez toutes ces entrées à l'aide des fonctions HashMap. Cela a parfaitement fonctionné.

sous zéro
la source