Home Posts Tags Post Search Tag Search

Post 52

Hacker Rank 25 - Password Hacker

Published on: 2025-08-03 Tags: elixir, Side Project, Hacker Rank
So for this one we are given a set of passwords and a passphrase. For this fake horribly designed site you are to give a set of passwords that will work, the site will allow for any passphrase at is any combination of the passphrases. So if the passphrases are ~w(cat dog chicken)
catdog or dogchicken or chickenchicken all would work along with many other text strings.

So for this we need to do a few things in order to find out if a phrase and set of passphrases will work for the website.

A trie is a tree where each level represents one letter of a word. It allows fast checking of whether a word or prefix exists. In our solution, we use it to walk through the loginAttempt string efficiently, checking if any prefix matches a password. If a path ends at a word, we note it and continue searching from there.

First inorder to avoid a longer set of queries we will make a trie that is short for "retrieval" tree that will create a tree for each letter of the passphrases, so ~w(cat dog) would look like
C
|- A
|   |- T (:end)
|- D
    |- O
        |- G (:end)

This way we can check the first letter and then other letters as we get further down the tree. and avoid longer checks for the entire word.

  # Build a trie from the list of passwords
  defp build_trie(words) do
    Enum.reduce(words, %{}, fn word, trie ->
      insert_trie(String.to_charlist(word), trie)
    end)
  end

  defp insert_trie([], trie), do: Map.put(trie, :end, true)

  defp insert_trie([h | t], trie) do
    Map.update(trie, h, insert_trie(t, %{}), fn subtrie ->
      insert_trie(t, subtrie)
    end)
  end

Next is the BFS search that will keep an updated queue of the position of in the phrase and the current word list.

At each letter of the phrase we will:
If we’ve reached the end of the phrase, we return the matched word list

If the current position was already visited, skip it

Otherwise, we:

    Use the trie to find all valid words starting at that position

    Add new states to the queue for each of those

If the queue empties without a solution, return "WRONG PASSWORD"
  # BFS search over the loginAttempt using the trie
  defp bfs([phrase], trie) do
    chars = String.to_charlist(phrase)
    len = length(chars)

    # Queue holds {position_in_phrase, current_word_list}
    queue = :queue.from_list([{0, []}])
    visited = MapSet.new()

    bfs_loop(chars, trie, len, queue, visited)
  end

  defp bfs_loop(chars, trie, len, queue, visited) do
    if :queue.is_empty(queue) do
      :error
    else
      {{:value, {pos, words}}, queue} = :queue.out(queue)

      if pos == len do
        {:ok, words}
      else
        if MapSet.member?(visited, pos) do
          bfs_loop(chars, trie, len, queue, visited)
        else
          visited = MapSet.put(visited, pos)
          new_states = next_states(chars, trie, pos, len)

          queue =
            Enum.reduce(new_states, queue, fn {next_pos, word}, acc_q ->
              :queue.in({next_pos, words ++ [word]}, acc_q)
            end)

          bfs_loop(chars, trie, len, queue, visited)
        end
      end
    end
  end

Lastly we need to check the current set of letters of the passphrase and see if we can find a word within the trie:
  # From position `pos`, find all valid words in trie
  defp next_states(chars, trie, pos, len) do
    do_next_states(Enum.drop(chars, pos), trie, pos, [], [], "")
  end

  defp do_next_states([], _trie, _pos, _acc, results, _current_word), do: results

  defp do_next_states([h | t], trie, pos, acc, results, current_word) do
    case Map.get(trie, h) do
      nil ->
        results

      subtrie ->
        new_word = current_word <> <<h>>

        results =
          if Map.has_key?(subtrie, :end) do
            results ++ [{pos + length(acc) + 1, new_word}]
          else
            results
          end

        do_next_states(t, subtrie, pos, acc ++ [h], results, new_word)
    end
  end

We try to walk down the trie from the current phrase position. If the first letter matches a trie key, we add it to the current word and check if we've reached the end of a valid word. If so, we save this word and its end position. Then, we continue checking the next letters to find longer words. This way, we find all possible valid words starting from that position to expand our BFS search.