24 de Março
Maior subsequência crescente
Como sair da programacao dinamica em O(n²) e chegar na ideia de menores finais possiveis com ajuda de busca binaria.
Programação Dinamica Intermediario 22 min TypeScript
Dado um array de inteiros nums, retorne o comprimento da maior subsequencia estritamente crescente.
Uma subsequencia pode pular elementos, mas precisa manter a ordem relativa.
Exemplo:
Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
A maior subsequencia crescente pode ser [2,3,7,101].
O que perceber antes de codar
- O problema pede subsequencia, não subarray nem substring.
- A resposta pede só o comprimento, não a lista em si.
- Na otimização, o array de menores finais, muitas vezes chamado de `tails`, ajuda a medir o comprimento, mas nem sempre representa uma subsequencia real completa.
Erros comuns
- tratar o problema como contiguo
- olhar para o array de menores finais e achar que ele sempre e exatamente a resposta final
Passo 1 - Não confundir subsequencia com contiguidade
Aqui você pode pular elementos.
Isso muda bastante a leitura do problema.
Em [10,9,2,5,3,7,101,18], a resposta pode usar:
2- depois
3 - depois
7 - depois
101
Mesmo com outros valores no meio.
Passo 2 - Escrever a versão inicial de DP
A versão inicial forte e:
dp[i]= melhor comprimento terminando emnums[i]
Para calcular dp[i], você olha todo j < i.
Se nums[j] < nums[i], da para estender a subsequencia que terminava em j.
Passo 3 - Perguntar o que realmente ajuda o futuro
Se eu tenho duas subsequencias com o mesmo comprimento, qual delas e melhor guardar?
A que termina com valor menor.
Porque ela deixa mais espaco para crescer depois.
Esse e o insight da versão forte.
Passo 4 - Trocar "melhor comprimento por indice" por "menor final por comprimento"
Um array de menores finais, conhecido em muitas soluções como tails, guarda:
- em cada posição, o menor valor final possível para uma subsequencia daquele tamanho
Quando chega um número novo:
- se ele for maior que todos, aumenta o comprimento
- senao, substitui o primeiro final que e maior ou igual a ele
Essa substituicao e justamente o que a busca binaria encontra.
O editor interativo precisa de JavaScript. Voce ainda pode ler o desafio e copiar o codigo inicial abaixo.
Codigo inicial
export function lengthOfLIS(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 log n)
Espaço: O(n)
Solução 1 - DP O(n²)
Boa versão inicial porque deixa a recorrencia muito clara.
export function lengthOfLIS(nums: number[]): number {
if (nums.length === 0) {
return 0
}
const dp = new Array(nums.length).fill(1)
let best = 1
for (let index = 0; index < nums.length; index += 1) {
for (let previous = 0; previous < index; previous += 1) {
if (nums[previous] < nums[index]) {
dp[index] = Math.max(dp[index], dp[previous] + 1)
}
}
best = Math.max(best, dp[index])
}
return best
}
Funciona bem e ensina a regra.
Mas cada posição olha para todas as anteriores.
Solução 2 - Array de menores finais mais busca binaria
Agora a ideia não e guardar todas as respostas por indice.
E guardar o melhor final possível para cada comprimento.
export function lengthOfLIS(nums: number[]): number {
const tails: number[] = []
for (const num of nums) {
let left = 0
let right = tails.length
while (left < right) {
const middle = Math.floor((left + right) / 2)
if (tails[middle] < num) {
left = middle + 1
} else {
right = middle
}
}
tails[left] = num
}
return tails.length
}
O insight forte aqui e:
- o array de menores finais não e necessariamente a subsequencia final
- ele e a melhor fronteira para continuar crescendo
O que dizer na entrevista
Uma explicação curta e boa seria:
A versão inicial usa DP:
dp[i]e a melhor subsequencia crescente terminando emi, entao eu testo todos os anteriores menores. A versão forte troca isso por um array de menores finais, muitas vezes chamado details, onde cada posição guarda o menor final possível para um certo comprimento. Com busca binaria eu descubro onde o número atual melhora essa fronteira e chego emO(n log n).
Isso mostra que você:
- entende primeiro a versão de DP
- sabe explicar por que o array de menores finais funciona
- não vende busca binaria como truque magico
Quando esse padrão aparece de novo
Esse padrão reaparece quando você precisa:
- pensar em subsequencia e não em contiguidade
- guardar a melhor fronteira para o futuro
- combinar programação dinamica com
busca binaria - otimizar uma resposta O(n²) sem perder a intuição
O ponto não e decorar o nome
tails.
O ponto e aprender a perguntar qual estado deixa mais espaco para crescer depois.