Advent of Code 2024 - Day 5

I think you’ll find the data was deliberately designed that way :slight_smile:

Livebook. solution using maps

My solution is very similar to @seeplusplus

Didn’t realize I could use the rules to sort the pages, until I got a bit stuck in part 2 and decided to rethink the whole thing again

I used a sort function to Enum.sort based on the rules. This created a list of update entries each with {original_order, sorted_order). From there it was a simple sort for; part1 == and part 2 !=, then summing the middle value in each list.
Here’s the code for part 1 and part 2:

defmodule Aoc.Day05 do
  def run() do
    IO.puts("Part 1 : result: #{part1()}")
    IO.puts("Part 2 : result: #{part2()}")
  end

  def part1() do
    get_ordered_lists()
    |> Enum.filter(fn {original, ordered} -> original == ordered end)
    |> Enum.map(fn {l, _o} -> Enum.at(l, trunc(Enum.count(l) / 2)) end)
    |> Enum.sum()
  end

  def part2() do
    get_ordered_lists()
    |> Enum.filter(fn {original, ordered} -> original != ordered end)
    |> Enum.map(fn {_l, o} -> Enum.at(o, trunc(Enum.count(o) / 2)) end)
    |> Enum.sum()
  end

  def get_ordered_lists() do
    {rules, updates} = get_input()

    updates
    |> Enum.map(fn page_list -> {page_list, check_page_order(page_list, rules)} end)
  end

  def check_page_order(page_list, rules) do
    page_list
    |> Enum.sort(fn n1, n2 ->
      rule = Map.get(rules, n1, [n1])
      not Enum.member?(rule, n2)
    end)
    |> Enum.reverse()
  end

  # parsed_rules is map of page => [pages list must be after]
  # parsed_update is list of updates, each update is a list of original order.

  def get_input() do
    [rules, updates] =
      String.split(Atom.to_string(__MODULE__), ".")
      |> List.last()
      |> String.downcase()
      |> then(fn day -> "lib/#{day}/input.txt" end)
      |> File.read!()
      |> String.split("\n\n")

    parsed_rules =
      rules
      |> String.split("\n")
      |> Enum.map(fn rule -> String.split(rule, "|") end)
      |> Enum.map(fn [f, l] -> {String.to_integer(f), String.to_integer(l)} end)
      |> Enum.group_by(fn {b, _a} -> b end)
      |> Enum.map(fn {p, list} -> {p, list |> Enum.map(fn {_p, i} -> i end)} end)
      |> Enum.into(%{})

    parsed_updates =
      updates
      |> String.split("\n")
      |> Enum.map(fn nums -> String.split(nums, ",") end)
      |> Enum.map(fn list -> list |> Enum.map(fn strint -> String.to_integer(strint) end) end)

    {parsed_rules, parsed_updates}
  end
end

Many problems in AoC are NOT the general case. Given input is built in a specific way to ensure there is a solution. Those “meta” bias can sometimes be used to find shortcuts.

Interesting, I did not know that. It’s my first time trying AoC.
Another possible condition that would cause confusion is if the rule-set is not transitive.

1|2
2|3
3|1

1,2,3

No way to sort that to fit all the rules.

Another one would be if the rules form islands.

1|2
3|4

4,3,2,1

There are many ways to sort that like 1,2,3,4 and 1,3,2,4

Speaking of which, if the data has an even number of elements then there is not middle element.

I spent a lot of time fretting over these possibilities. I guess the best way to do this would be to check for them as assumptions before trying to handle them somehow.

Hey!

First, I have grouped the rules by their second number. For each number of a page, I get the relevant rules from this map. I then check whether the rest of my list has any numbers in common with the list of first numbers of the relevant rules. If so, it means that the page violates a rule.

Source Code:

$ ./day05.exs
Part1. Sum: 4790
Part 1 + I/O done in 5729 µs
Part2. Sum: 6319
Part 2 done in 1605 µs

File day05.exs:

