0Pricing
DSA Interview Prep · Aula

Troca de Moedas e Escada de Custo Mínimo

Formule as recorrências de coin-change e min-cost-climbing-stairs, escolha a direção correta da DP e acompanhe a tabela manualmente.

Troca de Moedas e Escada de Custo Mínimo é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.

Troca de Moedas: o Problema

Troca de Moedas (LeetCode #322) fornece denominações de moedas e um valor-alvo. Encontre o número mínimo de moedas necessário para formar exatamente esse valor. Você tem moedas ilimitadas de cada denominação. Essa é uma variante do problema da mochila ilimitada — cada item (moeda) pode ser usado qualquer número de vezes. Esse é um dos problemas de DP mais importantes, pois testa sua capacidade de formular uma recorrência do zero.

# Problem examples:
# coins=[1,5,6,9], amount=11 -> 2 (5+6 or 2+9? no: 5+6=11 YES)
# coins=[2],       amount=3  -> -1 (impossible)
# coins=[1,2,5],   amount=11 -> 3 (5+5+1)
# coins=[186,419,83,408], amount=6249 -> 20

# Key choices:
# - Try each coin denomination at each step
# - Minimum coins = 1 + minimum(coins to make amount - coin)
# - If amount < 0: impossible
# - If amount = 0: done (0 coins)

print('Coin change: unbounded knapsack, find minimum count')

Troca de Moedas: Derivação da Recorrência

Defina dp[i] = número mínimo de moedas para formar o valor i. Para cada valor i, tente usar cada moeda c: se i >= c, então dp[i] = min(dp[i], 1 + dp[i-c]). O '1' representa a moeda que acabamos de usar; dp[i-c] é a solução ideal para o valor restante. Isso pressupõe moedas infinitas. Caso-base: dp[0] = 0. Inicialize todas as outras entradas com infinito para representar valores que 'ainda não podem ser alcançados'.

def coin_change(coins, amount):
    # dp[i] = min coins to make amount i
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0  # base: 0 coins for amount 0

    for i in range(1, amount + 1):
        for coin in coins:
            if i >= coin and dp[i - coin] != float('inf'):
                dp[i] = min(dp[i], 1 + dp[i - coin])

    return dp[amount] if dp[amount] != float('inf') else -1

print(coin_change([1, 5, 6, 9], 11))  # 2
print(coin_change([2], 3))             # -1
print(coin_change([1, 2, 5], 11))      # 3

# Trace dp for coins=[1,5] amount=6:
# dp[0]=0, dp[1]=1, dp[2]=2, dp[3]=3, dp[4]=4, dp[5]=1, dp[6]=2

Troca de Moedas: Por que a Abordagem Gulosa Falha

A abordagem gulosa (sempre escolher a maior moeda que cabe) falha na troca de moedas. Exemplo: coins=[1, 3, 4], amount=6. A abordagem gulosa escolhe 4 e depois 1+1 = 3 moedas. A solução ideal é 3+3 = 2 moedas. A abordagem gulosa funciona para denominações padrão (moedas de 1, 5, 10 e 25 centavos), pois elas coincidentemente satisfazem a propriedade gulosa. Porém, para conjuntos arbitrários de moedas, é necessário usar DP. Esse é um ponto clássico de entrevistas — afirmar que a abordagem gulosa falha e explicar o motivo demonstra forte capacidade de análise.

# Greedy failure example:
# coins=[1,3,4], amount=6
# Greedy: 4 (rem=2), 1 (rem=1), 1 (rem=0) -> 3 coins
# Optimal: 3 (rem=3), 3 (rem=0) -> 2 coins

def coin_change_greedy_wrong(coins, amount):
    coins_sorted = sorted(coins, reverse=True)
    count = 0
    for coin in coins_sorted:
        while amount >= coin:
            amount -= coin
            count += 1
    return count if amount == 0 else -1

print('Greedy:', coin_change_greedy_wrong([1,3,4], 6))  # 3 (WRONG)
print('DP:    ', coin_change([1,3,4], 6))               # 2 (CORRECT)

Troca de Moedas II: Contando as Formas

Troca de Moedas II (LeetCode #518) pede o número de formas de formar o valor (não a quantidade mínima). A recorrência muda: em vez de usar o mínimo, use a soma. dp[i] += dp[i-coin] para cada moeda. A ordem de preenchimento é importante: para contar cada combinação uma única vez, percorra as moedas no laço externo e os valores no laço interno. Inverter os laços conta permutações em vez de combinações (um problema diferente).

def coin_change_ii(coins, amount):
    # dp[i] = number of ways to make amount i
    dp = [0] * (amount + 1)
    dp[0] = 1  # one way to make amount 0: use no coins

    # Outer loop: coins -- ensures each coin type processed once
    for coin in coins:
        # Inner loop: amounts
        for i in range(coin, amount + 1):
            dp[i] += dp[i - coin]

    return dp[amount]

print(coin_change_ii([1, 2, 5], 5))   # 4: [1,1,1,1,1],[1,1,1,2],[1,2,2],[5]
print(coin_change_ii([2], 3))          # 0: impossible
print(coin_change_ii([10], 10))        # 1

# Key: coin outer, amount inner = COMBINATIONS (unordered)
# Reverse (amount outer, coin inner) = PERMUTATIONS (ordered)

Escadas de Custo Mínimo: o Problema

Escadas de Custo Mínimo (LeetCode #746) fornece uma escada em que cada degrau tem um custo. Você pode subir 1 ou 2 degraus por vez. Encontre o custo mínimo para chegar ao topo (um degrau além da última escada). Você pode começar no degrau 0 ou no degrau 1 gratuitamente. Esse problema combina elegantemente a recorrência de subir escadas com o padrão de minimização de custos da troca de moedas, tornando-se uma ponte natural entre os dois.

# cost = [10, 15, 20]
# Pay cost[i] to leave step i
# You can step to i+1 or i+2
# Goal: reach top (index 3) with minimum cost

# Path options:
# Start at 0: cost 10, go to 2: cost 20, done -> 30
# Start at 1: cost 15, go to 3: done -> 15  <- OPTIMAL
# Start at 0: cost 10, go to 1: cost 15 -> 25

cost = [10, 15, 20]
# Optimal: start at step 1, pay 15, jump to top -> cost = 15
print('Expected:', 15)

Escadas de Custo Mínimo: Recorrência

Defina dp[i] = custo mínimo para chegar ao degrau i. Você chega ao degrau i pagando cost[i-1] (a partir do degrau i-1) ou cost[i-2] (a partir do degrau i-2). Portanto, dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]). Casos-base: dp[0] = 0 (começar antes da escada é gratuito), dp[1] = 0 (também é possível começar no degrau 1 gratuitamente). A resposta é dp[n], onde n = len(cost).

def min_cost_climbing_stairs(cost):
    n = len(cost)
    # dp[i] = minimum cost to reach step i
    # Steps 0 to n; step n is the top (goal)
    dp = [0] * (n + 1)
    # dp[0] = 0 (free to start here)
    # dp[1] = 0 (free to start here)
    for i in range(2, n + 1):
        dp[i] = min(dp[i-1] + cost[i-1],   # step from i-1
                    dp[i-2] + cost[i-2])    # jump from i-2
    return dp[n]

print(min_cost_climbing_stairs([10, 15, 20]))      # 15
print(min_cost_climbing_stairs([1,100,1,1,1,100,1,1,100,1]))  # 6

Escadas de Custo Mínimo: Otimização de Espaço

Como dp[i] depende apenas de dp[i-1] e dp[i-2], podemos reduzir o espaço para O(1) com duas variáveis, assim como em Fibonacci. Substitua a matriz por prev2 e prev1. Atualize-as a cada degrau. Essa é uma otimização padrão de uma linha que os entrevistadores esperam depois que você apresenta a solução com tabela em O(n). Sempre mencione isso proativamente: 'Podemos reduzir o espaço para O(1), pois precisamos apenas dos dois últimos valores.'

def min_cost_optimised(cost):
    n = len(cost)
    prev2, prev1 = 0, 0  # dp[0] and dp[1]
    for i in range(2, n + 1):
        curr = min(prev1 + cost[i-1], prev2 + cost[i-2])
        prev2, prev1 = prev1, curr
    return prev1

print(min_cost_optimised([10, 15, 20]))  # 15
print(min_cost_optimised([1,100,1,1,1,100,1,1,100,1]))  # 6

# Alternative: directly use cost array as rolling storage
def min_cost_v2(cost):
    n = len(cost)
    for i in range(2, n):
        cost[i] += min(cost[i-1], cost[i-2])
    return min(cost[-1], cost[-2])

from copy import deepcopy
cost_test = [10,15,20]
print(min_cost_v2(deepcopy(cost_test)))  # 15

Formulação Alternativa de DP

Alguns problemas têm várias formulações válidas de DP. Para as escadas de custo mínimo, você pode definir dp[i] = custo mínimo para SAIR do degrau i (pagar cost[i] e escolher ir para i+1 ou i+2). Então dp[i] = cost[i] + min(dp[i+1], dp[i+2]), preenchendo da direita para a esquerda, e a resposta é min(dp[0], dp[1]). Ambas as formulações estão corretas. Pratique explicar qual formulação você escolheu e por quê — isso demonstra domínio de DP.

def min_cost_alternative(cost):
    n = len(cost)
    # dp[i] = min cost when starting FROM step i
    # Fill right to left
    dp = cost[:] + [0]  # dp[n] = 0 (already at top)
    for i in range(n - 1, -1, -1):
        # Pay cost[i], then choose i+1 or i+2
        if i + 2 <= n:
            dp[i] = cost[i] + min(dp[i+1], dp[i+2])
        else:
            dp[i] = cost[i] + dp[i+1]
    # Can start at step 0 or step 1
    return min(dp[0], dp[1])

print(min_cost_alternative([10, 15, 20]))  # 15
print(min_cost_alternative([1,100,1,1,1,100,1,1,100,1]))  # 6

Conectando a Troca de Moedas e as Escadas

Tanto a troca de moedas quanto as escadas de custo mínimo são instâncias do mesmo padrão de DP: a cada etapa, faça uma escolha entre um conjunto finito de opções e otimize um objetivo ao longo da sequência de escolhas. As diferenças são apenas superficiais: a troca de moedas acompanha a quantidade (adiciona 1 por moeda), enquanto as escadas acompanham o custo (adiciona cost[i] por degrau). Reconhecer essa estrutura compartilhada permite resolver novos problemas de DP mapeando-os para modelos conhecidos.

# Shared pattern:
# dp[state] = optimise(dp[prev_state_1] + cost_1,
#                      dp[prev_state_2] + cost_2, ...)

# Coin change:  dp[amount] = min(1 + dp[amount - coin] for coin in coins)
# Min stair:    dp[step]   = min(cost[step-1]+dp[step-1], cost[step-2]+dp[step-2])
# Max path sum: dp[cell]   = max(dp[top], dp[left]) + grid[cell]
# House robber: dp[house]  = max(dp[house-1], dp[house-2] + value[house])

# All four are the SAME pattern with different:
# - State representation
# - Number of choices per state
# - Objective (min/max)
# - Transition cost
print('DP pattern: state + choices + objective + cost = template')

Número Mínimo de Quadrados Perfeitos

Quadrados Perfeitos (LeetCode #279) pede o número mínimo de quadrados perfeitos (1, 4, 9, 16, ...) cuja soma seja n. Isso é exatamente um problema de troca de moedas em que as 'moedas' são números quadrados perfeitos. Gere todos os quadrados perfeitos até n e depois execute a troca de moedas. A DP resulta em tempo O(n * sqrt(n)). O Teorema dos Quatro Quadrados de Lagrange nos diz que a resposta é no máximo 4, o que também permite uma abordagem matemática em O(sqrt(n)) — mas a solução esperada é a DP.

import math

def num_squares(n):
    # Generate all perfect squares up to n
    squares = [i*i for i in range(1, int(math.sqrt(n)) + 1)]
    # Coin change with squares as 'coins'
    dp = [float('inf')] * (n + 1)
    dp[0] = 0
    for i in range(1, n + 1):
        for sq in squares:
            if i >= sq:
                dp[i] = min(dp[i], 1 + dp[i - sq])
    return dp[n]

print(num_squares(12))  # 3: 4+4+4
print(num_squares(13))  # 2: 4+9
print(num_squares(1))   # 1: 1

Depurando DP: Erros Comuns

Erros comuns em DP: caso-base incorreto (dp[0] definido incorretamente), ordem de preenchimento incorreta (acessar um valor que ainda não foi calculado), erro de uma unidade na definição do estado (dp[i] é o custo PARA chegar a i ou o custo para SAIR de i) e não retornar -1 quando ainda há infinito (casos impossíveis). Sempre teste os casos mais simples (entrada vazia, um único elemento, target=0) antes de testar entradas maiores.

# Common DP debugging checklist:
# 1. Base case: what is dp[0]? dp[1]? Are they correct?
# 2. State definition: write it in English before coding
# 3. Recurrence: trace manually on a 3-element example
# 4. Fill order: dependency arrows point left/up? Fill left/up first
# 5. Infinity check: return -1 or 0 when dp[target] == inf?
# 6. Array bounds: dp has size n+1 for 0..n, or n for 0..n-1?

# Quick test template:
def test_coin_change():
    assert coin_change([1], 0) == 0     # base case
    assert coin_change([1], 1) == 1     # single coin
    assert coin_change([2], 3) == -1    # impossible
    assert coin_change([1,5,6,9], 11) == 2
    print('All tests passed!')

test_coin_change()

Verificação rápida

Teste sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação desta lição.

Recapitulação da lição

Nesta lição, você aprendeu: DP de contagem mínima para troca de moedas (mochila ilimitada) e por que a abordagem gulosa falha, troca de moedas II para contar combinações com a ordem de moedas no laço externo e valores no laço interno, e escadas de custo mínimo com formulações da esquerda para a direita e da direita para a esquerda. A seguir, exploraremos padrões de DP 1D com ladrão de casas, o algoritmo de Kadane e quebra de palavras.

Perguntas Frequentes

A aula “Troca de Moedas e Escada de Custo Mínimo” é grátis?

Sim — o texto completo de “Troca de Moedas e Escada de Custo Mínimo” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.

O que vou aprender em “Troca de Moedas e Escada de Custo Mínimo”?

Formule as recorrências de coin-change e min-cost-climbing-stairs, escolha a direção correta da DP e acompanhe a tabela manualmente. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DSA Interview Prep?

Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “Troca de Moedas e Escada de Custo Mínimo”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DSA Interview Prep?

Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Reconhecendo DP: Subproblemas Sobrepostos
  2. DP de Cima para Baixo com Memoização
  3. DP de Baixo para Cima com Tabulação
  4. Troca de Moedas e Escada de Custo Mínimo
← Voltar para DSA Interview Prep