O problema "House Robber" é um clássico de programação dinâmica que explora a tomada de decisão otimizada sob restrições de adjacência. Abordaremos três variações fundamentais: a sequência linear de casas, a disposição circular e a estrutura organizada em árvore binária.
1. House Robber: Sequência Linear
Neste cenário básico, temos um array de valores representando o dinheiro em cada casa. A única restrição é que não podemos asslatar duas casas adjacentes. Para resolver isso, definimos dp[i] como o valor máximo acumulado até a casa i.
A lógica de transição baseia-se em duas escolhas para a casa atual:
- Não assaltar a casa atual: O valor acumulado será o mesmo da casa anterior (
dp[i-1]). - Assaltar a casa atual: O valor será a soma do valor da casa atual com o acumulado até duas casas atrás (
dp[i-2] + nums[i]).
class Solution {
public int rob(int[] casas) {
if (casas == null || casas.length == 0) return 0;
int totalCasas = casas.length;
if (totalCasas == 1) return casas[0];
int[] lucroMaximo = new int[totalCasas];
lucroMaximo[0] = casas[0];
lucroMaximo[1] = Math.max(casas[0], casas[1]);
for (int i = 2; i < totalCasas; i++) {
// Decisão: pular a casa atual ou somar a casa atual com o lucro de i-2
lucroMaximo[i] = Math.max(lucroMaximo[i - 1], lucroMaximo[i - 2] + casas[i]);
}
return lucroMaximo[totalCasas - 1];
}
}
2. House Robber II: Disposição Circular
Nesta variação, as casas estão dispostas em um círculo, o que significa que a primeira e a última casa são vizinhas. Para resolver essa depandência circular, quebramos o problema em dois subproblemas lineares:
- Considerar apenas as casas da primeira até a penúltima (índice
0an-2). - Considerar apenas as casas da segunda até a última (índice
1an-1).
O resultado final será o valor máximo entre essas duas possibilidades.
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0];
if (n == 0) return 0;
return Math.max(
calcularMaximo(nums, 0, n - 2),
calcularMaximo(nums, 1, n - 1)
);
}
private int calcularMaximo(int[] lista, int inicio, int fim) {
if (inicio == fim) return lista[inicio];
int anterior = 0;
int atual = 0;
for (int i = inicio; i <= fim; i++) {
int temporario = atual;
atual = Math.max(atual, anterior + lista[i]);
anterior = temporario;
}
return atual;
}
}
3. House Robber III: Estrutura em Árvore
Nesta versão, as casas estão coenctadas como nós de uma árvore binária. Se um nó pai for assaltado, os filhos diretos não podem ser. A abordagem mais eficiente aqui é utilizar uma travessia de pós-ordem (bottom-up), onde cada nó retorna um array de dois elementos:
indice 0: Valor máximo se não assaltarmos o nó atual.indice 1: Valor máximo se assaltarmos o nó atual.
class Solution {
public int rob(TreeNode raiz) {
int[] resultado = explorar(raiz);
return Math.max(resultado[0], resultado[1]);
}
private int[] explorar(TreeNode no) {
// [0] não rouba, [1] rouba
if (no == null) return new int[]{0, 0};
int[] esquerda = explorar(no.left);
int[] direita = explorar(no.right);
// Se roubarmos o nó atual, não podemos roubar os filhos
int roubarAtual = no.val + esquerda[0] + direita[0];
// Se não roubarmos o nó atual, podemos escolher o máximo de cada filho
int pularAtual = Math.max(esquerda[0], esquerda[1]) + Math.max(direita[0], direita[1]);
return new int[]{pularAtual, roubarAtual};
}
}
A complexidade de tempo para as três abordagens é O(n), onde n representa o número de casas ou nós, garantindo uma execução eficiente mesmo para grandes volumes de dados.