#!/usr/bin/env elixir

# AoC 2024. day 5.

defmodule Part1 do
  @spec order_ok?([integer()], MapSet.t()) :: boolean()
  def order_ok?(numbers, rules)
  def order_ok?([], _set), do: true
  def order_ok?([number | numbers], rules) do
    # If for all the following `numbers` x after `number`, we cannot find a rule x|number
    # Then `number` is at the right position
    Enum.all?(numbers, fn x -> !MapSet.member?(rules, {x, number}) end)
    && order_ok?(numbers, rules)
  end
end

# part 1. accumulates the data and at the same time solve part 1.
# it could be decoupled
start = System.monotonic_time(:microsecond)
{rules, bad_updates, sum} = File.stream!("../day05.txt")
  |> Enum.reduce({MapSet.new(), [], 0}, fn line, {rules, updates, sum} ->
    case Regex.run(~r/^(?:(\d+)\|(\d+))|\d+(?:,\d+)+/, line) do
      [_, fst, snd] -> # first, accumulate the rules in the MapSet
        {MapSet.put(rules, {String.to_integer(fst), String.to_integer(snd)}), updates, sum}
      nil -> {rules, updates, sum}
      [update] -> # then, check all the updates
        update = update |> String.split(",") |> Enum.map(&String.to_integer/1)
        if Part1.order_ok?(update, rules) do
         # take the middle number and add it to sum
         {rules, updates, sum + :lists.nth(div(length(update), 2) + 1, update)}
        else
         {rules, [update | updates], sum}
        end
    end
  end)
IO.puts("Part1. Sum: #{sum}")
elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Part 1 + I/O done in #{elapsed} µs"

# part 2
start = System.monotonic_time(:microsecond)
Enum.reduce(bad_updates, 0, fn update, sum ->
  update = Enum.sort(update, fn n1, n2 -> MapSet.member?(rules, {n1, n2}) end)
  sum + :lists.nth(div(length(update), 2) + 1, update)
end)
|> IO.inspect(label: "Part2. Sum")
elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Part 2 done in #{elapsed} µs"

I used local topological sort over the local graph formed by pair of nodes in each updated runs under 30ms so seems fair enough

Why not
not is_in_rules
:cowboy_hat_face:

@king-11 Figured I’d rather bring some discussion here than Reddit.

Looks like it took about 36ms for the swap method in part 2. (Using Livebook, so bear with my formatting).

Edit: Got it down to around 29ms now.

Part 1

[rules, updates] = Kino.FS.file_path("day5_input.txt")
  |> File.read!()
  |> String.trim()
  |> String.split("\n\n")
  |> Enum.map(fn section ->
    String.trim(section)
    |> String.split("\n")
  end)

rules = Enum.map(rules, fn pair ->
  String.split(pair, "|") 
  |> Enum.map(&String.to_integer/1)
  |> List.to_tuple()
end)

updates = Enum.map(updates, fn group ->
  String.split(group, ",")
  |> Enum.map(&String.to_integer/1)
end)
defmodule UpdateValidator do
  @doc """
  Check if all {x before y} rules pass for the given update.
  """
  def is_valid_update?(update, rules) do
    not Enum.any?(rules, fn {x, y} -> not is_before?(x, y, update) end)
  end

  @doc """
  Check if x occurs before y in the given list
  """
  def is_before?(x, y, list = [first | rest]) do
    if x in list and y in list do
      if y == first, do: false, else: is_before?(x, y, rest)
    else
      true
    end
  end
end

sum_middle_numbers = fn updates -> updates
  |> Enum.map(& {length(&1) |> Integer.floor_div(2), &1})  # Get index of middle num.
  |> Enum.map(fn {middle_index, update} ->
      Enum.at(update, middle_index)
    end)  # Extract value at index
  |> Enum.sum()
end

valid_updates = Enum.filter(updates, fn update ->
  UpdateValidator.is_valid_update?(update, rules)
end)

