Pular para o conteúdo

Matemática para Computação · parte 2 de 7

Indução Matemática e Recursão: Do Efeito Dominó à Corretude de Algoritmos

Compreenda indução fraca, forte e estrutural com provas formais de corretude, terminação recursiva e invariantes de laço na prática.

intermediárioPublicada em · 13 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Indução matemática e recursão

Se você enfileirar um milhão de peças de dominó no chão, não precisa empurrar uma por uma para derrubar todas. Basta garantir duas coisas: primeiro, que a primeira peça caia; segundo, que o espaço entre qualquer peça e a seguinte seja curto o bastante para que a queda de uma derrube a próxima. Se essas duas condições forem verdadeiras, a fileira inteira tombará, não importa se ela tem dez dominós ou se estende até a Lua.

A indução matemática é exatamente esse efeito dominó transformado em método de prova. Na computação, ela é a espinha dorsal de todo código recursivo. Quando você escreve uma função que chama a si mesma em Go, você está aplicando, linha por linha, a estrutura de uma prova por indução. Se você entende a indução, você nunca mais terá medo de recursão, nem deixará passar um estouro de pilha de memória em produção.

Esta aula ensina o método indutivo do zero até a análise de algoritmos reais. Começaremos com a intuição e a anatomia da prova (seções 1 a 3), faremos a ponte direta com o runtime e a pilha de execução de Go (seções 4 e 5), exploraremos a aplicada a algoritmos de divisão e conquista (seção 6), e fecharemos com armadilhas clássicas, exercícios resolvidos e glossário.

1. O problema de tentar provar por testes

Imagine que um colega de equipe implemente uma função para calcular a soma dos primeiros n números inteiros e diga: “testei para n = 1, n = 2 e n = 10, e funcionou perfeitamente nos três casos; logo, o algoritmo está certo para qualquer número do universo”.

No mundo da engenharia de software rápida, esse teste unitário com três valores acalma o pipeline de integração contínua, mas matematicamente não prova nada. O fato de uma propriedade valer para os primeiros dez números não garante que ela não falhe no número 10.001. A conjectura de Fermat sobre primos e vários problemas clássicos de teoria dos números pareciam verdadeiros para milhares de exemplos antes de encontrarem um contraexemplo gigantesco que os derrubou.

Como não temos tempo de vida útil nem memória RAM para testar infinitos números um por um, precisamos de um mecanismo finito capaz de abraçar o infinito. Esse mecanismo é o princípio da indução matemática.

2. A anatomia do método indutivo

Uma demonstração por indução matemática sobre uma afirmação P(n) é dividida estritamente em três etapas:

1.

Você prova manualmente que a afirmação vale para o menor valor possível do domínio, normalmente n = 0 ou n = 1. É o empurrão inicial no primeiro dominó. Se o primeiro dominó não cair sozinho, a fila inteira fica de pé.

2.

Você assume temporariamente que a afirmação é verdadeira para um valor arbitrário k (ou seja, supõe que P(k) é verdade). Aqui muitos iniciantes travam e dizem: “mas eu não posso assumir o que ainda não provei!”. Pode, porque você não está afirmando que P(k) é verdade incondicional; você está dizendo “se a peça k cair, o que acontece a seguir?”.

3.

Usando a hipótese de que P(k) é verdade, você demonstra algebricamente que P(k+1) também precisa ser verdade. Isso prova que a distância entre os dominós é perfeita: se qualquer peça k cair, ela inevitavelmente empurra a peça k+1.

Unindo o caso base com o passo indutivo, a lógica conclui: P(1) é verdade pelo caso base. Como P(1) é verdade, o passo indutivo garante P(2). Como P(2) é verdade, o passo garante P(3), e assim sucessivamente para todo número natural até o infinito.

O mecanismo da indução matemática e recursão. Base: P(0) é verdadeiro → Hipótese: P(k) (inicia efeito dominó)…
figura 1 · O mecanismo da indução matemática e recursão

O diagrama acima coloca lado a lado a demonstração matemática e o código de software. No fluxo superior, o caso base acende a primeira peça, a hipótese indutiva arma a transição e o passo indutivo propaga a garantia até o infinito. No fluxo inferior, cada uma dessas etapas corresponde exatamente a uma instrução em uma função Go: a checagem if n == 0 é o caso base, a chamada Somar(n - 1) é a hipótese indutiva que confia no subproblema menor, e a operação return n + sub é o passo indutivo que compõe a solução final.

3. Um exemplo clássico: a fórmula de Gauss

