Encontrar tres elementos en una matriz cuya suma es más cercana a un número dado


155

Dada una matriz de enteros, A 1 , A 2 , ..., A n , incluidos los negativos y positivos, y otro entero S. Ahora necesitamos encontrar tres enteros diferentes en la matriz, cuya suma es más cercana al entero dado S Si existe más de una solución, cualquiera de ellas está bien.

Puede suponer que todos los enteros están dentro del rango int32_t, y no se producirá un desbordamiento aritmético al calcular la suma. S no es nada especial sino un número elegido al azar.

¿Existe algún algoritmo eficiente que no sea la búsqueda de fuerza bruta para encontrar los tres enteros?


1
Si está buscando una suma igual a un número (y no el más cercano), este sería el problema de 3SUM .
Bernhard Barker

Respuestas:


186

¿Existe algún algoritmo eficiente que no sea la búsqueda de fuerza bruta para encontrar los tres enteros?

Sí; ¡podemos resolver esto en O (n 2 ) tiempo! Primero, considere que su problema Ppuede expresarse de manera equivalente de una manera ligeramente diferente que elimine la necesidad de un "valor objetivo":

problema original P: Dada una matriz Ade nenteros y un valor objetivo S, ¿existe una tupla de 3 a partir de Aesa suma S?

problema modificado P': Dada una matriz Ade nenteros, ¿existe una tupla de 3 de Aesas sumas a cero?

Tenga en cuenta que se puede pasar de esta versión del problema P'del Prestando su S / 3 de cada elemento A, pero ahora no es necesario el valor objetivo más.

Claramente, si simplemente probamos todas las 3 tuplas posibles, resolveríamos el problema en O (n 3 ), esa es la línea base de fuerza bruta. ¿Es posible hacerlo mejor? ¿Qué pasa si elegimos las tuplas de una manera algo más inteligente?

Primero, invertimos algo de tiempo para ordenar la matriz, lo que nos cuesta una penalización inicial de O (n log n). Ahora ejecutamos este 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.
}

Este algoritmo funciona mediante la colocación de tres puntos, i, j, y ken varios puntos de la matriz. icomienza al principio y avanza lentamente hasta el final. kapunta al último elemento. jseñala dónde iha comenzado. Intentamos sumar iterativamente los elementos en sus respectivos índices, y cada vez que ocurre uno de los siguientes:

  • ¡La suma es exactamente la correcta! Hemos encontrado la respuesta.
  • La suma era demasiado pequeña. Mover jmás cerca del final para seleccionar el siguiente número más grande.
  • La suma fue demasiado grande. Mover kmás cerca del comienzo para seleccionar el siguiente número más pequeño.

Para cada uno i, los punteros de jy kgradualmente se acercarán entre sí. Eventualmente se pasarán entre sí, y en ese punto no necesitamos probar nada más para eso i, ya que estaríamos sumando los mismos elementos, solo en un orden diferente. Después de ese punto, intentamos el siguiente iy repetimos.

Finalmente, agotaremos las posibilidades útiles o encontraremos la solución. Puede ver que esto es O (n 2 ) ya que ejecutamos el bucle externo O (n) veces y ejecutamos el bucle interno O (n) veces. Es posible hacer esto subcuadráticamente si te apetece, representando cada número entero como un vector de bits y realizando una transformación rápida de Fourier, pero eso está más allá del alcance de esta respuesta.


Nota: Debido a que esta es una pregunta de entrevista, he engañado un poco aquí: este algoritmo permite la selección del mismo elemento varias veces. Es decir, (-1, -1, 2) sería una solución válida, como lo sería (0, 0, 0). También encuentra solo las respuestas exactas , no la respuesta más cercana, como menciona el título. Como ejercicio para el lector, te dejaré descubrir cómo hacer que funcione solo con elementos distintos (pero es un cambio muy simple) y respuestas exactas (que también es un cambio simple).


8
Parece que el algoritmo solo puede encontrar 3 tuplas que equivalen a S, no más cercanas a S.
ZelluX

77
ZelluX: Como mencioné en la nota, no quería regalar demasiado ya que es un problema de entrevista. Sin embargo, espero que pueda ver cómo modificarlo para que obtenga la respuesta más cercana. (Sugerencia: una forma es realizar un seguimiento de la respuesta más cercana hasta ahora y sobrescribirla si encuentra una mejor).
John Feminella

12
¿Qué sucede si no modificamos el enunciado del problema? En su lugar, buscaremos aj y ak que sumen a ai + S.
Booleano