result = sum_middle_numbers.(valid_updates)
----

Part 2

defmodule UpdateFixer do
  import UpdateValidator

  @doc """
  Recursively applies fixes until no more
  modifications are made (some fixes break other rules).
  """
  def fix_update(update, rules) do
    recursively_fix_update({true, update}, rules)
  end

  # Simply facilitates recursive calling.
  defp recursively_fix_update({true, update}, rules) do
    result = do_fix_update(update, rules)
    recursively_fix_update(result, rules)
  end
  defp recursively_fix_update({false, update}, _), do: update

  # Applies fixes for all rules by swapping values.
  defp do_fix_update(update, rules, modified? \\ false)
  defp do_fix_update(update, [], modified?), do: {modified?, update}
  defp do_fix_update(update, [{x, y} | rules], modified?) do
    {update_, modified?} = if not is_before?(x, y, update) do
      # Swap values if this rule failed validation.
      update_ = update
        |> Enum.map(& {&1})  # Pack into tuple to make compat. with keyreplace
        |> List.keyreplace(x, 0, {y})
        |> List.keyreplace(y, 0, {x})
        |> Enum.map(fn {val} -> val end)
      {update_, true}
    else
      # Otherwise, pass update as-is.
      {update, modified?}
    end
    # Check next rule.
    do_fix_update(update_, rules, modified?)
  end
end

start = System.monotonic_time(:microsecond)

result = updates -- valid_updates  # Invalid updates
  |> Enum.map(fn update -> UpdateFixer.fix_update(update, rules) end)
  |> sum_middle_numbers.()

IO.puts("Part 2 result: #{result}")

elapsed = System.monotonic_time(:microsecond) - start
IO.puts "Part 2 finished in #{elapsed / 1000}ms"
Part 2 result: ----
Part 2 finished in 29.442ms

The first task of today was much easier than yesterdays first task. Took a while, but i made it:

for part 2 im not really sure how to debug why it doesnt pass on the real input. It passes on the test input. Not sure what i can do to debug. Any help? Or tips to fix my code :wink: maybe it has bugs …

i mean good thing santa doesnt get angry even if i complete the parts next day right :smiley: :rofl:

I don’t have much advice to offer on debugging, but I can offer a hint: after you run fix_invalid_update() on an update, check if it’s valid again. I think you’ll find that some still aren’t :wink:

1 Like

Fun problem!

