Localizando três elementos em uma matriz cuja soma está mais próxima de um determinado número


155

Dada uma matriz de números inteiros, A 1 , A 2 , ..., A n , incluindo negativos e positivos e outro número S. Agora precisamos encontrar três números inteiros diferentes na matriz, cuja soma é a mais próxima do número inteiro S Se houver mais de uma solução, qualquer uma delas está correta.

Você pode assumir que todos os números inteiros estão dentro do intervalo int32_t, e nenhum estouro aritmético ocorrerá com o cálculo da soma. S não é nada de especial, mas um número escolhido aleatoriamente.

Existe algum algoritmo eficiente além da pesquisa de força bruta para encontrar os três números inteiros?


1
Se você está procurando uma soma igual a um número (e não o mais próximo), esse seria o problema 3SUM .
Bernhard Barker

Respostas:


186

Existe algum algoritmo eficiente além da pesquisa de força bruta para encontrar os três números inteiros?

Sim; podemos resolver isso em O (n 2 ) tempo! Primeiro, considere que seu problema Ppode ser formulado de forma equivalente de uma maneira ligeiramente diferente que elimina a necessidade de um "valor-alvo":

problema original P: Dada uma matriz Ade nnúmeros inteiros e um valor de destino S, existe uma tupla de 3 Asomas para S?

problema modificado P': Dada uma matriz Ade nnúmeros inteiros, existe uma tupla de 3 Aque soma a zero?

Observe que você pode ir a partir desta versão do problema P'de Psubtraindo o seu S / 3 de cada elemento A, mas agora você não precisa o valor-alvo mais.

Claramente, se simplesmente testássemos todas as três tuplas possíveis, resolveríamos o problema em O (n 3 ) - essa é a linha de base da força bruta. É possível fazer melhor? E se escolhermos as tuplas de uma maneira um pouco mais inteligente?

Primeiro, investimos algum tempo para classificar a matriz, o que nos custa uma penalidade inicial de O (n log n). Agora, executamos 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 colocando três ponteiros, i, j, e kem vários pontos da matriz. icomeça no começo e lentamente segue seu caminho até o fim. kaponta para o último elemento. japonta para onde icomeçou. Tentamos iterativamente somar os elementos em seus respectivos índices e cada vez que acontece um dos seguintes:

  • A soma é exatamente correta! Nós encontramos a resposta.
  • A soma era muito pequena. Aproxime- jse do final para selecionar o próximo maior número.
  • A soma era muito grande. Aproxime- kse do início para selecionar o próximo número menor.

Para cada um i, os ponteiros je kgradualmente se aproximam um do outro. Eventualmente, eles passarão um pelo outro e, nesse ponto, não precisamos tentar mais nada para isso i, pois estaríamos somando os mesmos elementos, apenas em uma ordem diferente. Depois desse ponto, tentamos o próximo ie repetimos.

Eventualmente, esgotaremos as possibilidades úteis ou encontraremos a solução. Você pode ver que esse é O (n 2 ), pois executamos o loop externo O (n) vezes e executamos o loop interno O (n) vezes. É possível fazer isso sub-quadraticamente se você realmente gosta, representando cada número inteiro como um vetor de bits e realizando uma transformação rápida de Fourier, mas isso está além do escopo desta resposta.


Nota: Por se tratar de uma pergunta de entrevista, eu trapacei um pouco aqui: esse algoritmo permite a seleção do mesmo elemento várias vezes. Ou seja, (-1, -1, 2) seria uma solução válida, como seria (0, 0, 0). Ele também encontra apenas as respostas exatas , e não a mais próxima, como o título menciona. Como exercício para o leitor, deixarei você descobrir como fazê-lo funcionar apenas com elementos distintos (mas é uma mudança muito simples) e respostas exatas (que também são uma mudança simples).


8
Parece que o algoritmo pode encontrar apenas três tuplas que é igual a S, não mais próximo de S. #
ZelluX 15/01/10

7
ZelluX: Como mencionei na nota, não quis revelar muito, já que é um problema de entrevista. Espero que você possa ver como modificá-lo para que você obtenha a resposta mais próxima. (Dica: é uma maneira de manter o controle da resposta mais próxima até agora e substituí-lo se você encontrar um melhor.)
John Feminella

12
e se não modificarmos a declaração do problema, procuraremos aj e ak que somam ai + S.
Boolean

