Pular para o conteudo principal

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.

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

Erros comuns

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 em nums[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
}
solution.ts
Travado? Dicas disponiveis.

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 em i, entao eu testo todos os anteriores menores. A versão forte troca isso por um array de menores finais, muitas vezes chamado de tails, 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 em O(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:

O ponto não e decorar o nome tails.

O ponto e aprender a perguntar qual estado deixa mais espaco para crescer depois.

Proximo desafio Clonar grafo Desafio anterior Mediana em fluxo de dados

Desafios relacionados

Artigos relacionados