Pesquisa Binária

Imagine que escrevemos as alturas de todos os edifícios do mundo em ordem crescente. Assim, o primeiro elemento da lista é o edifício mais baixo do mundo e o último é o mais alto. Jogamos um jogo em que, dado um valor inteiro que representa a altura de um edifício, devemos responder com a posição (o índice) desse edifício na lista. Caso não exista nenhum edifício com a altura solicitada, respondemos com -1.

Uma abordagem mais simples seria percorrer cada elemento do array com um loop for e verificar se coincide com a altura pesquisada. No pior dos casos, isso causaria n operações para cada consulta (se a lista possui n elementos).

h = [...]                # Alturas de todos os edifícios em ordem crescente
q = int(input())         # A altura consultada

for i, height in enumerate(h):
    if height == q:      # Caso encontremos o elemento
        print(i)         # Imprime o índice
        break            # E interrompe
else:                    # Caso não encontremos um edifício com altura q (else = sem break)
    print(-1)            # Imprime -1 para indicar a ausência

No entanto, percebendo que a lista está ordenada em ordem crescente, começamos a notar que estamos a fazer muitas operações redundantes. Em vez de percorrer toda a lista do início ao fim, poderíamos descartar partes inúteis e chegar à resposta mais rapidamente.

Como o array inteiro está ordenado, uma forma mais eficiente de resolver este problema é olhar para o elemento do meio do array. Se esse elemento for menor do que q, descartamos toda a metade esquerda e concentramos a pesquisa apenas na direita. Se esse elemento for maior do que q, descartamos a metade direita e pesquisamos apenas na esquerda. Repetir este procedimento, procurando sempre o elemento do meio e descartando a metade redundante até encontrar o elemento, é essencialmente o algoritmo de Pesquisa Binária.

h = [...]                # Alturas de todos os edifícios em ordem crescente
q = int(input())         # A altura consultada

l, r = 0, len(h)         # A resposta estará sempre entre [l; r). Note que r é exclusivo
while r - l > 1:         # Existe pelo menos um elemento para considerar
    mid = (l + r) // 2   # Pegar o índice do meio
    if h[mid] > q:       # h[mid] > q => descartar a metade direita
        r = mid
    else:                # h[mid] <= q => descartar a metade esquerda
        l = mid

# No fim, vamos verificar se h[l] é, de fato, o elemento consultado
print(l if h[l] == q else -1)

Imagine que temos um array de alturas de edifícios h = [20, 22, 23, 23, 34, 49, 52, 55, 58] e queremos saber o índice do edifício com altura 49.

Com este algoritmo, faremos várias iterações:

  1. mid = (0 + 9) // 2 = 4h[4] = 3449 ⇒ descartamos a parte esquerda.

  2. mid = (4 + 9) // 2 = 6h[6] = 52 > 49 ⇒ descartamos a parte direita.

  3. mid = (4 + 6) // 2 = 5h[5] = 4949 ⇒ descartamos a parte esquerda.

  4. l = 5, r = 6r - l == 1 ⇒ o ciclo termina e imprimimos 5, pois h[5] = 49.

Este pequeno exemplo pode não parecer grande coisa em termos de otimização, mas imagine que temos 10 mil milhões de edifícios. Se fôssemos percorrer todos os elementos, no pior caso faríamos 10 mil milhões de iterações. Já ao partir o array ao meio repetidamente e descartar as partes redundantes, o ganho de desempenho é enorme. Vejamos quantas iterações seriam necessárias para chegar a uma resposta, dividindo 10 mil milhões de elementos ao meio em cada iteração:

Spoiler: seriam apenas 32 iterações em vez de 10 mil milhões
  1. 10,000,000,000 / 2 = 5,000,000,000

  2. 5,000,000,000 / 2 = 2,500,000,000

  3. 1250000000 / 2 = 625000000

  4. 625000000 / 2 = 312500000

  5. 312500000 / 2 = 156250000

  6. 156250000 / 2 = 78125000

  7. 78125000 / 2 = 39062500

  8. 39062500 / 2 = 19531250

  9. 19531250 / 2 = 9765625

  10. 9765625 / 2 = 4882812

  11. 4882812 / 2 = 2441406

  12. 2441406 / 2 = 1220703

  13. 1220703 / 2 = 610351

  14. 610351 / 2 = 305175

  15. 305175 / 2 = 152587

  16. 152587 / 2 = 76293

  17. 76293 / 2 = 38146

  18. 38146 / 2 = 19073

  19. 19073 / 2 = 9536

  20. 9536 / 2 = 4768

  21. 4768 / 2 = 2384

  22. 2384 / 2 = 1192

  23. 1192 / 2 = 596

  24. 596 / 2 = 298

  25. 298 / 2 = 149

  26. 149 / 2 = 74

  27. 74 / 2 = 37

  28. 37 / 2 = 18

  29. 18 / 2 = 9

  30. 9 / 2 = 4

  31. 4 / 2 = 2

  32. 2 / 2 = 1

Agora imagine que exista um algoritmo que faça uma pesquisa linear, verificando cada um dos 10 mil milhões de itens e que analisar cada elemento demore 1 segundo. Esse algoritmo demoraria 317 anos para terminar. Enquanto isso, um algoritmo de pesquisa binária levaria apenas 32 segundos!

Esta é a diferença entre fazer operações e operações.

Desafio

Dadas n médias (GPAs) entre 1 e 4 (sendo 1 a nota mais baixa e 4 a nota perfeita) e vários valores limite, a tarefa é determinar quantas pessoas passariam para o próximo nível, considerando que, para passar, o valor da média deve ser igual ou superior a esse limite.

Entrada

A primeira linha da entrada contém um único inteiro n (1 ≤ n ≤ ), que é o número de estudantes.

A linha seguinte contém n números em ponto flutuante que representam as médias dos estudantes em ordem crescente (1 ≤ ≤ 4).

A terceira linha contém um único inteiro q (1 ≤ q ≤ ), que é o número de valores limite a considerar.

A última linha da entrada contém q números em ponto flutuante que representam os limites mínimos para passar de nível (1 ≤ ≤ 4).

Saída

O programa deve imprimir q linhas. A linha i deve representar o número de estudantes que passariam de nível dado o limite .

Exemplos

Entrada

Saída

10 1.1 1.2 1.4 1.7 2 3 3.3 3.5 3.8 3.8 5 3.7 3.8 1 1.5 2

2 2 10 7 6

Constraints

Time limit: 6 seconds

Memory limit: 512 MB

Output limit: 1 MB

To check your solution you need to sign in
Sign in to continue