Você quer saber quantos arquivos existem na pasta Documentos. Ela tem alguns arquivos e algumas subpastas. Cada subpasta pode ter mais arquivos e mais subpastas, sem limite de profundidade. Uma quantidade fixa de laços for aninhados não resolve, porque você não sabe quantos níveis existem. (Dá para resolver com um laço e uma pilha montada à mão, mas a recursão é o caminho mais direto e é o que vamos estudar.)
Como repetir a mesma estratégia em cada subpasta e saber quando parar?
Pense em como você faria à mão: “o total de Documentos é o número de arquivos soltos nela mais o total de cada subpasta”. Para saber o total de uma subpasta, você aplica a mesma regra. Quando chega a uma pasta sem subpastas, a resposta é imediata. Uma regra que usa a si mesma em um problema menor é uma recursão. Vamos implementar exatamente esse problema mais adiante. Antes, um exemplo menor para enxergar o mecanismo.
Conceitos centrais
Uma função recursiva chama a si mesma com uma versão menor do problema, até chegar a um caso que sabe responder diretamente.
Caso base
O caso pequeno o bastante para responder sem nova chamada, como uma pasta sem subpastas. É ele que faz a recursão parar.
Redução
Transforma o problema em problemas menores, como cada subpasta em vez da pasta inteira.
Junção
Junta as respostas menores para montar a resposta atual. No exemplo: arquivos soltos + total de cada subpasta.
Pilha
Cada chamada em andamento fica guardada na memória, esperando a resposta das chamadas que ela fez.
A receita em três perguntas
Qual é o menor caso? Ele vira o caso base.
Se eu já tivesse a resposta para um problema menor, como montaria a resposta atual? Isso define a junção.
Toda chamada se aproxima do caso base? Isso garante que a recursão termina.
Na pergunta 2, confie na chamada recursiva. Não tente simular tudo de cabeça: suponha que contar(subpasta) já funciona e pense apenas em como usar a resposta dela.
A pilha de chamadas
O exemplo mais simples possível: somar os números de 1 até n. A soma de 1 a 4 é 4 + (soma de 1 a 3), e assim por diante, até a soma de 1 a 0, que é 0.
Programa completo: compile e execute
#include <stdio.h>
// Pré-condição: n >= 0 (com n negativo, nunca chega ao caso base).
int soma(int n) {
if (n == 0) return 0; // caso base
return n + soma(n - 1); // redução + junção
}
int main(void) {
printf("%d\n", soma(4));
return 0;
}
Saída: 10
O comentário “Pré-condição” no código diz o que precisa ser verdade antes de chamar a função para que ela funcione. Aqui, n não pode ser negativo.
O que acontece por baixo é o que mais confunde quem está começando, e a figura mostra passo a passo:
As chamadas descem até o caso base, e cada uma fica parada esperando a de baixo. Só então os resultados sobem, e cada nível faz a sua soma. A conta acontece na volta, e não na ida.
Cada chamada em espera ocupa um pedaço da pilha, uma área de memória pequena (cerca de 1 MB no Windows e 8 MB no Linux, por padrão). Por isso, uma recursão muito profunda trava o programa, mesmo com um caso base correto. Nos testes desta aula (Windows, compilador MSVC sem otimizações, pilha padrão de 1 MB), soma(10000) funcionou, e soma(50000) terminou com stack overflow. O limite exato muda com o sistema, o compilador e as opções de compilação, e o tamanho da pilha pode ser configurado. Para somar 50 mil números, um laço for é a escolha certa.
Ida ou volta: onde fica o printf muda tudo
Imagine uma função que imprime n e depois chama a si mesma com n − 1, parando em 0. Com n = 3, a impressão acontece na descida, e a saída é 3 2 1. Se a mesma função chamar a si mesma primeiro e imprimir depois, o printf só roda na volta, e a saída se inverte para 1 2 3. O exercício 2 mostra o código das duas versões para você prever a saída.
Exemplo real: contar arquivos em pastas
Agora o problema do início. Cada pasta é uma struct com o número de arquivos soltos e ponteiros para as subpastas. É a junção das duas aulas anteriores.
Programa completo: compile e execute
#include <stdio.h>
typedef struct Pasta {
const char *nome;
int arquivos; // arquivos diretamente nesta pasta
int qtd_sub; // quantas subpastas ela tem
struct Pasta *sub[4]; // ponteiros para as subpastas
} Pasta;
// Pré-condições: 0 <= qtd_sub <= 4, ponteiros válidos e nenhuma pasta
// contida em si mesma (sem ciclos), senão a recursão nunca termina.
int contar(const Pasta *p) {
int total = p->arquivos; // arquivos soltos
for (int i = 0; i < p->qtd_sub; i++) {
total += contar(p->sub[i]); // mesma regra em cada subpasta
}
return total;
}
int main(void) {
Pasta viagem = {"Viagem", 3, 0, {0}};
Pasta fotos = {"Fotos", 5, 1, {&viagem}};
Pasta relat = {"Relatorios", 4, 0, {0}};
Pasta trab = {"Trabalho", 1, 1, {&relat}};
Pasta docs = {"Documentos", 2, 2, {&fotos, &trab}};
printf("Total de arquivos: %d\n", contar(&docs));
return 0;
}
Saída: Total de arquivos: 15
Cada pasta faz a mesma pergunta às suas subpastas e soma as respostas. Viagem e Relatórios não têm subpastas: o laço não executa nenhuma vez, e elas devolvem só os próprios arquivos. Esse é o caso base, mesmo sem um if explícito.
Repare que contar não sabe, e não precisa saber, quantos níveis a árvore tem. Ela funciona igual para 2 ou 200 níveis. Esse tipo de estrutura, em que cada item contém outros itens do mesmo tipo, aparece em todo lugar: pastas, menus com submenus, comentários com respostas, organogramas, o HTML desta página. Recursão é a ferramenta natural para percorrê-los.
Padrões recursivos
Muitos problemas recursivos de prova ou de entrevista se encaixam em um destes formatos. Reconhecer o formato é metade da solução.
1. Reduzir de um em um
Uma chamada por vez, cada uma com um problema uma unidade menor: soma, fatorial, contagem regressiva. É fácil de escrever, mas faz uma chamada por elemento. Para listas grandes, prefira um laço.
2. Uma chamada para cada filho
Pastas, menus, organogramas: a função se chama uma vez para cada item interno, como em contar. O número total de chamadas é igual ao número de itens da estrutura, sem desperdício.
3. Cortar pela metade: busca binária
Pense em procurar uma palavra no dicionário. Você não lê página por página: abre no meio, vê se a palavra vem antes ou depois e descarta metade do livro. Depois repete na metade que sobrou. Em um vetor ordenado, a busca binária faz exatamente isso:
Funções: copie para um programa que tenha main
// Procura alvo em v[ini..fim], vetor em ordem crescente. Retorna a posição ou -1.
int busca_binaria(const int v[], int ini, int fim, int alvo) {
if (ini > fim) return -1; // não sobrou nada: não existe
int meio = ini + (fim - ini) / 2;
if (v[meio] == alvo) return meio; // achou
if (v[meio] < alvo)
return busca_binaria(v, meio + 1, fim, alvo); // só pode estar à direita
return busca_binaria(v, ini, meio - 1, alvo); // só pode estar à esquerda
}
A cada passo, o trecho onde o alvo pode estar cai pela metade. As posições em cinza foram descartadas. Delas, só os meios de cada passo (47, 88 e 63) chegaram a ser lidos; as outras nunca foram olhadas.
O ganho é enorme na prática. Nos testes desta aula, o pior caso foi:
Elementos examinados no pior caso, medidos
Tamanho da lista
Procurando um por um
Busca binária
100 nomes
100
7
1 000 nomes
1 000
10
1 milhão de nomes
1 000 000
20
Cada vez que a lista dobra de tamanho, a busca binária precisa de só um passo a mais. A condição é que a lista esteja ordenada, e é para isso que servem as próximas três aulas.
E onde está a recursão? Cada passo da figura é uma chamada de busca_binaria. Para achar o 72, a pilha chega a ter no máximo 4 chamadas abertas, uma por passo. Para 1 milhão de nomes, seriam no máximo 20.
4. Duas chamadas que se repetem: Fibonacci Aprofundamento
Na sequência de Fibonacci (0, 1, 1, 2, 3, 5, 8, 13…), cada número é a soma dos dois anteriores. A tradução direta para C é curta e elegante, e muito lenta:
Funções: copie para um programa que tenha main
long long fib_ingenuo(int n) {
if (n < 2) return n;
return fib_ingenuo(n - 1) + fib_ingenuo(n - 2);
}
Para calcular fib(5), a função calcula fib(3) duas vezes e fib(2) três vezes, sem lembrar que já fez esse trabalho. Cada valor a mais de n multiplica o trabalho por cerca de 1,6.
Medido em um Intel Core i5-9600K, Windows 11, MSVC 19.51 sem otimizações (/Od). As contagens de chamadas são exatas; os tempos variam com a máquina.
Chamada
Chamadas feitas
Tempo
fib_ingenuo(20)
21 891
instantâneo
fib_ingenuo(30)
2 692 537
milissegundos
fib_ingenuo(40)
331 160 281
0,66 segundo
fib_ingenuo(45)
3 672 623 805
cerca de 7 segundos
A solução é anotar cada resposta na primeira vez que ela é calculada e consultar a anotação nas vezes seguintes. Essa técnica se chama memoização:
Funções: copie para um programa que tenha main
long long memo[91]; // global: começa zerado; fib(90) ainda cabe em long long
long long fib(int n) { // Pré-condição: 0 <= n <= 90.
if (n < 2) return n;
if (memo[n] != 0) return memo[n]; // já calculado: só consulta
memo[n] = fib(n - 1) + fib(n - 2); // calcula uma vez e anota
return memo[n];
}
Com a anotação, fib(40) faz 79 chamadas em vez de 331 milhões, e fib(90), que na versão ingênua levaria séculos, faz 179 chamadas e responde na hora. Repare no padrão: na primeira chamada, com a tabela memo ainda zerada e n ≥ 1, fib(n) faz exatamente 2n − 1 chamadas. Se a tabela já tiver valores anotados de chamadas anteriores, faz menos. Cada valor de 2 a n é calculado uma única vez e consulta a tabela no máximo uma vez, então o trabalho cresce junto com n, e não explode.
5. Testar todas as escolhas: backtracking Aprofundamento
Uma pizzaria quer listar todas as pizzas possíveis com três ingredientes opcionais. Para cada ingrediente, há duas escolhas: colocar ou não colocar. A recursão decide um ingrediente por nível e, ao voltar, experimenta a outra opção:
A recursão percorre a árvore de decisões da esquerda para a direita. Cada folha é uma pizza impressa. Quando você executar o programa abaixo, a ordem da saída será exatamente a ordem das folhas.
Agora o código. Repare na linha escolhido[i] = 0;: é o momento em que a recursão volta a um nó da árvore e pega o outro ramo.
Programa completo: compile e execute
#include <stdio.h>
const char *ingredientes[] = {"queijo", "calabresa", "cebola"};
// i: qual ingrediente está sendo decidido; escolhido[k] = 1 se o k-ésimo entrou.
void montar(int i, int escolhido[]) {
if (i == 3) { // decidiu todos: imprime a pizza
printf("Pizza:");
int vazia = 1;
for (int k = 0; k < 3; k++) {
if (escolhido[k]) { printf(" %s", ingredientes[k]); vazia = 0; }
}
if (vazia) printf(" so massa");
printf("\n");
return;
}
escolhido[i] = 1; // escolha 1: COM o ingrediente i
montar(i + 1, escolhido);
escolhido[i] = 0; // escolha 2: SEM o ingrediente i (desfaz)
montar(i + 1, escolhido);
}
int main(void) {
int escolhido[3];
montar(0, escolhido);
return 0;
}
Esse padrão resolve problemas como gerar senhas, montar escalas de trabalho e escolher itens para caber em uma mochila. É a base do desafio hacker desta aula.
Quanto custa uma recursão? Aprofundamento
Não é preciso fórmula para ter uma boa estimativa. Faça duas perguntas:
Quantas chamadas vão acontecer no total, e quanto trabalho cada uma faz? É o que define o tempo. Desenhe a árvore para uma entrada pequena e veja como ela cresce. Cuidado com o trabalho por chamada: as 8 pizzas são 8 folhas, mas imprimir cada uma percorre os ingredientes.
Quantas chamadas ficam abertas ao mesmo tempo? É o que define a memória da pilha: a altura da árvore, e não o total.
Como cada padrão cresce na prática
Formato
Exemplo
Se a entrada dobra…
Na prática
Reduz de um em um
soma, fatorial
as chamadas dobram
ok até alguns milhares, depois a pilha estoura
Uma chamada por item
contar pastas
as chamadas dobram
proporcional ao tamanho da estrutura
Corta pela metade
busca binária
só +1 chamada
1 milhão de itens em 20 passos
Duas chamadas com repetição
Fibonacci ingênuo
explode
cerca de 7 s para n = 45; use memoização
Todas as combinações
pizzas, subconjuntos
cada item a mais dobra tudo
20 itens: 1 milhão de casos; 40 itens: 1 trilhão
Nas próximas aulas, esses comportamentos ganham nomes: O(n) para “cresce junto com a entrada”, O(log n) para “corta pela metade” e O(2ⁿ) para “dobra a cada item”. A intuição da tabela vale mais que a notação.
Armadilhas frequentes
Esquecer o caso base, ou escrever um que nunca é alcançado: contagem(-1) com teste n == 0 nunca para.
Ignorar o retorno: escrever soma(n - 1); em vez de return n + soma(n - 1);. A conta é feita e jogada fora.
Calcular a mesma coisa duas vezes:f(n/2) + f(n/2) dobra o trabalho em cada nível, e esse dobro se acumula: o que seria cerca de log n chamadas vira algo proporcional a n. Guarde o resultado em uma variável e reutilize.
Recursão profunda demais: dezenas de milhares de níveis estouram a pilha. Para percorrer uma lista longa, use um laço.
Overflow: 13! não cabe em um int de 32 bits. Em C, estourar um inteiro com sinal é comportamento indefinido: o programa não avisa, e o resultado não é confiável.
Exercícios progressivos
Para cada exercício, desenhe a pilha ou a árvore de chamadas antes de escrever código, como nas figuras da aula. A dica não entrega a resposta: ela indica a pergunta certa a fazer.
1. Contagem regressiva do foguete Básico
O software de uma base de lançamento usa esta função para a contagem regressiva:
contagem(3) imprime 3 2 1 Lancar!. Um operador digitou -1 por engano, e o sistema travou. O que aconteceu? Corrija a função para que contagem(-1) apenas imprima Lancar!.
Ver uma dica
Escreva a sequência de chamadas a partir de contagem(-1). Ela chega a 0 em algum momento? Que mudança no teste do caso base cobre 0 e qualquer valor abaixo de 0?
Conferir a resposta
A partir de −1, as chamadas seguem para −2, −3, −4…, se afastando de 0. O caso base nunca é alcançado. Cada chamada ocupa um pedaço da pilha até ela acabar, e o programa termina com stack overflow.
Funções: copie para um programa que tenha main
void contagem(int n) {
if (n <= 0) { // cobre 0 e qualquer negativo
printf("Lancar!\n");
return;
}
printf("%d ", n);
contagem(n - 1);
}
Lição: o caso base deve cobrir todas as entradas em que não há mais o que reduzir, e não só a entrada “esperada”. Use <= em vez de == sempre que o valor puder passar do ponto de parada.
2. O log fora de ordem Básico
Um desenvolvedor adicionou mensagens de log a três versões de uma função e se surpreendeu com a ordem em que elas apareceram. Preveja a saída de cada uma para n = 3:
Use a figura da pilha. Um printfantes da chamada roda enquanto as chamadas descem. Um printfdepois da chamada só roda quando a chamada de baixo terminar, ou seja, na subida. Em que ordem as chamadas terminam?
Conferir a resposta
ida(3): 3 2 1. Imprime na descida, do maior para o menor.
volta(3): 1 2 3. volta(3) só imprime depois que volta(2) termina, que só imprime depois de volta(1). A primeira a imprimir é a mais profunda.
ambos(3): 3 2 1 1 2 3. Imprime na descida e de novo na subida, como um espelho.
Esse comportamento é útil, e não só uma curiosidade. Para imprimir os dígitos de um número na ordem certa (123 → “1 2 3”), o truque é chamar a recursão com n / 10antes de imprimir n % 10.
3. Ordem dos seminários Básico
Cinco grupos vão apresentar seminários, e o professor quer saber de quantas ordens diferentes as apresentações podem acontecer. Há 5 opções para o primeiro, 4 para o segundo, e assim por diante: 5 × 4 × 3 × 2 × 1, que é o fatorial de 5. Escreva int fatorial(int n) recursivo, para 0 ≤ n ≤ 12, e responda: quantas ordens existem para 5 grupos? E por que a função não se pode confiar no resultado para 13 grupos?
Ver uma dica
Aplique a receita. Qual é o menor caso? Quantas ordens existem com 1 grupo? E com 0 grupos: zero ordens ou uma? Escolha a resposta que faz a fórmula funcionar para n = 1. Se você soubesse quantas ordens existem para n − 1 grupos, como chegaria às de n grupos?
Conferir a resposta
Funções: copie para um programa que tenha main
int fatorial(int n) {
if (n == 0) return 1; // 0 grupos: 1 ordem (a vazia)
return n * fatorial(n - 1); // n escolhas para o 1º, vezes o resto
}
fatorial(5) = 120 ordens.
O fatorial cresce muito rápido. 12! = 479 001 600 ainda cabe em um int de 32 bits, cujo limite é cerca de 2,1 bilhões. Já 13! = 6 227 020 800 não cabe. Em C, estourar um int (inteiro com sinal) é comportamento indefinido: o padrão da linguagem não diz o que acontece. Em muitas máquinas o valor parece “dar a volta” e sai errado sem aviso, mas não se pode contar nem com isso. Só os tipos unsigned têm a volta garantida. A solução é verificar o limite antes de multiplicar. Para ir um pouco além, use long long, que comporta até 20!.
4. A pasta mais funda Intermediário
O sistema de backup precisa saber quantos níveis de pastas existem, porque caminhos muito longos causam erro em alguns sistemas. Usando a struct Pasta da aula, escreva int profundidade(const Pasta *p). Uma pasta sem subpastas tem profundidade 1. Para a árvore de exemplo (Documentos → Fotos → Viagem), a resposta é 3.
Ver uma dica
Compare com contar. Lá, você somava as respostas das subpastas. Aqui, que operação faz sentido aplicar às profundidades das subpastas? Depois disso, quanto a pasta atual acrescenta?
Conferir a resposta
Funções: copie para um programa que tenha main
int profundidade(const Pasta *p) {
int maior = 0; // maior profundidade entre as subpastas
for (int i = 0; i < p->qtd_sub; i++) {
int d = profundidade(p->sub[i]);
if (d > maior) maior = d;
}
return 1 + maior; // a própria pasta conta como 1 nível
}
Para Viagem e Relatórios, o laço não executa, e a função devolve 1 + 0 = 1. Fotos devolve 1 + 1 = 2, Trabalho também 2, e Documentos devolve 1 + max(2, 2) = 3.
A estrutura é idêntica à de contar. Só muda a junção: soma virou máximo. Esse é o poder da receita: o esqueleto se repete, e a pergunta 2 muda.
5. Torre de Hanói Avançado
O quebra-cabeça tem três hastes (A, B, C) e n discos de tamanhos diferentes empilhados em A, do maior para o menor. O objetivo é levar todos para C, movendo um disco por vez e nunca colocando um disco maior sobre um menor.
Escreva hanoi(n, origem, destino, aux), que imprime os movimentos.
Quantos movimentos são necessários para 3 discos? E para 20?
Uma lenda diz que monges movem 64 discos, um por segundo. Quanto tempo eles levam?
Ver uma dica
Não tente pensar disco por disco. Pense só no maior. Para ele sair de A e ir para C, onde precisam estar todos os outros n − 1 discos? Como eles chegam lá? Depois que o maior está em C, o que falta fazer? Confie que a recursão sabe mover n − 1 discos.
Conferir a resposta
Funções: copie para um programa que tenha main
void hanoi(int n, char origem, char destino, char aux) {
if (n == 0) return; // nenhum disco: nada a fazer
hanoi(n - 1, origem, aux, destino); // 1) tira os n-1 menores do caminho
printf("disco %d: %c -> %c\n", n, origem, destino); // 2) move o maior
hanoi(n - 1, aux, destino, origem); // 3) coloca os n-1 sobre o maior
}
disco 1: A -> C
disco 2: A -> B
disco 1: C -> B
disco 3: A -> C
disco 1: B -> A
disco 2: B -> C
disco 1: A -> C
Para 3 discos, são 7 movimentos. Cada disco a mais exige mover a pilha menor duas vezes, mais um movimento: 1, 3, 7, 15, 31… o número dobra e soma 1 a cada disco. Para 20 discos, o programa contou 1 048 575 movimentos.
Para 64 discos, são cerca de 18 quintilhões (1,8 × 10¹⁹) de movimentos. A um por segundo, isso daria uns 585 bilhões de anos, mais de 40 vezes a idade do universo. É o mesmo crescimento explosivo da tabela de custos: cada item a mais dobra tudo.
./desafio --nivel=hacker
Desafio hacker: o cofre das somas
Tipo: opcional
Dificuldade: ★★★★
Tema: backtracking
Restrição: n ≤ 20
Um cofre abre quando você coloca na balança um conjunto de pesos que soma exatamente o valor alvo. Você recebe n pesos inteiros positivos. Imprima todos os subconjuntos cuja soma é alvo, cada um uma única vez, preservando a ordem original dos elementos. Os subconjuntos são de posições: se dois pesos têm o mesmo valor, as combinações com um ou com o outro contam como diferentes, mesmo que as linhas impressas fiquem iguais.
Entradav = {3, 34, 4, 12, 5, 2}
alvo = 9
Saída{3, 4, 2}
{4, 5}
Requisitos: use recursão, sem laços para gerar combinações, e corte os caminhos que não podem levar a uma solução. Esse corte se chama poda: abandonar um caminho da árvore assim que fica claro que ele não leva a nada. Ao final, estime até quantos pesos o programa continua rápido.
Dica 1: é a pizzaria de novo
Cada peso é um “ingrediente”: entra ou não entra na balança. A árvore de decisões é a mesma da figura das pizzas, só que com 6 níveis em vez de 3. Em vez de imprimir todas as folhas, imprima só as que somam o alvo.
Dica 2: o que a função precisa saber?
Ela precisa do índice do peso que está decidindo (i), de quanto ainda falta para o alvo (resto) e dos pesos escolhidos até agora, em um vetor auxiliar com tamanho k. Quando resto chega a 0, a combinação é uma solução. Quando resto fica negativo, nenhum peso positivo pode consertar esse caminho: desista dele.
Solução comentada
Programa completo: compile e execute
#include <stdio.h>
void imprimir(const int escolhidos[], int k) {
printf("{");
for (int i = 0; i < k; i++) printf(i ? ", %d" : "%d", escolhidos[i]);
printf("}\n");
}
// i: próximo peso a decidir; resto: quanto falta; escolhidos[0..k-1]: decisão parcial.
void buscar(const int v[], int n, int i, int resto, int escolhidos[], int k) {
if (resto == 0) { // caso base 1: achou
imprimir(escolhidos, k);
return; // pesos positivos: adicionar mais só passaria do alvo
}
if (i == n || resto < 0) return; // caso base 2: acabou ou estourou (poda)
escolhidos[k] = v[i]; // escolha: INCLUIR v[i]
buscar(v, n, i + 1, resto - v[i], escolhidos, k + 1);
buscar(v, n, i + 1, resto, escolhidos, k); // escolha: NÃO incluir v[i]
}
int main(void) {
int v[] = {3, 34, 4, 12, 5, 2};
int escolhidos[6];
buscar(v, 6, 0, 9, escolhidos, 0);
return 0;
}
Saída: {3, 4, 2} e {4, 5}.
Por que funciona. Cada caminho da raiz até uma folha corresponde a exatamente uma combinação de pesos, como cada caminho da figura corresponde a uma pizza. Por isso, nenhuma combinação aparece duas vezes e nenhuma é esquecida. O “desfazer” é implícito: ao voltar da primeira chamada, o próximo peso escolhido sobrescreve escolhidos[k], e k continua valendo o que valia antes.
Até onde é rápido? Cada peso a mais dobra o número de caminhos. Com 6 pesos, são no máximo 64 caminhos. Com 20, cerca de 1 milhão, o que leva uma fração de segundo. Com 40, cerca de 1 trilhão, o que levaria horas. A poda resto < 0 não ajuda no pior caso, mas na prática elimina a maior parte da árvore. Uma poda extra que respeita a ordem original: calcule antes, para cada posição i, a soma de todos os pesos de i até o fim. Se essa soma já for menor que resto, nenhuma escolha restante alcança o alvo, e o ramo pode ser cortado. (Ordenar os pesos também ajudaria a cortar, mas mudaria a ordem em que as soluções são impressas, que o enunciado exige preservar.)
Testes que você deve rodar: alvo impossível (nenhuma saída), alvo igual à soma de todos os pesos (uma saída), pesos repetidos como {2, 2} com alvo 2 (duas saídas, pois são posições diferentes) e comparação com uma força bruta que testa todas as 2ⁿ combinações.
Indo além: se você só precisa saber se existe solução, e o alvo é pequeno, dá para aplicar a mesma ideia da memoização de Fibonacci: anotar quais pares (i, resto) já foram testados e falharam. Essa técnica se chama programação dinâmica.
Revisão rápida
Três perguntas: menor caso, como reduzir, como juntar as respostas. Confie na chamada recursiva.
As chamadas descem e esperam. Os resultados sobem. Cada chamada aberta ocupa pilha.
Estruturas que contêm a si mesmas, como pastas e menus, pedem recursão.
Cortar pela metade é rápido. Repetir trabalho é lento: anote as respostas com memoização.
Testar todas as escolhas (backtracking) dobra o trabalho a cada item. Corte os caminhos sem futuro.