Pular para o conteudo principal

Soma de combinações

Como usar backtracking com reuso controlado para montar combinações únicas em ordem canônica.

Dado um array de inteiros distintos candidates e um inteiro target, retorne todas as combinações unicas em que os numeros escolhidos somam target.

Você pode usar o mesmo número de candidates quantas vezes quiser.

Aqui no SeniorPath:

  • cada combinação deve vir em ordem não decrescente
  • a lista final deve vir na ordem natural da busca em profundidade (DFS) depois de ordenar candidates em ordem crescente

Exemplo:

Input:  candidates = [2, 3, 6, 7], target = 7
Output: [[2, 2, 3], [7]]

O que perceber antes de codar

Erros comuns

Passo 1 - Entender o que significa "combinação unica"

O problema não quer todas as ordens possiveis.

Se [2, 2, 3] já apareceu, [2, 3, 2] e [3, 2, 2] não sao novas respostas.

Isso muda bastante o jeito de montar a busca.

Passo 2 - Escrever a versão inicial mais direta

A versão inicial natural e:

  • tentar cada candidato
  • guardar o caminho atual
  • parar quando a soma bater ou passar do alvo

Funciona.

Mas, se você sempre puder escolher qualquer candidato de novo, acaba montando as mesmas combinações em várias ordens.

Passo 3 - Perguntar como impedir essas ordens duplicadas

A ideia forte e impor uma ordem fixa.

Em vez de deixar o caminho ir e voltar entre candidatos, você decide:

  • ou usa o número atual e continua do mesmo indice
  • ou avanca para candidatos maiores

Assim o caminho nunca volta para tras.

Passo 4 - Transformar isso em backtracking

O estado necessário e:

  • startIndex: de onde o loop pode recomecar
  • remaining: quanto ainda falta para chegar no alvo
  • path: a combinação atual

Se remaining === 0, encontrou uma resposta.

Se remaining < 0, aquele caminho morreu.

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

Codigo inicial

export function combinationSum(candidates: number[], target: number): number[][] {
  // sua solução aqui
  return []
}
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(number of explored paths)

Espaço: O(target / min(candidates))

Solução 1 - Gerar ordens demais e deduplicar depois

Boa versão inicial para enxergar o desperdicio.

export function combinationSum(candidates: number[], target: number): number[][] {
  const unique = new Set<string>()

  function dfs(remaining: number, path: number[]) {
    if (remaining === 0) {
      unique.add([...path].sort((a, b) => a - b).join(","))
      return
    }

    if (remaining < 0) {
      return
    }

    for (const candidate of candidates) {
      path.push(candidate)
      dfs(remaining - candidate, path)
      path.pop()
    }
  }

  dfs(target, [])

  return [...unique].map((entry) => entry.split(",").map(Number))
}

Ela até funciona.

Mas tem um problema feio: ela constroi o mesmo grupo de numeros em várias ordens e só limpa a bagunca no fim.

Solução 2 - Backtracking + busca em profundidade (DFS) com indice para manter ordem fixa

Agora a ideia fica mais disciplinada:

  1. ordene candidates
  2. faca busca em profundidade (DFS) a partir de um startIndex
  3. no loop, só ande para frente
  4. quando escolher candidates[i], recurse com o mesmo i para permitir reuso
export function combinationSum(candidates: number[], target: number): number[][] {
  const sorted = [...candidates].sort((a, b) => a - b)
  const result: number[][] = []
  const path: number[] = []

  function dfs(startIndex: number, remaining: number) {
    if (remaining === 0) {
      result.push([...path])
      return
    }

    for (let index = startIndex; index < sorted.length; index += 1) {
      const candidate = sorted[index]

      if (candidate > remaining) {
        break
      }

      path.push(candidate)
      dfs(index, remaining - candidate)
      path.pop()
    }
  }

  dfs(0, target)
  return result
}

O detalhe mais importante esta aqui:

dfs(index, remaining - candidate)

E index, não index + 1.

Isso permite usar o mesmo número outra vez.

Se o problema proibisse reuso, ai sim seria index + 1.

O que dizer na entrevista

Uma explicação curta e boa seria:

A versão inicial tenta candidatos em qualquer ordem e depois precisa deduplicar respostas iguais. A versão forte ordena a entrada e usa backtracking com startIndex. Assim eu mantenho cada combinação em ordem fixa, evito permutacoes repetidas e ainda consigo reutilizar o mesmo número quando faco a recursão com o mesmo indice.

Isso mostra quatro sinais bons:

  • você entendeu por que a duplicação aparece
  • você controlou a ordem em vez de limpar depois
  • você soube explicar o reuso
  • você conectou a regra do problema ao parametro da recursão

Quando esse padrão aparece de novo

A mesma ideia volta quando o problema pede:

  • enumerar combinações
  • escolher repetindo itens
  • evitar respostas duplicadas por ordem diferente
  • podar cedo quando o restante já não fecha

Aqui, o ganho real não e só “usar recursão”.

E perceber que uma ordem fixa simplifica o problema inteiro.

Proximo desafio Adicionar e buscar palavras com árvore de prefixos (`trie`) Desafio anterior Dicionário alienígena

Desafios relacionados

Artigos relacionados