3
@ZelluX: es similar a cómo funciona un tipo de fusión (así fue como primero hizo clic para mí). Lo que está tratando de hacer ese bucle interno es demostrar que A [j] o A [k] no pueden ser parte de ninguna solución satisfactoria. El problema en cualquier punto es: "¿Hay algún par j '> = j y k' <= k tal que A [j] + A [k] = S - A [i]?" Mirando el par actual (i, j), hay 3 posibilidades: la suma es explosiva (¡alto, hemos ganado!), Es demasiado baja o demasiado alta. Si es demasiado bajo, entonces la suma A [j] + A [k '] también debe ser demasiado baja para cada k' <= k, ya que en cada suma el primer término (A [j]) será el mismo. ..
j_random_hacker

1
... y el segundo término (A [k ']) será igual o incluso menor que A [k]. Entonces, en este caso, hemos demostrado que A [j] no puede participar en ninguna suma satisfactoria, ¡así que también podemos descartarlo! Lo que hacemos estableciendo j = j + 1 y comenzando de nuevo (aunque puede ser útil pensar en términos de resolver un subproblema más pequeño de forma recursiva). Del mismo modo, si la suma A [j] + A [k] es demasiado alta, entonces sabemos que A [j '] + A [k] también debe ser demasiado alta para cada j'> = j, ya que A [j '] debe ser al menos tan grande como A [j] y ya estamos demasiado altos. Esto significa que podemos descartar con seguridad A [k] configurando k = k-1 y comenzando de nuevo.
j_random_hacker

28

Sin duda, esta es una mejor solución porque es más fácil de leer y, por lo tanto, menos propensa a errores. El único problema es que necesitamos agregar algunas líneas de código para evitar la selección múltiple de un elemento.

Otra solución O (n ^ 2) (mediante el uso de 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
La desventaja es el almacenamiento de O (N), en lugar de hacerlo en el lugar.
Charles Munger el

66
El uso de un hashset no es estricto O (n ^ 2) ya que el conjunto de hash puede degenerar en raras ocasiones, lo que resulta en tiempos de búsqueda lineal.
Ext3h

@Charles: también la solución de John necesita espacio O (N), ya que cambia la matriz original mientras ordena. Eso significa que la persona que llama puede necesitar una copia defensiva antes de usar la función.
gamliela

Creo que hay un error en tu algoritmo. s2podría ser un elemento ya seleccionado. Por ejemplo, si la matriz es 0,1,2y Kes 2, no debería haber una respuesta. Creo que su algoritmo saldrá, lo 0,1,1que obviamente es incorrecto.
Yamcha

7

La solución de John Feminella tiene un error.

En la linea

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

Necesitamos verificar si i, j, k son todos distintos. De lo contrario, si mi elemento de destino es 6y si mi matriz de entrada contiene {3,2,1,7,9,0,-4,6}. Si imprimo las tuplas que suman 6, entonces también obtendría 0,0,6como salida. Para evitar esto, necesitamos modificar la condición de esta manera.

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

2
La solución de John Feminella es solo para presentar el algoritmo para resolver el problema, también ha especificado que su solución no funcionaría para una condición de número distinto y debe modificar un poco el código anterior que le queda al lector.
EmptyData

3
En realidad, nunca seré j ya que siempre lo está comenzando en j = i + 1. La única condición real que debe verificar es si j == k. Sin embargo, al establecer el bucle while en j <k, ha resuelto los problemas sin una larga declaración if ya que k siempre será mayor que j y j siempre mayor que i.
lorenzocastillo

2
Esto no parece una respuesta a la pregunta, sino un comentario sobre la respuesta de John Feminella.
Bernhard Barker, el

6

¿Qué tal algo como esto, que es 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.
        }
    }

Esto determina si la suma de 3 elementos es exactamente igual a su número. Si desea lo más cercano, puede modificarlo para recordar el delta más pequeño (diferencia entre su número de triplete actual) y al final imprimir el triplete correspondiente al delta más pequeño.


si quieres encontrar k elementos para obtener la suma, ¿cuál es la complejidad? ¿Cómo lidias con esto?
codificador_15

Con este enfoque, la complejidad para k elementos es O (n ^ (k-1)) para k> = 2. Debe agregar un bucle externo para cada sumando adicional.
Ext3h

5

Tenga en cuenta que tenemos una matriz ordenada. Esta solución es similar a la solución de John solo que busca la suma y no repite el mismo 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));
}