defmodule AOC.Y2024.Day5 do
  @moduledoc false

  use AOC.Solution

  @impl true
  def load_data() do
    Data.load_day(2024, 5, "\n\n")
    |> then(fn [rules, updates] ->
      {
        rules
        |> String.split("\n")
        |> Enum.map(&(&1 |> String.split("|") |> List.to_tuple()))
        |> MapSet.new(),
        updates
        |> String.split("\n")
        |> Enum.map(&String.split(&1, ","))
      }
    end)
  end

  @impl true
  def part_one({rules, updates}) do
    updates
    |> Enum.filter(fn update -> right_order?(rules, update) end)
    |> General.map_sum(fn update ->
      update |> Enum.at(div(length(update), 2)) |> String.to_integer()
    end)
  end

  @impl true
  def part_two({rules, updates}) do
    updates
    |> Enum.reject(fn update -> right_order?(rules, update) end)
    |> Enum.map(fn update ->
      Enum.sort(update, fn a, b -> not MapSet.member?(rules, {b, a}) end)
    end)
    |> General.map_sum(fn update ->
      update |> Enum.at(div(length(update), 2)) |> String.to_integer()
    end)
  end

  defp right_order?(rules, update) do
    update
    |> Enum.with_index(1)
    |> Enum.map(fn {v, i} -> update |> Enum.slice(i..-1//1) |> Enum.map(fn k -> {v, k} end) end)
    |> Enum.all?(fn order ->
      Enum.all?(order, fn {a, b} -> !MapSet.member?(rules, {b, a}) end)
    end)
  end
end
1 Like

You’re right … Thank you.
I think I missed this as I was refactoring the code after part2 was complete. Here’s the revised code:

def check_page_order(page_list, rules) do
page_list

Enum.sort(fn n1, n2 →
rule = Map.get(rules, n1, [n1])
not Enum.member?(rule, n2)
end)
Enum.reverse()
end

I’ve used digraph and digraph_utils.
Doing so may be overkill but digraph_utils really helped quickly solving part 2.

defmodule D5 do
  def p1(file) do
    {digraph, updates} = parse(file)

    updates
    |> Enum.filter(fn update -> right_order?(update, digraph) end)
    |> Enum.map(fn update -> Enum.at(update, div(Enum.count(update), 2)) end)
    |> Enum.sum()
  end

  defp parse(file) do
    [section1, section2] =
      file
      |> File.read!()
      |> String.split("\n\n")

    digraph = :digraph.new()

    section1
    |> String.split("\n")
    |> Enum.map(fn rule -> rule |> String.split("|") |> Enum.map(&String.to_integer/1) end)
    |> Enum.each(fn [n1, n2] ->
      :digraph.add_vertex(digraph, n1)
      :digraph.add_vertex(digraph, n2)
      :digraph.add_edge(digraph, n1, n2)
    end)

    updates =
      section2
      |> String.split(["\n"])
      |> Enum.map(fn u -> String.split(u, ",") |> Enum.map(&String.to_integer/1) end)

    {digraph, updates}
  end

  defp right_order?([n1, n2 | rest], digraph) do
    case :digraph.get_short_path(digraph, n1, n2) do
      [^n1, ^n2] ->
        right_order?([n2 | rest], digraph)

      _ ->
        false
    end
  end

  defp right_order?(_, _) do
    true
  end

  def p2(file) do
    {digraph, updates} = parse(file)

    updates
    |> Enum.filter(fn update -> not right_order?(update, digraph) end)
    |> Enum.map(fn update ->
      digraph
      |> :digraph_utils.subgraph(update)
      |> :digraph_utils.topsort()
    end)
    |> Enum.map(fn update -> Enum.at(update, div(Enum.count(update), 2)) end)
    |> Enum.sum()
  end
end

Here’s how I did this:

defmodule AdventOfCode.Y2024.Day05 do
  alias AdventOfCode.Helpers.{InputReader, Transformers}

  def input, do: InputReader.read_from_file(2024, 5)

  def run(input \\ input()) do
    input = parse(input)

    {run_1(input), run_2(input)}
  end

  defp run_1(input) do
    input
    |> Enum.filter(fn {a, b} -> a == b end)
    |> Enum.map(fn {a, _} -> a |> Enum.at(div(length(a), 2)) end)
    |> Enum.sum()
  end

  defp run_2(input) do
    input
    |> Enum.filter(fn {a, b} -> a != b end)
    |> Enum.map(fn {_, b} -> b |> Enum.at(div(length(b), 2)) end)
    |> Enum.sum()
  end

  def parse(data \\ input()) do
    [deps, updates] = Transformers.sections(data)
    given_sorted_pair({parse_deps(deps), parse_updates(updates)})
  end

  defp parse_deps(deps) do
    for line <- Transformers.lines(deps),
        into: MapSet.new(),
        do: String.split(line, "|") |> Enum.map(&String.to_integer/1) |> List.to_tuple()
  end

  defp parse_updates(updates) do
    for line <- Transformers.lines(updates) do
      for update <- String.split(line, ","), do: String.to_integer(update)
    end
  end

  defp given_sorted_pair({deps, updates}) do
    for update <- updates do
      {update, Enum.sort(update, &({&1, &2} in deps))}
    end
  end
end

Nice. I was thinking of giving digraph a try, but confused myself into thinking it would sort on the values, not the edges. Nice to see it in action.

I need to not code at 12am on workdays. :sweat_smile: This took me longer than I wanted, but I was out today. I’m just glad to have it done before the next one.

Part 1:

#!/usr/bin/env elixir

defmodule Day5.Part1 do
  defp parse(str) do
    [sorting_rules, [""], updates] =
      str
      |> String.split("\n")
      |> Enum.chunk_by(fn x -> x == "" end)

    sorting_rules =
      sorting_rules
      |> Enum.reduce(
        %{},
        fn str, state ->
          [l, r] =
            str
            |> String.split("|")

          Map.update(state, :"#{l}", [r], fn tail -> [r|tail] end)
        end
      )

    updates =
      updates
      |> Enum.map(fn str -> String.split(str, ",") end)

    {sorting_rules, updates}
  end

  def sorted?([_], _sorting_rules), do: true

  def sorted?([a | tail], must_suffix) do
    tail
    |> Enum.all?(fn t ->
      must_suffix_t = must_suffix[:"#{t}"]
      is_nil(must_suffix_t) or a not in must_suffix_t
    end) and sorted?(tail, must_suffix)
  end

  def solve() do
    {sorting_rules, updates} =
      File.read!("05/input.txt")
      |> parse()

    updates
    |> Enum.filter(
      fn update ->
        sorted?(update, sorting_rules)
      end
    )
    |> Enum.reduce(
      0,
      fn update, acc ->
        middle_num =
          update
          |> Enum.at(
            ((update
              |> Enum.count()) / 2)
            |> Float.floor()
            |> trunc()
          )
          |> String.to_integer()

        acc + middle_num
      end
    )
    |> IO.puts()
  end
end

Day5.Part1.solve()

Part 2:

#!/usr/bin/env elixir

defmodule Day5.Part2 do
  defp parse(str) do
    [sorting_rules, [""], updates] =
      str
      |> String.split("\n")
      |> Enum.chunk_by(fn x -> x == "" end)

    sorting_rules =
      sorting_rules
      |> Enum.reduce(
        %{},
        fn str, state ->
          [l, r] =
            str
            |> String.split("|")

          Map.update(state, :"#{l}", [r], fn tail -> [r|tail] end)
        end
      )

    updates =
      updates
      |> Enum.map(fn str -> String.split(str, ",") end)

    {sorting_rules, updates}
  end

  def sorted?([_], _sorting_rules), do: true

  def sorted?([a | tail], must_suffix) do
    tail
    |> Enum.all?(fn t ->
      must_suffix_t = must_suffix[:"#{t}"]
      is_nil(must_suffix_t) or a not in must_suffix_t
    end) and sorted?(tail, must_suffix)
  end

  defp place(x, acc, rules) do
    index = acc
    |> Enum.map(
      fn item ->
        if not is_nil(rules[:"#{item}"]) do
          x in rules[:"#{item}"]
        else
          false
        end
      end
    )
    |> Enum.find_index(fn b -> b == false end) || acc |> Enum.count()


    List.insert_at(acc, index, x)
  end

  defp sort(list, rules) do
    list
    |> Enum.reduce(
      [],
      fn x, acc ->
        place(x, acc, rules)
      end
    )
  end

  def solve() do
    {sorting_rules, updates} =
      File.read!("05/input.txt")
      |> parse()

    updates
    |> Enum.filter(
      fn update ->
        not sorted?(update, sorting_rules)
      end
    )
    |> Enum.map(
      fn update ->
        update
        |> sort(sorting_rules)
      end
    )
    |> Enum.reduce(
      0,
      fn update, acc ->
        middle_num =
          update
          |> Enum.at(
            ((update
              |> Enum.count()) / 2)
            |> Float.floor()
            |> trunc()
          )
          |> String.to_integer()

        acc + middle_num
      end
    )
    |> IO.puts()
  end
end

Day5.Part2.solve()

is this correct algorithm for part2?

invalid: [[47, 89, 77, 74, 61, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]]
current: [47, 89, 77, 74, 61, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 89 at index 1 with 77 at index 2"
current: [47, 77, 89, 74, 61, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 47 at index 0 with 74 at index 3"
current: [74, 77, 89, 47, 61, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 89 at index 2 with 61 at index 4"
current: [74, 77, 61, 47, 89, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 77 at index 1 with 61 at index 2"
current: [74, 61, 77, 47, 89, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 89 at index 4 with 53 at index 5"
current: [74, 61, 77, 47, 53, 89, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 47 at index 3 with 53 at index 4"
current: [74, 61, 77, 53, 47, 89, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 77 at index 2 with 53 at index 3"
current: [74, 61, 53, 77, 47, 89, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 61 at index 1 with 53 at index 2"
current: [74, 53, 61, 77, 47, 89, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 74 at index 0 with 53 at index 1"
current: [53, 74, 61, 77, 47, 89, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 89 at index 5 with 82 at index 6"
current: [53, 74, 61, 77, 47, 82, 89, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 61 at index 2 with 82 at index 5"
current: [53, 74, 82, 77, 47, 61, 89, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 74 at index 1 with 82 at index 2"
current: [53, 82, 74, 77, 47, 61, 89, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 61 at index 5 with 38 at index 7"
current: [53, 82, 74, 77, 47, 38, 89, 61, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 47 at index 4 with 38 at index 5"
current: [53, 82, 74, 77, 38, 47, 89, 61, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 77 at index 3 with 38 at index 4"
current: [53, 82, 74, 38, 77, 47, 89, 61, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 82 at index 1 with 38 at index 3"
current: [53, 38, 74, 82, 77, 47, 89, 61, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 74 at index 2 with 73 at index 9"
current: [53, 38, 73, 82, 77, 47, 89, 61, 45, 74, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 45 at index 8 with 15 at index 10"
current: [53, 38, 73, 82, 77, 47, 89, 61, 15, 74, 45, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 89 at index 6 with 15 at index 8"
current: [53, 38, 73, 82, 77, 47, 15, 61, 89, 74, 45, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 77 at index 4 with 15 at index 6"
current: [53, 38, 73, 82, 15, 47, 77, 61, 89, 74, 45, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 45 at index 10 with 76 at index 11"
current: [53, 38, 73, 82, 15, 47, 77, 61, 89, 74, 76, 45, 41, 24, 56, 55, 67, 68, 95]
"replace 82 at index 3 with 76 at index 10"
current: [53, 38, 73, 76, 15, 47, 77, 61, 89, 74, 82, 45, 41, 24, 56, 55, 67, 68, 95]
"replace 38 at index 1 with 76 at index 3"
current: [53, 76, 73, 38, 15, 47, 77, 61, 89, 74, 82, 45, 41, 24, 56, 55, 67, 68, 95]
"replace 47 at index 5 with 41 at index 12"
current: [53, 76, 73, 38, 15, 41, 77, 61, 89, 74, 82, 45, 47, 24, 56, 55, 67, 68, 95]
"replace 15 at index 4 with 41 at index 5"
current: [53, 76, 73, 38, 41, 15, 77, 61, 89, 74, 82, 45, 47, 24, 56, 55, 67, 68, 95]
"replace 15 at index 5 with 24 at index 13"
current: [53, 76, 73, 38, 41, 24, 77, 61, 89, 74, 82, 45, 47, 15, 56, 55, 67, 68, 95]
"replace 15 at index 13 with 55 at index 15"
current: [53, 76, 73, 38, 41, 24, 77, 61, 89, 74, 82, 45, 47, 55, 56, 15, 67, 68, 95]
"replace 89 at index 8 with 55 at index 13"
current: [53, 76, 73, 38, 41, 24, 77, 61, 55, 74, 82, 45, 47, 89, 56, 15, 67, 68, 95]
"replace 77 at index 6 with 55 at index 8"
current: [53, 76, 73, 38, 41, 24, 55, 61, 77, 74, 82, 45, 47, 89, 56, 15, 67, 68, 95]
"replace 24 at index 5 with 55 at index 6"
current: [53, 76, 73, 38, 41, 55, 24, 61, 77, 74, 82, 45, 47, 89, 56, 15, 67, 68, 95]
"replace 73 at index 2 with 67 at index 16"
current: [53, 76, 67, 38, 41, 55, 24, 61, 77, 74, 82, 45, 47, 89, 56, 15, 73, 68, 95]
"replace 45 at index 11 with 68 at index 17"
current: [53, 76, 67, 38, 41, 55, 24, 61, 77, 74, 82, 68, 47, 89, 56, 15, 73, 45, 95]
"replace 24 at index 6 with 68 at index 11"
current: [53, 76, 67, 38, 41, 55, 68, 61, 77, 74, 82, 24, 47, 89, 56, 15, 73, 45, 95]
"replace 55 at index 5 with 68 at index 6"
current: [53, 76, 67, 38, 41, 68, 55, 61, 77, 74, 82, 24, 47, 89, 56, 15, 73, 45, 95]
"replace 15 at index 15 with 95 at index 18"
current: [53, 76, 67, 38, 41, 68, 55, 61, 77, 74, 82, 24, 47, 89, 56, 95, 73, 45, 15]
"replace 56 at index 14 with 95 at index 15"
current: [53, 76, 67, 38, 41, 68, 55, 61, 77, 74, 82, 24, 47, 89, 95, 56, 73, 45, 15]
"replace 77 at index 8 with 95 at index 14"
fixed: [[53, 76, 67, 38, 41, 68, 55, 61, 95, 74, 82, 24, 47, 89, 77, 56, 73, 45, 15]]

If i a find a rule for a number in an update, where the update number should be before the the second number in the rule, i swap them with two simple replace_at

IO.inspect(acc_, label: "current", charlists: :as_lists, limit: :infinity)
IO.inspect("replace #{b} at index #{b_index} with #{next} at index #{new_next_id}")
acc_ |> List.replace_at(new_next_id, b) |> List.replace_at(b_index, next)

this part isnt actually rocket science … but i wonder if its correct … well hard to read so its more like is the assumptions correct

i have looked at some of the rules manually for this example, and it seems to checkout

for ex the first one is a good example for all the others, and it represents what all the others do

invalid: [[47, 89, 77, 74, 61, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]]
current: [47, 89, 77, 74, 61, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]
"replace 89 at index 1 with 77 at index 2"
current: [47, 77, 89, 74, 61, 53, 82, 38, 45, 73, 15, 76, 41, 24, 56, 55, 67, 68, 95]

here 89 swaps place with 77.

Is it correct assumption that the rules is always left to right oriented maybe?

The full “documented” algorithm in its full glory:

  defp fix_invalid_update(update, rules) do
    update
    # reduce with update itself as accumulator
    |> Enum.reduce(update, fn next, acc ->
      rules
      # get alll the rules for the number "next",
      # assuming i should only get rules where number "next" is the first number in the rule
      |> Enum.filter(&(&1 |> Tuple.to_list() |> hd() == next))
      # reduce the rules with the current acumulator as accumulator (the update)
      |> Enum.reduce(acc, fn {_, b}, acc_ ->
        b_index = acc_ |> Enum.find_index(&(&1 == b))
        # find new position for next, since it can have changed since last iteration
        new_next_id = acc_ |> Enum.find_index(&(&1 == next))

        if b_index == nil do
          acc_
        else
          if b_index < new_next_id do
            IO.inspect(acc_, label: "current", charlists: :as_lists, limit: :infinity)
            IO.inspect("replace #{b} at index #{b_index} with #{next} at index #{new_next_id}")
            acc_ |> List.replace_at(new_next_id, b) |> List.replace_at(b_index, next)
          else
            acc_
          end
        end
      end)
    end)
  end

havent read the others solutions yet. If i cant make it work myself its not worth the cheat :wink: