Pular para o conteudo principal

Busca de palavra

Como explorar caminhos em uma grade, marcar a escolha atual e desfazer a decisão ao voltar da busca.

Dada uma matriz board de letras e uma string word, retorne true se a palavra puder ser formada por letras adjacentes na grade.

As letras podem ser conectadas horizontalmente ou verticalmente.

A mesma celula não pode ser usada mais de uma vez no mesmo caminho.

Exemplo:

Input:
  board = [
    ["A","B","C","E"],
    ["S","F","C","S"],
    ["A","D","E","E"]
  ]
  word = "ABCCED"

Output: true

O que perceber antes de codar

Erros comuns

Passo 1 - Entender o que precisa ser controlado

O problema não e "andar na grade" apenas.

O problema e:

  • andar na grade seguindo a proxima letra certa
  • sem reutilizar a mesma celula no mesmo caminho

Isso já tem cara de busca em profundidade (DFS) com backtracking.

Passo 2 - Escrever a versão inicial mais direta

A versão inicial natural e:

Funciona e deixa o raciocínio bem visível.

Passo 3 - Perguntar onde esta o estado de verdade

O "visitado" não e global.

Ele só vale para a tentativa atual.

Se um caminho falhar, as outras tentativas devem poder reutilizar aquelas celulas.

Por isso backtracking precisa desfazer a marca ao voltar.

Passo 4 - Simplificar a estrutura auxiliar

Em vez de manter um conjunto de visitados separado, da para marcar a própria celula temporariamente e depois restaurar.

O efeito logico e o mesmo:

  • marca
  • explora
  • desfaz

Só que com menos custo por chamada.

O editor interativo precisa de JavaScript. Voce ainda pode ler o desafio e copiar o codigo inicial abaixo.

Codigo inicial

export function exist(board: string[][], word: string): boolean {
  // sua solução aqui
  return false
}
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(rows * cols * 4^word.length)

Espaço: O(word.length)

Solução 1 - Busca em profundidade (DFS) com conjunto hash de visitados

Boa versão inicial porque deixa o estado do caminho explícito.

export function exist(board: string[][], word: string): boolean {
  const rows = board.length
  const cols = board[0].length

  function dfs(row: number, col: number, index: number, visited: Set<string>): boolean {
    if (index === word.length) {
      return true
    }

    const outOfBounds = row < 0 || row >= rows || col < 0 || col >= cols
    if (outOfBounds || board[row][col] !== word[index]) {
      return false
    }

    const key = `${row},${col}`
    if (visited.has(key)) {
      return false
    }

    visited.add(key)

    const found =
      dfs(row + 1, col, index + 1, visited) ||
      dfs(row - 1, col, index + 1, visited) ||
      dfs(row, col + 1, index + 1, visited) ||
      dfs(row, col - 1, index + 1, visited)

    visited.delete(key)
    return found
  }

  for (let row = 0; row < rows; row += 1) {
    for (let col = 0; col < cols; col += 1) {
      if (dfs(row, col, 0, new Set())) {
        return true
      }
    }
  }

  return false
}

Funciona, mas o conjunto de visitados adiciona trabalho extra em toda chamada.

Solução 2 - Backtracking marcando a grade no lugar

Agora o estado continua local ao caminho, mas sem estrutura auxiliar de coordenadas.

export function exist(board: string[][], word: string): boolean {
  const rows = board.length
  const cols = board[0].length

  function dfs(row: number, col: number, index: number): boolean {
    if (index === word.length) {
      return true
    }

    const outOfBounds = row < 0 || row >= rows || col < 0 || col >= cols
    if (outOfBounds || board[row][col] !== word[index]) {
      return false
    }

    const current = board[row][col]
    board[row][col] = "#"

    const found =
      dfs(row + 1, col, index + 1) ||
      dfs(row - 1, col, index + 1) ||
      dfs(row, col + 1, index + 1) ||
      dfs(row, col - 1, index + 1)

    board[row][col] = current
    return found
  }

  for (let row = 0; row < rows; row += 1) {
    for (let col = 0; col < cols; col += 1) {
      if (dfs(row, col, 0)) {
        return true
      }
    }
  }

  return false
}

O insight forte aqui não e só “usar recursão”. E controlar o estado certo no lugar certo com busca em profundidade (DFS) e backtracking.

O que dizer na entrevista

Uma explicação curta e boa seria:

Eu testo cada celula como ponto de partida e faco busca em profundidade (DFS) para casar a palavra letra por letra. O cuidado principal e não reutilizar a mesma celula no caminho atual. Por isso marco a escolha antes de explorar os vizinhos e desfaco ao voltar. Essa parte de desfazer e o próprio backtracking.

Isso mostra que você:

  • entendeu o papel do estado local
  • sabe explicar por que precisa desfazer
  • não trata backtracking como magia

Quando esse padrão aparece de novo

Esse padrão reaparece quando você precisa:

  • explorar muitos caminhos possiveis
  • manter restrições locais por tentativa
  • marcar e restaurar estado em busca em profundidade (DFS)
  • resolver grade, combinações ou busca com poda

O ponto não e decorar esse problema.

O ponto e aprender que backtracking e explorar com disciplina, não tentar tudo no escuro.

Proximo desafio Cache LRU Desafio anterior Busca em array rotacionado

Desafios relacionados

Artigos relacionados