Es necesario calcular la diferencia absoluta de a[r] + a[l] + a[i] - sum. Pruébate arr = [-1, 2, 1, -4] sum = 1.
Dimitry

3

Aquí está el código 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

Solución muy simple de N ^ 2 * logN: clasifique la matriz de entrada, luego revise todos los pares A i , A j (tiempo N ^ 2), y para cada par verifique si (S - A i - A j ) está en la matriz ( logN time).

Otra solución O (S * N) usa programación dinámica clásica enfoque .

En breve:

Cree una matriz 2-d V [4] [S + 1]. Llénalo de tal manera que:

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

V 1 [A i ] = 1 para cualquier i, V 1 [x] = 0 para todos los demás x

V [2] [A i + A j ] = 1, para cualquier i, j. V [2] [x] = 0 para todos los demás x

V [3] [suma de 3 elementos] = 1.

Para llenarlo, itere a través de A i , para cada A i itere a través de la matriz de derecha a izquierda.


ligero cambio en el primer algoritmo ... si el elemento no existe, entonces al final de la búsqueda binaria, tendríamos que mirar el elemento a la izquierda, actual y derecha para ver cuál da el resultado más cercano .
Anurag

La matriz es demasiado grande y no es O (s * N). Este paso es O (N ^ 2): V [2] [Ai + Aj] = 1, para cualquier i, j. V [2] [x] = 0 para todos los demás x.
Richard

1

Esto se puede resolver de manera eficiente en O (n log (n)) de la siguiente manera. Estoy dando una solución que dice si la suma de tres números es igual a un número dado.

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

No creo que esto vaya a funcionar. De acuerdo, tiene dos casos simples de cómo avanzar leftIndexo rightIndexcuando todos los elementos en el medio son estrictamente más pequeños o más grandes que su número deseado. Pero, ¿qué pasa con el caso cuando la búsqueda binaria se detuvo en algún punto intermedio? Debería verificar ambas ramas (dónde rightIndex--y leftIndex++). En su solución, simplemente ignora esta situación. Pero no creo que haya una manera de superar este problema.
Aivean

0

Reducción: creo que la solución @John Feminella O (n2) es muy elegante. Todavía podemos reducir la A [n] en la que buscar tuplas. Al observar A [k] tal que todos los elementos estarían en A [0] - A [k], cuando nuestra matriz de búsqueda es enorme y SUM (s) realmente pequeña.

A [0] es mínimo: - Matriz ordenada ascendente.

s = 2A [0] + A [k]: Dados sy A [] podemos encontrar A [k] usando la búsqueda binaria en el tiempo log (n).


0

Aquí está el programa en Java que es 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);
    }
}

Buen enfoque, pero no pude entender el punto en el que restringe el número de resultados a un triplete. Por ejemplo, considere la entrada: [1,11,3,4,5,6,7,8, 2] y suma 12, de su solución parece que [1, 11] [4,8] [1,4, 5,2] etc. todo funcionaría.
Anupam Saini

0

El problema se puede resolver en O (n ^ 2) extendiendo el problema de 2 sumas con modificaciones menores. A es el vector que contiene elementos y B es la suma requerida.

Solución 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

Aquí está el código 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

Otra solución que verifica y falla temprano:

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

Agregué algunas pruebas unitarias aquí: GivenArrayReturnTrueIfThreeElementsSumZeroTest .

Si el conjunto usa demasiado espacio, puedo usar fácilmente un java.util.BitSet que usará espacio O (n / w) .


-1

Programa para obtener esos tres elementos. Acabo de ordenar la matriz / lista primero y las actualicé en minClosenessfunción de cada triplete.

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

Hice esto en n ^ 3, mi pseudocódigo está debajo;

// Cree un hashMap con la clave como Integer y el valor como ArrayList // itere a través de la lista usando un bucle for, para cada valor en la lista itere nuevamente comenzando desde el siguiente valor;

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

// si la suma de arr [i] y arr [j] es menor que la suma deseada, entonces existe la posibilidad de encontrar un tercer dígito, así que haga otro bucle for

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

// en este caso ahora estamos buscando el tercer valor; si la suma de arr [i] y arr [j] y arr [k] es la suma deseada, entonces agréguelas al HashMap haciendo que arr [i] sea la clave y luego agregue arr [j] y arr [k] en ArrayList en el valor de esa clave

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

después de esto, ahora tiene un diccionario que tiene todas las entradas que representan los tres valores que se suman a la suma deseada. Extraiga todas estas entradas utilizando las funciones de HashMap. Esto funcionó perfectamente.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.