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 P
pode ser formulado de forma equivalente de uma maneira ligeiramente diferente que elimina a necessidade de um "valor-alvo":
problema original P
: Dada uma matriz A
de n
números inteiros e um valor de destino S
, existe uma tupla de 3 A
somas para S
?
problema modificado P'
: Dada uma matriz A
de n
números inteiros, existe uma tupla de 3 A
que soma a zero?
Observe que você pode ir a partir desta versão do problema P'
de P
subtraindo 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 k
em vários pontos da matriz. i
começa no começo e lentamente segue seu caminho até o fim. k
aponta para o último elemento. j
aponta para onde i
começ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-
j
se do final para selecionar o próximo maior número.
- A soma era muito grande. Aproxime-
k
se do início para selecionar o próximo número menor.
Para cada um i
, os ponteiros j
e k
gradualmente 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 i
e 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).