3
@ZelluX: É semelhante à forma como uma classificação de mesclagem funciona (foi assim que ela clicou pela primeira vez). O que esse loop interno está tentando fazer é provar que A [j] ou A [k] não podem fazer parte de nenhuma solução satisfatória. O problema a qualquer momento é: "Existe algum par j '> = j e k' <= k tal que A [j] + A [k] = S - A [i]?" Olhando para o par atual (i, j), existem três possibilidades: a soma é alta (pare - vencemos!), É muito baixo ou muito alto. Se for muito baixo, a soma A [j] + A [k '] também deve ser muito baixa para cada k' <= k, pois em cada uma dessas somas o primeiro termo (A [j]) será o mesmo. ..
j_random_hacker

1
... e o segundo termo (A [k ']) será igual ou até menor que A [k]. Portanto, neste caso, provamos que A [j] não pode participar de nenhuma soma satisfatória - para que possamos descartá-la! O que fazemos definindo j = j + 1 e recomeçando (embora possa ajudar pensar em termos de resolver um subproblema menor recursivamente). Da mesma forma, se a soma A [j] + A [k] for muito alta, sabemos que A [j '] + A [k] também deve ser muito alto para cada j'> = j, pois A [j '] deve ser pelo menos tão grande quanto A [j] e já estamos muito altos. Isso significa que podemos descartar A [k] com segurança, definindo k = k-1 e recomeçando.
Jrandom_hacker

28

certamente essa é uma solução melhor, porque é mais fácil de ler e, portanto, menos propensa a erros. O único problema é que precisamos adicionar algumas linhas de código para evitar a seleção múltipla de um elemento.

Outra solução O (n ^ 2) (usando um 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
A desvantagem é o armazenamento de O (N), em vez de fazê-lo no local.
Charles Munger #

6
O uso de um hashset não é estrito O (n ^ 2), pois o conjunto de hash pode degenerar em raras ocasiões, resultando em tempos de pesquisa lineares.
Ext3h

@ Charles - Também a solução de John precisa de espaço O (N), pois você altera a matriz original durante a classificação. Isso significa que o chamador pode precisar de uma cópia defensiva antes de usar a função.
gamliela

Eu acho que há um erro no seu algoritmo. s2pode ser um elemento já selecionado. Por exemplo, se a matriz é 0,1,2e Ké 2, não deve haver uma resposta. Eu acho que o seu algoritmo irá mostrar o 0,1,1que está obviamente incorreto.
Yamcha

7

A solução de John Feminella tem um bug.

Na linha

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

Precisamos verificar se i, j, k são todos distintos. Caso contrário, se meu elemento de destino for 6e se minha matriz de entrada contiver {3,2,1,7,9,0,-4,6}. Se eu imprimir as tuplas que somam 6, também obteria 0,0,6como saída. Para evitar isso, precisamos modificar a condição dessa maneira.

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

2
A solução John Feminella é apenas para apresentar o algoritmo para resolver o problema, ele também especificou que sua solução não funcionaria para condições de número distinto e você deve modificar um pouco o código acima, que ele deixou para o leitor.
EmptyData

3
Na verdade, nunca serei j, já que você sempre o inicia em j = i + 1. A única condição real que você deve verificar é se j == k. No entanto, definindo o loop while como j <k, você resolveu os problemas sem uma instrução if longa, pois k sempre será maior que j e j sempre maior que i.
Lorenzocastillo

2
Isso não parece uma resposta para a pergunta, mas sim um comentário sobre a resposta de John Feminella.
Bernhard Barker

6

Que tal algo assim, que é 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.
        }
    }

Isso verifica se a soma de 3 elementos é exatamente igual ao seu número. Se você quiser mais perto, pode modificá-lo para lembrar o menor delta (diferença entre o número de trigêmeos atual) e, no final, imprimir o trigêmeo correspondente ao menor delta.


se você deseja encontrar k elementos para obter a soma, qual é a complexidade? Como você lida com isso?
Coder_15

Com essa abordagem, a complexidade dos elementos k é O (n ^ (k-1)) para k> = 2. Você precisa adicionar um loop externo para cada comando adicional.
Ext3h

5

Observe que temos uma matriz classificada. Essa solução é semelhante à solução de John, apenas que procura a soma e não repete o mesmo 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));
}

É necessário calcular a diferença absoluta de a[r] + a[l] + a[i] - sum. Experimente arr = [-1, 2, 1, -4] sum = 1.
precisa