Vamos provar a fórmula da soma dos primeiros n inteiros positivos: 1 + 2 + 3 + … + n = n(n + 1) / 2

Seja P(n) a afirmação de que essa igualdade é verdadeira para n ≥ 1.

Caso base (n = 1)

O lado esquerdo da equação tem apenas o primeiro termo: 1. O lado direito aplica a fórmula: 1(1 + 1) / 2 = 2 / 2 = 1. Ambos os lados são iguais a 1. O caso base P(1) está provado.

Hipótese indutiva

Assumimos que a fórmula funciona para um número arbitrário k: 1 + 2 + … + k = k(k + 1) / 2

Passo indutivo

Precisamos provar que a fórmula vale para o próximo número, k + 1. Ou seja, queremos chegar em: 1 + 2 + … + k + (k + 1) = (k + 1)((k + 1) + 1) / 2 = (k + 1)(k + 2) / 2

Partimos do lado esquerdo da expressão para k + 1: (1 + 2 + … + k) + (k + 1)

Repare que o bloco entre parênteses é exatamente a soma até k, que a nossa hipótese indutiva já sabe calcular: = [k(k + 1) / 2] + (k + 1)

Colocamos tudo sobre o mesmo denominador comum 2: = [k(k + 1) + 2(k + 1)] / 2

Colocamos o fator comum (k + 1) em evidência: = (k + 1)(k + 2) / 2

Chegamos exatamente à fórmula esperada para k + 1. Pelo princípio da indução matemática, a fórmula é verdadeira para todo n ≥ 1.

4. O espelho em código: recursão em Go

Existe uma correspondência perfeita, conhecida na ciência da computação teórica, entre indução e recursão:

  • O caso base da indução é a condição de parada da função recursiva.
  • O passo indutivo é a chamada recursiva combinada com a operação local.

Veja a implementação em Go que calcula essa mesma soma de forma puramente recursiva:

package main

import "fmt"

// Somar calcula a soma dos inteiros de 0 ate n de forma recursiva.
func Somar(n int) int {
	// 1. Caso base: se n for 0, a soma e trivialmente 0.
	// Sem esta linha, a funcao chamaria Somar(-1), Somar(-2)... ate estourar a memoria.
	if n <= 0 {
		return 0
	}

	// 2. Hipotese indutiva + Passo indutivo:
	// Confiamos que Somar(n-1) sabe calcular a soma dos anteriores (hipotese).
	// Adicionamos n ao resultado do subproblema para resolver o tamanho atual (passo).
	return n + Somar(n-1)
}

func main() {
	resultado := Somar(5)
	fmt.Printf("Soma de 1 ate 5: %d\n", resultado) // Saida: 15
}

Quando você programa recursão, o segredo psicológico para não ficar confuso é adotar a chamada fé indutiva (inductive leap): quando você chama Somar(n - 1), você não tenta simular mentalmente toda a árvore de chamadas que vai acontecer dali para baixo. Você simplesmente assume que a função funciona para problemas de tamanho menor, exatamente como fazemos na hipótese indutiva, e foca apenas em como somar o valor atual n a essa resposta.

5. A pilha de execução por dentro

O que o runtime de Go faz nos bastidores enquanto a indução matemática opera no papel?

Cada vez que uma função chama outra função (ou a si mesma), o runtime aloca um quadro de pilha () na memória da goroutine. Esse quadro armazena os parâmetros locais e o endereço de retorno para onde o processador deve voltar quando a função terminar.

A execução de um algoritmo recursivo acontece em duas fases distintas: a descida (empilhamento de hipóteses pendentes) e a subida (resolução e retorno dos valores calculados).

Pilha de execução recursiva em Go: empilhamento e desempilhamento. Frame 1: Somar(3) → Frame 2: Somar(2) (chama n-1)…
figura 2 · Pilha de execução recursiva em Go: empilhamento e desempilhamento

A figura ilustra a chamada Somar(3). Na fase 1 (descida), a execução parte do topo: Somar(3) não sabe a resposta imediatamente, então empilha um quadro e invoca Somar(2). A mesma espera acontece em cascata até que Somar(1) invoca Somar(0).

Na fase 2, a descida bate no caso base: n == 0. Esse quadro não faz novas chamadas; ele responde 0 imediatamente.

Na fase 3 (subida), a pilha começa a ser desfeita de baixo para cima. O retorno 0 entra no cálculo de Somar(1), resultando em 1 + 0 = 1. Esse 1 sobe para Somar(2), calculando 2 + 1 = 3. Por fim, o 3 sobe para Somar(3), calculando 3 + 3 = 6. Cada quadro desempilhado libera sua memória e entrega o resultado ao chamador.

