Dividir para Conquistar

João Pedro Neto

Nas aulas de AED é referido o algoritmo de pesquisa binária. Para procurar um elemento num vetor ordenado podemos começar pelo elemento do meio e comparar com o que procuramos para ver se devemos continuar na metade direita ou esquerda. Desta forma, cada iteração reduz o espaço de procura a metade, acelerando imenso a pesquisa. De uma pesquisa linear O(n) passamos para uma pesquisa logarítmica O(log n).

Este algoritmo está disponível nas bibliotecas Java em Arrays.binarySearch():

       int[] v = new int[] {1, 5, 8, 11, 32, 123, 543, 1200};

System.out.println(Arrays.binarySearch(v, 11));

este exemplo devolve 3 dado o elemento 11 procurado está nesse índice.

Existe igualmente o método Collections.binarySearch() onde podemos fornecer um comparador feito por nós. Iremos falar de comparadores em AED.

Este tutorial parte desta ideia de partir o espaço de pesquisa em metade, para tentar encontrar soluções para problemas onde é difícil encontrar uma expressão que resolva o problema diretamente, mas onde as diferentes hipóteses têm um comportamento monótono.

Vejamos um primeiro exemplo. Um cliente chega a um Banco e quer realizar um empréstimo de 1000 euros, para ser pago a 20 prestações mensais iguais com 1% de juros sobre a dívida por pagar a cada prestação. Qual deve ser a prestação mensal?

Ora, se procurarem na net encontram que a fórmula a aplicar é esta:

$$T \frac{r (1+r)^n}{(1+r)^n - 1}$$

onde T é o total, n o número de prestações, e r o juro/prestação.

Assim, a resposta para a instância acima referida é de 55.42 euros/prestação.

***

Mas se tivéssemos num concurso de programação, sem acesso à net para descobrir a fórmula, como calcular esta resposta?

Podemos usar o método da pesquisa binária. Porquê? Porque os diferentes valores para as prestações têm um comportamento monótono. Se pagássemos 100 euros/prestação, o Banco iria dever-nos dinheiro no fim do prazo. Se fosse 200 euros, essa divida seria ainda maior. Por outro lado, se pagássemos apenas 20 euros, haveria um resto por pagar. E assim sucessivamente...

Ou seja, podemos usar o processo da pesquisa binária sobre os valores possíveis da prestação.

Para tal, definimos um intervalo onde sabemos seguramente que a resposta final irá estar. No nosso exemplo, esse intervalo pode ser [0,1000]. De certeza que temos de pagar mais que zero euros/prestação, e de certeza que não temos de pagar o total/prestação (os Bancos certamente gostariam desta possibilidade).

A partir deste intervalo podemos escolher o valor do meio, 500 euros, e verificar quanto sobraria no fim das 20 prestações: -9789,31 euros (!) Ou seja, estamos a pagar demais. Então o valor tem de estar entre [0,500]. Se experimentarmos 250 euros resulta em -4284,56 euros. Então o resultado tem de estar entre [0,250]...

O seguinte método executa este algoritmo:


private static double findMonthlyPaymentExact(int months, 
                                              double total, 
                                              double monthlyInterest) {
    double r = monthlyInterest-1;
    double n = months;
    return total * (r*Math.pow(1+r,n) / (Math.pow(1+r,n)-1));
}

private static double findMonthlyPayment(int months, 
                                         double total, 
                                         double monthlyInterest) {
double Epsilon = 1e-6;
    double lo = 0.0, hi = total, mid = 0.0, ans = 0.0;
    
    while (lo < hi + Epsilon) { // when the answer is not found yet
        mid = (lo + hi) / 2.0;    // try the middle value
        double debt = leftDebt(mid, months, total, monthlyInterest); 
        if (Math.abs(debt) < Epsilon) { 
            ans = mid; 
            break;
      }
        if (debt > 0) 
            lo = mid;
        else 
            hi = mid;
    }
    return ans;
}

Reparem nos seguintes detalhes:


private static double leftDebt(double monthlyPayment, 
                               int months, 
                               double total, 
                               double monthlyInterest) {
    double debt = total;

    for(int i=0; i<months; i++)
        debt = debt*monthlyInterest - monthlyPayment;

    return debt;
}

Esta técnica costuma chamar-se por método da bissecção.

Antes de continuar, uma curiosidade histórica: nesta página apresenta-se um método usado na Pérsia para calcular os juros, muito similar à fórmula atual, e que se pode justificar a sua precisão através da aproximação com polinómios de Maclaurin.

***

Vejam agora o problema UVa 11881 e tentem resolver o problema usando o método da bisecção. Uma nota, o enunciado fala de haver a hipótese de múltiplas soluções, mas isso não é relevante para os inputs dados. Não considerem essa possibilidade.

Outros problemas que podem ser abordados com esta técnica:

Last modified: Tuesday, 15 March 2022, 3:27 PM