3

Aqui está o 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

Solução N ^ 2 * logN muito simples: classifique a matriz de entrada, depois passe por todos os pares A i , A j (hora N ^ 2) e, para cada par, verifique se (S - A i - A j ) está na matriz ( hora do logN).

Outra solução O (S * N) usa a abordagem de programação dinâmica clássica .

Em resumo:

Crie uma matriz 2-V V [4] [S + 1]. Preencha de tal maneira que:

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

V 1 [A i ] = 1 para qualquer i, V 1 [x] = 0 para todos os outros x

V [2] [A i + A j ] = 1, para qualquer i, j. V [2] [x] = 0 para todos os outros x

V [3] [soma de quaisquer 3 elementos] = 1.

Para preenchê-lo, percorra A i , para cada A i percorra a matriz da direita para a esquerda.


pequena alteração no primeiro algoritmo .. se o elemento não existir, no final da pesquisa binária, teremos que olhar para o elemento à esquerda, atual e direita para ver qual deles dá o resultado mais próximo .
Anurag

A matriz é muito grande e não é O (s * N). Este passo é O (N ^ 2): V [2] [Ai + Aj] = 1, para qualquer i, j. V [2] [x] = 0 para todos os outros x.
Richard

1

Isso pode ser resolvido com eficiência em O (n log (n)) da seguinte maneira. Estou dando uma solução que diz se a soma de quaisquer três números é igual a um determinado número.

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

Eu não acho que isso vai funcionar. Você tem dois casos simples de como avançar leftIndexou rightIndexquando todos os elementos no meio são estritamente menores ou maiores que o número desejado. Mas e o caso em que a pesquisa binária parou em algum lugar no meio? Você precisaria verificar os dois ramos (onde rightIndex--e leftIndex++). Na sua solução, você simplesmente ignora esta situação. Mas não acho que exista uma maneira de superar esse problema.
Aivean

0

Redução: eu acho que a solução O (n2) da John Feminella é a mais elegante. Ainda podemos reduzir o A [n] no qual procurar tupla. Observando A [k] de modo que todos os elementos estejam em A [0] - A [k], quando nossa matriz de pesquisa é enorme e SUM (s) muito pequeno.

A [0] é mínimo: - Matriz ordenada crescente.

s = 2A [0] + A [k]: Dados s e A [], podemos encontrar A [k] usando pesquisa binária no log (n) tempo.


0

Aqui está o programa em java que é 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);
    }
}

Boa abordagem, mas não consegui entender onde você restringe o número de resultados a um trigêmeo. Por exemplo, considere a entrada: [1,11,3,4,5,6,7,8, 2] e soma 12, da sua solução parece que [1, 11] [4,8] [1,4, 5,2] etc funcionaria.
Anupam Saini

0

O problema pode ser resolvido em O (n ^ 2) estendendo o problema de 2 somas com pequenas modificações.A é o vetor que contém elementos e B é a soma necessária.

int Solution :: threeSumClosest (vetor e 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

Aqui está o 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

Outra solução que verifica e falha com antecedência:

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

Adicionei alguns testes de unidade aqui: GivenArrayReturnTrueIfThreeElementsSumZeroTest .

Se o conjunto está usando muito espaço I pode facilmente usar um java.util.BitSet que usará O (n / w) espaço .


-1

Programa para obter esses três elementos. Acabei de classificar a matriz / lista primeiro e atualizá-las com minClosenessbase em cada trigêmeo.

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

Eu fiz isso em n ^ 3, meu pseudocódigo está abaixo;

// Crie um hashMap com chave como Inteiro e valor como ArrayList // itere na lista usando um loop for, para cada valor na lista itere novamente a partir do próximo valor;

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

// se a soma de arr [i] e arr [j] for menor que a soma desejada, é possível encontrar um terceiro dígito, assim como outro loop for

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

// neste caso, agora estamos procurando o terceiro valor; se a soma de arr [i] e arr [j] e arr [k] for a soma desejada, adicione-os ao HashMap, tornando o arr [i] a chave e adicionando arr [j] e arr [k] em o ArrayList no valor dessa chave

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

depois disso, você agora tem um dicionário que possui todas as entradas que representam os três valores adicionados à soma desejada. Extraia todas essas entradas usando as funções HashMap. Isso funcionou perfeitamente.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.