Se o programador esquecer o caso base, essa descida nunca encontra um chão. A goroutine aloca quadros continuamente até esgotar o limite da pilha, gerando o temido erro fatal em tempo de execução: runtime: goroutine stack exceeds 1000000000-byte limit (o ).

6. Indução forte e divisão e conquista

A indução que vimos até agora é chamada de indução fraca ou simples, porque para provar P(k+1) usamos apenas o antecessor imediato, P(k).

Porém, muitos dos algoritmos mais rápidos da computação dividem o problema pela metade, e não por n - 1. Quando você divide um array de 100 elementos em dois pedaços de 50, o estado de tamanho 50 não é o antecessor imediato de 100. Nesses cenários, precisamos da indução forte (ou indução completa).

Na indução forte, a hipótese indutiva assume que a afirmação vale para todos os valores anteriores, desde a base até k: Assumimos que P(0), P(1), P(2), …, P(k) são todos verdadeiros. Com isso, provamos que P(k+1) é verdadeiro.

O exemplo supremo da indução forte na computação é o Merge Sort (ordenação por intercalação):

Indução forte e divisão e conquista em Go (Merge Sort). MergeSort([38, 27, 43, 3]) → Sub-array esquerdo (divide ao meio)…
figura 3 · Indução forte e divisão e conquista em Go (Merge Sort)

O diagrama representa a ordenação do slice [38, 27, 43, 3]. O algoritmo opera em três atos:

  1. Caso base: slices com tamanho 0 ou 1 já estão trivialmente ordenados.
  2. Hipótese indutiva forte: assumimos que qualquer slice com tamanho estritamente menor que n será perfeitamente ordenado pelo algoritmo.
  3. Passo indutivo: dividimos o slice de tamanho n ao meio. Pela hipótese forte, a metade esquerda [38, 27] vira [27, 38] e a metade direita [43, 3] vira [3, 43]. O passo consiste em intercalar (merge) essas duas listas já ordenadas, produzindo [3, 27, 38, 43].

Veja como essa estrutura se traduz em código Go idiomático:

package main

import "fmt"

// MergeSort ordena uma fatia de inteiros aplicando divisao e conquista.
func MergeSort(itens []int) []int {
	// 1. Caso base da inducao forte:
	// Um slice com 0 ou 1 elemento ja esta ordenado por definicao.
	if len(itens) <= 1 {
		return itens
	}

	// 2. Divisao do problema em subproblemas estritamente menores:
	meio := len(itens) / 2

	// 3. Hipotese forte: confiamos que o algoritmo ordena qualquer fatia com len < n.
	esquerda := MergeSort(itens[:meio])
	direita := MergeSort(itens[meio:])

	// 4. Passo indutivo: intercalar duas fatias que ja estao garantidamente ordenadas.
	return intercalar(esquerda, direita)
}

// intercalar combina dois slices ordenados em um unico slice ordenado.
func intercalar(a, b []int) []int {
	resultado := make([]int, 0, len(a)+len(b))
	i, j := 0, 0

	for i < len(a) && j < len(b) {
		if a[i] <= b[j] {
			resultado = append(resultado, a[i])
			i++
		} else {
			resultado = append(resultado, b[j])
			j++
		}
	}

	resultado = append(resultado, a[i:]...)
	resultado = append(resultado, b[j:]...)
	return resultado
}

func main() {
	dados := []int{38, 27, 43, 3, 9, 82, 10}
	ordenados := MergeSort(dados)
	fmt.Println("Slice ordenado:", ordenados)
}

A prova de que o Merge Sort tem complexidade O(n log n) e funciona para qualquer tamanho decorre diretamente do fato de que a divisão sempre produz fatias menores que convergem para o caso base de tamanho 1.

7. Erros comuns e boas práticas

  1. O passo indutivo que não diminui o problema. Em Go, se a sua chamada recursiva for f(n) em vez de f(n - 1) ou f(n / 2), o tamanho do subproblema nunca diminui. O código entra em recursão infinita e a pilha estoura. Todo passo recursivo precisa caminhar estritamente em direção ao caso base.
  2. Caso base ausente ou incompleto. Se uma função lida com números inteiros e só tem if n == 0, passar um número negativo fará o programa rodar até quebrar a pilha. Sempre proteja o caso base com comparações defensivas, como if n <= 0.
  3. Esquecer que Go aloca pilha dinamicamente. Diferente de C, que tem pilhas fixas e pequenas por thread (geralmente 1 a 8 MB), cada goroutine em Go começa com uma pilha minúscula (cerca de 2 KB) que cresce sob demanda na heap se necessário. Mesmo assim, recursões profundas com centenas de milhares de níveis degradam a performance devido a cópias de stack frame. Para problemas lineares gigantescos, prefira laços iterativos com for.
  4. Suposição circular. Na hora de escrever uma prova matemática, usar o próprio P(k+1) para provar P(k+1) é o erro mais comum entre estudantes. Você só pode manipular algebricamente os termos somando ou subtraindo a partir da hipótese P(k).

