Trovare tre elementi in un array la cui somma è più vicina a un determinato numero


155

Dato un array di numeri interi, A 1 , A 2 , ..., A n , inclusi negativi e positivi, e un altro numero intero S. Ora dobbiamo trovare tre numeri interi diversi nella matrice, la cui somma è più vicina al numero intero indicato S Se esiste più di una soluzione, ognuna di esse è ok.

Si può presumere che tutti gli interi siano compresi nell'intervallo int32_t e non si verificherà alcun overflow aritmetico nel calcolo della somma. S non è altro che un numero scelto casualmente.

Esiste un algoritmo efficiente diverso dalla ricerca della forza bruta per trovare i tre numeri interi?


1
Se stai cercando una somma pari a un numero (e non più vicino), questo sarebbe il problema 3SUM .
Bernhard Barker,

Risposte:


186

Esiste un algoritmo efficiente diverso dalla ricerca della forza bruta per trovare i tre numeri interi?

Sì; possiamo risolverlo in O (n 2 ) tempo! Innanzitutto, considera che il tuo problema Ppuò essere espresso in modo equivalente in un modo leggermente diverso che elimina la necessità di un "valore target":

problema originale P: dati un array Adi nnumeri interi e un valore target S, esiste una 3 tupla da Aquella somma a S?

problema modificato P': data una matrice Adi nnumeri interi, esiste una 3 tupla da Aquella somma a zero?

Si noti che si può andare da questa versione del problema P'dal Psottraendo la vostra S / 3 da ogni elemento A, ma ora non è necessario più il valore di riferimento.

Chiaramente, se semplicemente testassimo tutte le possibili 3 tuple, risolveremmo il problema in O (n 3 ) - questa è la linea di base della forza bruta. È possibile fare di meglio? E se prendessimo le tuple in un modo un po 'più intelligente?

Innanzitutto, investiamo del tempo per ordinare l'array, il che ci costa una penalità iniziale di O (n log n). Ora eseguiamo questo algoritmo:

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

Questo algoritmo funziona inserendo tre puntatori, i, j, e kin vari punti della matrice. iinizia all'inizio e lentamente si fa strada fino alla fine. kindica l'ultimo elemento. jindica dove iè iniziato. Proviamo iterativamente a sommare gli elementi nei rispettivi indici e ogni volta si verifica una delle seguenti condizioni:

  • La somma è esattamente giusta! Abbiamo trovato la risposta.
  • La somma era troppo piccola. Spostare jpiù vicino alla fine per selezionare il più grande numero successivo.
  • La somma era troppo grande. Avvicinati kall'inizio per selezionare il numero più piccolo successivo.

Per ciascuno i, i puntatori je ksi avvicineranno gradualmente l'uno all'altro. Alla fine si passeranno l'un l'altro, e a quel punto non avremo bisogno di provare nient'altro per questo i, dato che sommeremmo gli stessi elementi, solo in un ordine diverso. Dopo quel punto, proviamo il prossimo ie ripetiamo.

Alla fine esauriremo le possibilità utili o troveremo la soluzione. Potete vedere che questo è O (n 2 ) poiché eseguiamo il ciclo esterno O (n) volte e eseguiamo il ciclo interno O (n) volte. È possibile farlo in modo sub-quadratico se si diventa davvero fantasiosi, rappresentando ogni numero intero come un vettore bit ed eseguendo una trasformata di Fourier veloce, ma questo va oltre lo scopo di questa risposta.


Nota: poiché questa è una domanda di intervista, ho tradito un po 'qui: questo algoritmo consente la selezione dello stesso elemento più volte. Cioè, (-1, -1, 2) sarebbe una soluzione valida, come lo sarebbe (0, 0, 0). Trova anche solo le risposte esatte , non la risposta più vicina, come menziona il titolo. Come esercizio per il lettore, ti farò capire come farlo funzionare solo con elementi distinti (ma è un cambiamento molto semplice) e risposte esatte (che è anche un semplice cambiamento).


8
Sembra che l'algoritmo possa trovare solo 3 tuple uguale a S, non più vicino a S.
ZelluX

7
ZelluX: Come ho già detto nella nota, non volevo dare troppo in quanto si tratta di un problema di intervista. Spero che tu possa vedere come modificarlo in modo che ti dia la risposta più vicina, però. (Suggerimento: un modo è tenere traccia della risposta più vicina finora e sovrascriverla se ne trovi una migliore.)
John Feminella

12
cosa succede se non modifichiamo l'istruzione del problema, invece cercheremo aj e ak che sommano a ai + S.
Booleano

