Módulo 1 · Lógica e demonstrações

Invariantes de laço e corretude

Aula 1.3 · cerca de 18 minutos

Um invariante de laço é uma afirmação que vale antes de cada iteração. Provar que o código está correto se resume a três passos:

Inicialização:  o invariante vale antes da 1ª iteração
Manutenção:     se vale antes de uma iteração, vale depois dela
Término:        quando o laço para, invariante + condição de parada ⇒ resultado correto

Repare que isso é indução sobre o número de iterações.

Exemplo: busca binária

def busca(xs, alvo):
    lo, hi = 0, len(xs)          # intervalo semiaberto [lo, hi)
    # Invariante: se alvo está em xs, está em xs[lo:hi]
    while lo < hi:
        mid = (lo + hi) // 2
        if xs[mid] < alvo:
            lo = mid + 1         # alvo não está em xs[:mid+1]
        elif xs[mid] > alvo:
            hi = mid             # alvo não está em xs[mid:]
        else:
            return mid
    return -1                    # intervalo vazio: alvo não está em xs

Terminação: a quantidade hi − lo é um inteiro não negativo que diminui estritamente a cada volta. Uma quantidade assim se chama variante (ou função de limite) e garante que o laço termina.

Dica: A maioria dos bugs de off-by-one na busca binária vem de misturar convenções de intervalo ([lo, hi] com [lo, hi)). Escreva o invariante num comentário e confira cada atribuição contra ele.

Exercício 1

Qual é o invariante do laço m = xs[0]; for x in xs[1:]: m = max(m, x)?

Exercício 2

Se trocarmos lo = mid + 1 por lo = mid na busca acima, o que pode acontecer?