8. Exercícios de fixação

Exercício 1: Prova matemática da soma de potências de 2

Demonstre por indução matemática que, para todo n ≥ 0: 2⁰ + 2¹ + 2² + … + 2ⁿ = 2ⁿ⁺¹ - 1

Resolução:

  1. Caso base (n = 0): Lado esquerdo: 2⁰ = 1. Lado direito: 2⁰⁺¹ - 1 = 2¹ - 1 = 2 - 1 = 1. Ambos são iguais a 1. A base está provada.

  2. Hipótese indutiva: Assumimos verdadeiro para k: 2⁰ + 2¹ + … + 2ᵏ = 2ᵏ⁺¹ - 1.

  3. Passo indutivo: Queremos provar para k + 1: (2⁰ + 2¹ + … + 2ᵏ) + 2ᵏ⁺¹ Substituímos a hipótese indutiva no bloco inicial: = (2ᵏ⁺¹ - 1) + 2ᵏ⁺¹ Agrupamos os termos iguais: = 2 · (2ᵏ⁺¹) - 1 Pelas propriedades da potenciação (multiplicação de bases iguais soma os expoentes): = 2⁽ᵏ⁺¹⁾⁺¹ - 1 A expressão resultante é exatamente a fórmula para k + 1. Demonstração concluída.

Exercício 2: Exponenciação rápida recursiva em Go

Implemente uma função recursiva em Go que calcule xⁿ em tempo O(log n), usando a técnica de divisão e conquista baseada na indução forte: Se n for par, xⁿ = (xⁿ/²)². Se n for ímpar, xⁿ = x · xⁿ⁻¹.

Resolução em código Go:

package main

import "fmt"

// Potencia calcula x^n em tempo O(log n) usando divisao indutiva.
func Potencia(x float64, n int) float64 {
	// Caso base: qualquer numero elevado a zero e 1.
	if n == 0 {
		return 1
	}

	// Se n for negativo, invertemos a base.
	if n < 0 {
		return 1 / Potencia(x, -n)
	}

	// Se n for par: calculamos a metade uma unica vez (hipotese) e elevamos ao quadrado.
	if n%2 == 0 {
		metade := Potencia(x, n/2)
		return metade * metade
	}

	// Se n for impar: isolamos um fator x e resolvemos o restante como n-1.
	return x * Potencia(x, n-1)
}

func main() {
	fmt.Printf("2^10 = %.0f\n", Potencia(2, 10)) // Saida: 1024
}

9. Glossário

  • Caso base: O menor elemento sobre o qual uma afirmação ou função é avaliada, resolvido diretamente sem chamadas subsequentes.
  • Hipótese indutiva: A premissa temporária de que uma propriedade P vale para um índice genérico k ou para todos os índices até k.
  • Passo indutivo: A demonstração formal de que a validade no índice k acarreta a validade no índice k+1.
  • Indução forte: Variante do método onde a hipótese assume a veracidade de todos os antecessores anteriores, essencial para algoritmos de divisão e conquista.
  • Stack frame: Registro alocado na memória para guardar variáveis locais e contexto de retorno durante a execução de uma função.
  • Stack overflow: Falha de memória causada pelo empilhamento excessivo de chamadas que ultrapassa o espaço limite alocado pelo sistema.

10. Fontes e referências

  • Rosen, Kenneth H. Discrete Mathematics and Its Applications, 8ª edição, McGraw-Hill, 2019 (Capítulo 5: Induction and Recursion).
  • Donovan, Alan A. A.; Kernighan, Brian W. The Go Programming Language, Addison-Wesley, 2015 (Capítulo 5: Functions and Recursion).
  • Lehman, Eric; Leighton, F. Thomson; Meyer, Albert R. Mathematics for Computer Science, MIT OpenCourseWare, 2018 (Capítulo 5: Well Ordering Principle and Induction). Consulta em novembro de 2026.

Relacionados

  • Lógica proposicional e de primeira ordem
  • Tempo logarítmico
  • Big O
  • Tecnologia/Linguagem de programação/GoLang/Go e Integração com Hardware

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?