2024-12-27
[Go to index] [Advent of Code website] [My code on Gitlab]
Day 23 was an easy one for me. I guess I prefer graphs to recursions.
The setup here is that we have a list of connections between computers, such as:
kh-tc
qp-kh
de-cg
ka-co
...
Where each computer is identified by two letters, and the connections
are biderectional. From the connections we can determine a graph of the
network. I decided to store it in a dict
, mapping each
computer name to a set
of its neighbours.
= dict[str, set[str]]
Network
def parse_input() -> Network:
with open("day23", 'r') as fp:
= [l.strip().split('-') for l in fp.readlines()]
data
= defaultdict(set)
network for c1, c2 in data:
network[c1].add(c2)
network[c2].add(c1)
return network
For part 1, we need to find all triplets of computers which are all
connected to each others, and contain at least one computer whose name
starts with a t
.
We can find those triplets very easily in our graph: starting from
each computer (whose name starts with t
), we look at all
their neighbours. If we start from A
, and B
is
in their list of neighbour, then they form a triplet with every computer
C
that is in the neighbour list of both A
and
B
. Since we store those neighbours as set
s, we
can use intersect
to get those common neighbours.
def find_triplets(network: Network, predicate: Callable[[str], bool] = lambda x: True) -> list[tuple[str, str, str]]:
= set()
triplets for key, neighbours in network.items():
if not predicate(key):
continue
for n in neighbours:
if len((intersect := neighbours.intersection(network[n]))) >= 1:
= {tuple(sorted([key, n, x])) for x in intersect}
t = triplets.union(t)
triplets return triplets
I made the “filter computers starting with t
” part
unecessarily generic with a predicate
lambda
function, which by default doesn’t filter anything. The triplets are
stored in a set
to avoid duplicates (as long as we sort the
computer names before putting them in a tuple
). And that’s
about it for part 1, we just need to call the function with the
appropriate predicate and we’re done. The answer for part 1 is the
number of triplets we found.
= parse_input()
network = find_triplets(network, lambda x: x.startswith('t'))
triplets print(f"Part 1 answer: {len(triplets)}")
For part 2, we need to find the largest “dense” subnetwork, i.e. a part of the network where every node is connected to every other node. The answer is the list of the computers in that subnetwork, sorted alphabetically.
Here I went with the following idea:
M
such that
M[i][j]
is True
if i
is connected
to j
) to prune the neighbour list until we get a
connectivity matrix where everything is True
.I wrote two functions to do all of that:
def get_dense(connectivity, network: Network, keys: list[str], key: str):
# get largest dense network from a seed key
# get connectivity matrix with only neighbours from the seed
= [(k in network[key] or k == key) for k in keys]
keep = [[c for i, c in enumerate(row) if keep[i]] for j,row in enumerate(connectivity) if keep[j]]
connectivity_ = [k for i,k in enumerate(keys) if keep[i]]
keys_
# remove key with fewest connection with the others until we have a fully connected network
while sum(sum(not v for v in row) for row in connectivity_) > 0:
= -1
most_false_id = 0
most_false for r, row in enumerate(connectivity_):
if (n_false := sum(not v for v in row)) > most_false:
= n_false
most_false = r
most_false_id if most_false_id == -1:
break
= [[v for c, v in enumerate(row) if c != most_false_id] for r, row in enumerate(connectivity_) if r != most_false_id]
connectivity_ = [k for i,k in enumerate(keys_) if i != most_false_id]
keys_ return keys_
def find_largest_dense(network: Network) -> list[str]:
# sort by most connections
= [(key, len(neighbours)) for key, neighbours in network.items()]
connections = sorted(connections, key=lambda x: x[1], reverse=True)
connections # connectivity matrix with keys sorted alphabetically
= sorted(network.keys())
keys = [[(k2 in network[k] or k2 == k) for k2 in keys] for k in keys]
connectivity
= 0
most_connected = []
keys_in_most_connected for key, n in connections:
# if we have fewer neighbours than the current largest subnetwork, we can stop
if n < most_connected:
break
= get_dense(connectivity, network, keys, key)
keys_ # update largest subnetwork
if len(keys_) > most_connected:
= len(keys_)
most_connected = keys_
keys_in_most_connected
return keys_in_most_connected
And with that, we have everything necessary for part 2:
= find_largest_dense(network)
dense_net print(f"Part 2 answer: {','.join(dense_net)}")