3
@ZelluX: è simile a come funziona un ordinamento di tipo merge (è così che mi ha fatto clic per la prima volta). Ciò che quel ciclo interno sta cercando di fare è dimostrare che A [j] o A [k] non possono far parte di alcuna soluzione soddisfacente. Il problema in qualsiasi momento è: "Esiste una coppia j '> = j e k' <= k tale che A [j] + A [k] = S - A [i]?" Guardando l'attuale coppia (i, j), ci sono 3 possibilità: la somma è bang (stop - abbiamo vinto!), È troppo bassa o è troppo alta. Se è troppo basso, anche la somma A [j] + A [k '] deve essere troppo bassa per ogni k' <= k, poiché in ciascuna di tali somme il primo termine (A [j]) sarà lo stesso. ..
j_random_hacker,

1
... e il secondo termine (A [k ']) sarà uguale o addirittura inferiore a A [k]. Quindi in questo caso abbiamo dimostrato che A [j] non può partecipare ad alcuna somma soddisfacente, quindi possiamo anche scartarla! Cosa che facciamo impostando j = j + 1 e ricominciando (anche se potrebbe aiutare a pensare in termini di risoluzione ricorsiva di un sottoproblema invece ricorsivamente). Allo stesso modo se la somma A [j] + A [k] è troppo alta, allora sappiamo che anche A [j '] + A [k] deve essere troppo alto per ogni j'> = j, poiché A [j '] deve essere grande almeno quanto A [j] e siamo già troppo alti. Questo significa che possiamo scartare in sicurezza A [k] impostando k = k-1 e ricominciando.
j_random_hacker,

28

sicuramente questa è una soluzione migliore perché è più facile da leggere e quindi meno soggetta a errori. L'unico problema è che è necessario aggiungere alcune righe di codice per evitare la selezione multipla di un elemento.

Un'altra soluzione O (n ^ 2) (usando 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])

8
Il rovescio della medaglia è la memoria O (N), piuttosto che farlo sul posto.
Charles Munger,

6
L'uso di un hashset non è rigoroso O (n ^ 2) poiché il set di hash può degenerare in rare occasioni, con conseguenti tempi di ricerca lineari.
Ext3h

@Charles - Anche la soluzione di John ha bisogno di spazio O (N), poiché si modifica l'array originale durante l'ordinamento. Ciò significa che il chiamante potrebbe aver bisogno di una copia difensiva prima di utilizzare la funzione.
gamliela,

Penso che ci sia un errore nel tuo algoritmo. s2potrebbe essere un elemento già selezionato. Ad esempio, se l'array è 0,1,2ed Kè 2, non dovrebbe esserci una risposta. Penso che il tuo algoritmo produrrà il 0,1,1che è ovviamente errato.
Yamcha,

7

La soluzione di John Feminella ha un bug.

Alla linea

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

Dobbiamo verificare se i, j, k sono tutti distinti. Altrimenti, se il mio elemento target è 6e se il mio array di input contiene {3,2,1,7,9,0,-4,6}. Se stampo le tuple che ammontano a 6, otterrei anche 0,0,6come output. Per evitare ciò, dobbiamo modificare la condizione in questo modo.

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

2
La soluzione di John Feminella è solo quella di presentare l'algoritmo per risolvere il problema, ha anche specificato che la sua soluzione non funzionerebbe per una condizione di numero distinto e devi modificare un po 'sopra il codice che ha lasciato al lettore.
EmptyData

3
In realtà, non sarò mai j poiché lo stai sempre avviando da j = i + 1. L'unica vera condizione che dovresti controllare è se j == k. Tuttavia, impostando il ciclo while su j <k, hai risolto i problemi senza una lunga istruzione if poiché k sarà sempre maggiore di j e j sempre maggiore di i.
Lorenzocastillo,

2
Questa non sembra una risposta alla domanda, ma piuttosto un commento sulla risposta di John Feminella.
Bernhard Barker,

6

Che ne dici di qualcosa del genere, che è 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.
        }
    }

Questo trova se la somma di 3 elementi è esattamente uguale al tuo numero. Se si desidera il più vicino, è possibile modificarlo per ricordare il delta più piccolo (differenza tra il numero della tripletta corrente) e alla fine stampare la tripletta corrispondente al delta più piccolo.


se vuoi trovare k elementi per ottenere la somma qual è la complessità? Come gestisci questo?
coder_15,

Con questo approccio, la complessità per k elementi è O (n ^ (k-1)) per k> = 2. È necessario aggiungere un ciclo esterno per ogni summand aggiuntivo.
Ext3h

5

Si noti che abbiamo un array ordinato. Questa soluzione è simile alla soluzione di John solo che cerca la somma e non ripete lo stesso elemento.

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

