Advent of Code 2017, Day 12: Digital Plumber

#ruby #advent of code 2017

Part A

On Day 12 we are working with disjoint sets. Our input looks like this:

0 <-> 2
1 <-> 1
2 <-> 0, 3, 4
3 <-> 2, 4
4 <-> 2, 3, 6
5 <-> 6
6 <-> 4, 5

Numbers represent program ids. The input file defines which programs are connected via pipes. Our goal is to output how many programs are in the group 0.

Because each number can belong to just one set we have disjoint sets here. In the beginning each number has its own set with just one element. We can read our input and combine sets as specified in the input. There is a perfect structure for representing disjoint sets. I know this data structure by the name Find & Union.

We will use the simpler version with just one array @p that will hold the parent value. The value will be used for indexing, so for number 5 it’s parent will be stored at @p[5]. In the beginning each value of this array will be equal to its index. This means that each number is in its own set.

If we want to combine two sets need to pick one number as parent and then set it as. Let’s see an example given our input file.

In the beginning we have 7 numbers from 0 to 6, so our @p array will look like this [0, 1, 2, 3, 4, 5, 6]

First line of our input is 0 <-> 2, so we need to join set containing number 0 with set containing number 2. After this joining we will have @p looking like this [0, 1, 0, 3, 4, 5, 6].

Now if join 2 and 6 we will have [0, 1, 0, 3, 4, 5, 2]

If we want to find what is the parent of given number we can just look at @p and then follow references until we reach the value that is the same as index. If we check 6, it’s parent is 2. So we check 2 and it also has a parent 0. Finally parent of 0 is 0 so we stop here.

Here is the solution in Ruby:

data = File.readlines("12.txt", chomp: true)
size = data.size

@p = []

def make_set(x)
  @p[x] = x
end

def union(x, y)
  link(find(x), find(y))
end

def link(x, y)
  @p[y] = x
end

def find(x)
  if x != @p[x]
    @p[x] = find(@p[x])
  end

  @p[x]
end

size.times do |number|
  make_set(number)
end

data.each do |line|
  parts = line.split(" <-> ")
  number = parts[0].to_i
  neighbors = parts[1].split(",").map { |item| item.strip.to_i }

  neighbors.each do |neighbor|
    union(number, neighbor)
  end
end

root = find(0)
count = 0

size.times do |number|
  if find(number) == root
    count += 1
  end
end

puts count

find method is used to find the top-level value that is the parent for all numbers in the given set. It has certain optimization implemented called path compression. It is used to remove long chains of parents and link child numbers with one top-level parent directly.

For example we can have all numbers belonging to the same set, but with long parent chains like this [0, 0, 1, 2, 3, 4, 5]. You can see that all numbers belong to one set, but 1 is connected to 0, 2 is connected to 1, 3 to 2, 4 to 3, 5 to 4 and 6 to 5. To find top-level parent of 6 find method will need to follow this long path and scan the whole table. Path compression after following such long path is updating all numbers to link to the top-level element. So after calling it for 6 we would have the following [0, 0, 0, 0, 0, 0, 0].

Our solution is using Find & Union data structure. First we create all sets, each containing just one number. Then we read our input and we union sets as specified. Finally we find the root for 0 and count how many sets have the same root value, meaning that belong to the same set.

Part B

In the second part we need to output how many sets there are after joining everything. Here is the solution:

require "debug"
require "set"

data = File.readlines("12.txt", chomp: true)
size = data.size

@p = []

def make_set(x)
  @p[x] = x
end

def union(x, y)
  link(find(x), find(y))
end

def link(x, y)
  @p[y] = x
end

def find(x)
  if x != @p[x]
    @p[x] = find(@p[x])
  end

  @p[x]
end

size.times do |number|
  make_set(number)
end

data.each do |line|
  parts = line.split(" <-> ")
  number = parts[0].to_i
  neighbors = parts[1].split(",").map { |item| item.strip.to_i }

  neighbors.each do |neighbor|
    union(number, neighbor)
  end
end

count = (0...size).map do |number|
  find(number)
end.uniq.size

puts count