24 de Março
Ladrão de casas
Como modelar a escolha entre roubar ou pular cada casa respeitando a restrição de adjacência.
Programação Dinamica Intermediario 18 min TypeScript
Você e um ladrao planejando roubar casas ao longo de uma rua.
Cada casa tem uma quantidade de dinheiro, dada por nums[i].
Casas adjacentes não podem ser roubadas na mesma noite.
Retorne o valor máximo que você consegue roubar.
Exemplo:
Input: nums = [2,7,9,3,1]
Output: 12
Porque a melhor escolha e 2 + 9 + 1 = 12.
O que perceber antes de codar
- O problema e de maximizar, não de contar.
- A restrição importante e a adjacencia.
- Em cada posição, você escolhe entre carregar a melhor resposta anterior ou incluir a casa atual.
Erros comuns
- fazer escolha gulosa local pela casa maior do momento
- esquecer que pegar a casa atual te empurra para duas casas atras
Passo 1 - Ver por que o guloso local falha
Se você só olha a casa atual, pode pensar:
- pego a maior que estiver na frente
Mas isso quebra fácil.
O problema não e escolher uma casa boa agora.
E escolher uma sequência boa sem vizinhas.
Passo 2 - Escrever a versão inicial recursiva
Para uma casa no indice i, você tem duas opcoes:
- pular
ie ir parai + 1 - roubar
ie ir parai + 2
Isso gera uma recursão natural.
Funciona, mas recalcula muitos indices.
Passo 3 - Montar a recorrencia certa
Olhando da esquerda para a direita, a melhor resposta até a casa i e:
- ou a melhor resposta até
i - 1 - ou a melhor resposta até
i - 2maisnums[i]
Entao:
best(i) = max(best(i - 1), best(i - 2) + nums[i])
Passo 4 - Guardar só os dois estados anteriores
Você não precisa de uma tabela inteira.
Só precisa lembrar:
- a melhor resposta até a casa anterior
- a melhor resposta até duas casas antes
O editor interativo precisa de JavaScript. Voce ainda pode ler o desafio e copiar o codigo inicial abaixo.
Codigo inicial
export function rob(nums: number[]): number {
// sua solução aqui
return 0
}
Ainda não resolveu?
Ver a solução agora pode reduzir o aprendizado. Vale a pena tentar mais um pouco.
Abrir a solucao de referencia
Sem JavaScript, a solucao de referencia aparece inline em vez de um dialogo.
Solução
Complexidade final
Tempo: O(n)
Espaço: O(1)
Solução 1 - Recursao com incluir ou pular antes de memoization
Boa versão inicial para deixar a decisão central bem explicita.
export function rob(nums: number[]): number {
function solve(index: number): number {
if (index >= nums.length) {
return 0
}
return Math.max(solve(index + 1), nums[index] + solve(index + 2))
}
return solve(0)
}
Funciona, mas desperdica trabalho repetindo os mesmos indices.
Solução 2 - Programacao dinamica com estado reduzido em espaco O(1)
Agora a mesma ideia roda de forma linear.
export function rob(nums: number[]): number {
let twoHousesBefore = 0
let oneHouseBefore = 0
for (const value of nums) {
const current = Math.max(oneHouseBefore, twoHousesBefore + value)
twoHousesBefore = oneHouseBefore
oneHouseBefore = current
}
return oneHouseBefore
}
O ponto forte aqui e enxergar a escolha incluir-ou-não como estado.
O que dizer na entrevista
Uma explicação curta e boa seria:
Para cada casa, eu escolho entre pular e manter a melhor resposta anterior, ou roubar e somar com a melhor resposta de duas casas atras. Isso gera a recorrencia
max(best(i - 1), best(i - 2) + nums[i]). Como só dependo dos dois estados anteriores, posso reduzir o espaco paraO(1).
Isso mostra que você:
- viu a decisão certa
- evitou cair em
gulosolocal - conectou a recorrencia a uma otimização de espaco
Quando esse padrão aparece de novo
Esse padrão reaparece quando você precisa:
- escolher entre incluir ou excluir um item
- lidar com restrição de adjacencia
- montar
programacao dinamicalinear com dois estados - transformar uma recursão de decisão em iteração
O ponto não e decorar esse problema.
O ponto e aprender a modelar escolha com dependência curta.