È necessario per calcolare la differenza assoluta di a[r] + a[l] + a[i] - sum. Prova arr = [-1, 2, 1, -4] sum = 1.
Dimitry,

3

Ecco il codice 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

Soluzione N ^ 2 * logN molto semplice: ordina l'array di input, quindi passa attraverso tutte le coppie A i , A j (tempo N ^ 2) e per ogni coppia controlla se (S - A i - A j ) è in array ( ora logN).

Un'altra soluzione O (S * N) utilizza la programmazione dinamica classica approccio di .

In breve:

Crea un array 2-d V [4] [S + 1]. Riempilo in modo tale che:

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

V 1 [A i ] = 1 per qualsiasi i, V 1 [x] = 0 per tutti gli altri x

V [2] [A i + A j ] = 1, per qualsiasi i, j. V [2] [x] = 0 per tutti gli altri x

V [3] [somma di 3 elementi] = 1.

Per riempirlo, scorrere tra A i , per ogni A i scorrere attraverso l'array da destra a sinistra.


leggera modifica al primo algoritmo .. se l'elemento non esiste, quindi alla fine della ricerca binaria, dovremmo guardare l'elemento a sinistra, corrente e a destra per vedere quale dà il risultato più vicino .
Anurag,

La matrice è troppo grande e non è O (s * N). Questo passaggio è O (N ^ 2): V [2] [Ai + Aj] = 1, per qualsiasi i, j. V [2] [x] = 0 per tutti gli altri x.
Richard,

1

Questo può essere risolto in modo efficiente in O (n log (n)) come segue. Sto dando una soluzione che dice se la somma di tre numeri uguali a un dato numero.

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

Non penso che funzionerà. Certo, hai due semplici casi su come avanzare leftIndexo rightIndexquando tutti gli elementi nel mezzo sono rigorosamente più piccoli o più grandi del numero desiderato. Ma che dire del caso in cui la ricerca binaria si è fermata da qualche parte nel mezzo? Dovresti controllare entrambi i rami (dove rightIndex--e leftIndex++). Nella tua soluzione ignori questa situazione. Ma non penso che ci sia un modo per superare questo problema.
Aivean,

0

Riduzione: penso che la soluzione O (n2) di John Feminella sia la più elegante. Possiamo ancora ridurre la A [n] in cui cercare la tupla. Osservando A [k] tale che tutti gli elementi sarebbero in A [0] - A [k], quando il nostro array di ricerca è enorme e SUM (s) davvero piccolo.

Un [0] è minimo: - Matrice ordinata crescente.

s = 2A [0] + A [k]: dati se A [] possiamo trovare A [k] usando la ricerca binaria nel tempo log (n).


0

Ecco il programma in Java che è 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);
    }
}

Ottimo approccio, ma non sono riuscito a capire il punto in cui limitare il numero di risultati a una tripletta. Ad esempio, considera l'input: [1,11,3,4,5,6,7,8, 2] e somma 12, dalla tua soluzione sembra che [1, 11] [4,8] [1,4, 5,2] ecc. Funzionerebbero tutti.
Anupam Saini,

0

Il problema può essere risolto in O (n ^ 2) estendendo il problema della somma 2 con modifiche minori.A è il vettore contenente elementi e B è la somma richiesta.

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

Ecco il codice 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

Un'altra soluzione che controlla e fallisce presto:

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

Ho aggiunto alcuni test unitari qui: GivenArrayReturnTrueIfThreeElementsSumZeroTest .

Se il set utilizza troppo spazio, posso facilmente usare uno java.util.BitSet che utilizzerà lo spazio O (n / w) .


-1

Programma per ottenere quei tre elementi. Ho appena ordinato la matrice / lista prima e li ho aggiornati in minClosenessbase a ciascuna tripletta.

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

L'ho fatto in n ^ 3, il mio pseudocodice è sotto;

// Crea un hashMap con chiave come numero intero e valore come ArrayList // itera attraverso l'elenco usando un ciclo for, per ogni valore in elenco itera nuovamente a partire dal valore successivo;

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

// se la somma di arr [i] e arr [j] è inferiore alla somma desiderata, allora è possibile trovare una terza cifra, quindi fare un'altra per loop

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

// in questo caso stiamo cercando il terzo valore; se la somma di arr [i] e arr [j] e arr [k] è la somma desiderata, aggiungili alla HashMap rendendo arr [i] la chiave e quindi aggiungendo arr [j] e arr [k] in ArrayList nel valore di quella chiave

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

dopo questo ora hai un dizionario che ha tutte le voci che rappresentano i tre valori che si sommano alla somma desiderata. Estrarre tutte queste voci utilizzando le funzioni di HashMap. Questo ha funzionato perfettamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.