#AoC 2023 | Day 8 | Solutions & Spoilers
1605 messages Β· Page 2 of 2 (latest)
assume sucha cycle exists
then at step 22 we are at the start location
but now, we are looking at the directions at index 2, rather than index 0
btw you definitely can't bruteforce today right?
yeah that's my point

it's not specific to the data
It's a proof that given the problem construction, any cycle on the graph will always be divisible by the number of steps
unless the steps array itself has cycles
sorry, direction array I should say
if the direction array is size 8, but it internally has a cycle of length 4 (i.e. it's just the same 4 directions repeated twice), then you can have cycles that are divisible by 4 instead of 8
if we exclude that though
then if the direction array is size 8, then all cycles must be divisible by 8: this isn't an artifact of the data
I have reached the conclusion that: I successfully solved part 2 quickly by complete accident. My reading incomprehension paid off.
just to emphasize; when we are writing cycles out here we assume that the cycles always traverse distinct nodes.
that's not actually necessarily the case and it's not the case for the input data either
LCM occurred to me because I assume there were certain cycles... but looking closely, the cycles are more complicated than I expected
In the actual data, you have cycles that look like this
A -> B -> C -> A -> D -> ...
in other words, you go through A multiple times in the cycle
but it's not a cycle yet, because each time you reached A, you were at a different index in the directions
Here's what start/ends of the first cycle look like: ```py
start end startleft startright endleft endright
QKA QXZ QQD SSM SSM QQD
LBA GPZ XQJ PFH PFH XQJ
JMA VHZ CVR HFX HFX CVR
VMA MTZ DCG TDF TDF DCG
AAA ZZZ KPT QLD QLD KPT
RKA NDZ BGX BCX BCX BGX
we're discussing a different definition of cycle
So, for start QKA, it ends on QXZ, and the next positions are inverted. Presumably the better solution here is to memo-ize the starts, repeat until I have a complete cycle.
i'm talking about just the first time you reach Z after stepping away from Z
if we account for the current position in the steps, then yes, it will still cycle
it's not really a cycle then, because a cycle is something that repeats
but I understand what you're saying now
A better way to put it IMHO is that Z only shows up once per cycle
@lofty rose I forgot to thank you for your insight! Sorry I forgot
which is also not a given
yeah, my definition of cycle might've been a bit off there sorry
I'm not sure that's true, looking at my data... because the first "Z" doesn't lead to a cycle on the next element.
I actually mentioned this assumption at the start, btw, but someone else took that to mean it was different Z's
and I got a bit misled
@trail vale
wdym doesn't lead to a cycle?
I still think this is actually, correct, for what you need to do for every start; you have to find:
- the length of the cycle
- the offset of the end within the cycle (this is a big assumption; that there's exactly 1 end)
- the offset of the cycle from the start
so in 2 I mention that every cycle can only have one end
If Z => Z in a number of steps that was not a multiple of directions.len()
I'm going to go do some graphviz later
Which was your concern
then that would not be a cycle anymore, and in the process of completing a real cycle you'd have hit Z twice
it also happens to be the case, that in my bullets above, the first value is equal to the sum of the next two
the length of each cycle = offsend of the end + distance to cycle
because the RHS is just A => Z
(A to cycle start) + (cycle start to Z) = A to Z
Oh nm, my data is confusing me. I was right the first time, my data does repeat to a cycle , just not for the reason I expected. Only one Z is visited for each cycle.
hitting Z multiple times is the annoying thing to code tbh
it means that over your CRTs, you have to consider multiple congruences because the time when you match Zs doesn't care which point you're at in the steps
whereas for the current situation, it's guaranteed you'll be at the start because of all the nice divisibility, so you can't have any intermediate Zs to hit
interestingly there are starting nodes that do not cycle
but if you hit Z's multiple times, then you have to use chinese remainder theorem with multiple possible remainders
so you have to do a search over "which" Z you're going to hit in every cycle, plug that into CRT, pop out the number
and get the minimum of all those
pretty nuts
i've been going through my nodes and I got one here that doesn't to a cycle. stopped it at 13 million
so interesting
it doesn't cycle to itself, sure
that' snot that surprising/interesting
but every node eventually goes into a cycle
Not for me. I have 6 start nodes. All terminate.
oh no not from the tast
the phase space is finite so by the pidgeonhole principle you have to cycle
just going through all notes and checking if they go back to that node with the instruction
they may never return to the same node, sure
with these instructions you just keep missing it
you have to cycle, but the starting node doesn't have to be part of the cycle
A => B = > C => B
assuming that there is a solution and given the size of the input, i think it's fine to assume that the cycle will always contain the Z
I guess, it's less that I'm against this style of AOC, so much as I just didn't expect it, to thi sdegree
I don't think I've ever done an AOC problem before that was this much "data exploration"
half of this problem is basically figuring out just how simplistic and engineered the data is
Is this an anti-gpt feature?
and half is actually programming the solution
no, they've already said gpt did not affect problem selection
anyhow, in a normal AoC it's like... 90% programming at least, and maye 10% verifying some basic assumptions about the data
2015 day 19 is kinda similar
tho even for that day you had people programming a general solution
for today it would be difficult to do that i think
if i were to approach it, i'd first find all the Z nodes that connect to A nodes, join up all the Z nodes into their respective cycles at that offset of A -> Z, find the cycle length and mark down every single Z's offset from the start Z in each cycle, and then do CRT on every offset across every cycle and find the minimum value
I could be wrong but I don't think a genral solution is that bad
and yeah, it's what you said
it's not that bad, but it's a significant step up from "just take the lcm of the starting offsets"
it's a cycle length + list of offsets for every start
and then search on CRT for every combination of offsets
yep
the main thing is the CRT with multiple congruences
we had to do CRT for that bus problem a couple years ago didnm't we
so something like that could be suitable for a day 20+ problem
i don't know of any efficient algorithms for computing CRT across multiple moduli with multiple congruences
backtracking would be very helpful, but even then it'd take a lot
tho i guess it's reasonable to assume a reasonable input with few As and Zs if it were considering the general case
interestingly there are 6 nodes that don't return to the start, 6 nodes that have numbers in the 5 digits and everything else is under 600.
i like https://brilliant.org/wiki/chinese-remainder-theorem/
you can skip the proof bit and just go to the "solving systems of congruences" bit, and i think that gives a pretty clear method on how to do it
so they selected those inputs carefully
Ugh. Now you're going to make me think.
thank you, I appreciate it
in many many more ways than that
yes no doubt there
that's a pretty small thing compared to some of the other things discussed here
like A=>Z == Z=>Z
yeah i also found that with my test
assumptions:
- A -> Z == Z -> Z
- Z -> Z is a multiple of
len(steps) - each Z is contained within its own cycle (AZ can't reach BZ for example)
- A always reaches a Z (this one doesn't really count, since otherwise there would be no answer, so it's a valid assumption to make)
i think that's all you need to get today's inputs
I spent a long time trying to figure out how to account for all sorts of possible cycles only to find out here that the input was chosen to be simple π So here is my solution, part1: 4.5ms, part2: 28ms
https://paste.pythondiscord.com/5OOQ
is it normal to do an import in a function?
usually not
no, I just keep solutions for part1 and 2 together so I don't get confused
because I've never seen that before so I wondered
if I were writing a normal script I would throw my imports up on top
it's not a good practice π
but I'm only running this function once
yes but it doesn't hurt for it to be up top so why go out of way to put it in the function?
it's probably going out of the way to put it at the top
usually if you're coding a function you're already there, and putting the import there is more convenient
to put it on top I would have to scroll for half a second, too much work π
is it bad practise to just import math instead of import math import lcm?
Part 2 py o=open(0).readlines() f={x[:3]:(x[7:10],x[12:15])for x in o[2:]} a=[x for x in f if x[2]=="A"] r=[1]*len(a) for j,y in enumerate(a): t = __import__("itertools").cycle(o[0]) while(y:=f[y]["LR".index(next(t))])[2]!="Z":r[j]+=1 print(__import__("math").lcm(*r))
HEY , what do you think about my solution for today?
https://github.com/DownDev/advent-of-code/blob/main/2023/08-1.py
404 - page not found
now it works
from math import lcm is arguably the worse of the two
not saying it's that bad, I do it as well
lcm = __import__("math").lcm
but from a code readability standpoint it's less cluttering of the namespace
and you can see where things come from easier
from numpy import*
best version
what does that do?
It definitely depends. If it's something like pandas where it's mostly operations on the class instances, then import pandas as pd is desirable for less clutter. If it's something like manim where you will be spamming the module's functions and class, from manim import * is standard.
takes every single function, class and global variable from numpy and puts it into your namespace
quick question, my code works for the test case. it's been running for like 2 minutes or so. How many steps did it take for you to reach the end ```py
import importlib.util
def importFromPath(path):
spec = importlib.util.spec_from_file_location("module.name", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
myModule = importFromPath(r"D:\juliu\Python.py\adventOfCode\inputParser.py")
orders = [("(", ""), (")", ""), ("=", ","), (" ", "")]
for char, nchar in orders:
myModule.removingCharacters("challengeInput.txt", char, nchar)
def convertInstructions(string):
for element in string:
if element == "L":
yield 0
else:
yield 1
with open("challengeInput.txt", "r", encoding="utf-8") as file:
lines = file.readlines()
instructionSet, mapping, totalStep = lines[0].replace("\n", ""), {}, 0
for line in lines[2:]:
temp = line.replace("\n", "").split(",")
mapping[temp[0]] = temp[1:]
keyValues = list(mapping.keys())
key, endKey = keyValues[0], keyValues[-1]
while key != endKey:
for index in convertInstructions(instructionSet):
key = mapping[key][index]
totalStep += 1
if key == endKey:
break
are you trying to simulate all of the steps? it would take years
part 2 you can't bruteforce. 1 like a second
my answer was in trillions of steps
Maybe doable if you have a good graphics card and write shader code for it.
I did try, but after coming back to my laptop after lunch I found it still working and decided to stop π
how would i go about finding the steps? Im literally only following the steps provided in the first line
Hey, what image paste utility do folks use around here?
I want to paste a graphviz image
discord
Too big
put every node in a dict and use key = d.get(key)
then step += 1
Oh, that's weird. It failed first try. This worked
oh nice
That's my input data.
oh wow
im doing that?
but i have to decide to which node im going to go next based on the instruction (LR)
how are your values structured?
oh ok that graph clears it up for me, why lcm works for this data
l means you use the left one. r the right. convert l to 0 r to 1
Yah, I didn't distinguish between L and R in that graph. But the point is, it doesn't matter.
you're going from keys[0] to keys[-1] @steel rapids
in the full input those aren't in order but you still have to go from AAA to ZZZ
well you have to zoom in to see that every A path node matches with every Z path node
but yeah that makes it clearer why lcm works
i have my key : [leftKey, rightKey] based on the instruction i decide if i go with the left or the right key for my next node
do we even need LCM
I have just learned that itertools.cycle exists, nice
or are the cycles / dir length mutually prime
Sorry but what do you mean?
No, since they're all multiples of the "plan" length.
my cycles were {20803, 17287, 23147, 13771, 19631, 17873}
after division by that
is it possible to have an endless loop with nodes? So that you may never access the endnode`?
Then there'd be no solution. The problem, as stated, assumes that there is a solution.
you wrotepy key, endKey = keys[0], keys[-1]
this works for the example, where the dict starts at AAA and ends at ZZZ
but this may not be the case in the full input - you should write literally "AAA"
assuming there exists a solution is a valid assumption imo
yeah
lol yeah this explains it
you don't sort your keys. if you do it might just work
I think the assumptions that are always fair game are basically:
a) a solution exists
b) when solutions involve integers, they will fit inside a 64 bit integer
!e print(2**64)
@olive salmon :white_check_mark: Your 3.12 eval job has completed with return code 0.
18446744073709551616
well no need it finished like instantly
there's probably very specific cases where b) doesn't hold, where it's almost being treated like a string but it's rare
yeah I'd agree with that
if there were such a day though then python would suddenly have an enormous advantage π
python has no upper limit? (practically?)
I will say it's pretty annoying in some languages, like e.g. in Kotlin the pervasive default integer is 32 bits, and solutions that don't fit in 32 bits are not uncommon
so you have to use a 64 bit integer type everywhere basically, and there's no implicit conversions so there's a lot of casting. gets old fast
(but then it gets old in Rust too for differnt reasons)
it checks for overflow and switches to an arbitrary precision type
Rust has been good so far about inferring other types based on, say, one annotation.
quicknir, this is your input:
pretty much, it can update the necessary size of the data type to make the number fit
oh nice
the inference is good but there's still a lot of places you have to cast, particularly if you care about performance and want to use smaller types when possible for packing
what about concentric
also in a lot of sitautions where you want to use signed integers, and rust has sadly used unsigned integers for indexing
What?
it would look nice if they were all concentric
Oh, I'm not artistic π I just threw that into graphviz.
If I were more motivated, I'd animate the traversal
well that works
awesome
I had to cheat a bit with rescaling to make them all fit, but here you go
they do N steps at a time smh
Nsteps?
as you should when using a gpu π€·ββοΈ
link pls
as in, they transform the original from a one step mapping to a N step permutation and then apply that repeatedly
which only works if it finishes at a multiple of N steps (which it does, but eg LCM algorithm isn't 'brute force')
fair
Great! Perhaps we can use this map to escape the haunted wasteland.
this is not possible if all paths cycle and the cycle lengths are relatively prime
the term is 'pairwise coprime'
both terms are the same
also mutually prime
or just a is prime to b
i did take number theory courses once long ago
that's not guaranteed to work
but may work depending
o - o - o - o - o - z
o - z - o
these cycles won't line up, but they will if both the z's are on the end (like in our input)
Actually, the cycles don't have to be pairwise coprime, the constraint is slightly more relaxed than that - they just have to have the endpoint fall on the same step modulo their GCD
Like oooooz and ozoo have a GCD of 2, and the z occurs at 1 mod 2 in both cycles
yep
i maybe on the wrong track but this code works for the test case. All my challenge Inputs seem to be closed loops without having a node that has the letter z in the end```py
import importlib.util
def importFromPath(path):
spec = importlib.util.spec_from_file_location("module.name", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
myModule = importFromPath(r"D:\juliu\Python.py\adventOfCode\inputParser.py")
orders = [("(", ""), (")", ""), ("=", ","), (" ", "")]
for char, nchar in orders:
myModule.removingCharacters("challengeInput.txt", char, nchar)
def checkingForUnclosedLoop(start, end, mapping, instructions):
visited, stepCount, instructionCycle = set(), 0, 0
while start[-1] != end:
instructionCycle += 1
for instruction in instructions:
if start in visited:
return False
visited.add(start)
#print(f"{mapping[start]}{instruction} -> {mapping[start][instruction]}")
start = mapping[start][instruction]
stepCount += 1
if start[-1] == end:
break
return stepCount, instructionCycle
with open("challengeInput.txt", "r", encoding="utf-8") as file:
lines = file.readlines()
ordering, startPoints, instructionSet = {}, [], [0 if element == "L" else 1 for element in lines[0].replace("\n", "")]
for line in lines[2:]:
temp = line.replace("\n", "").split(",")
if temp[0][-1] == "A":
startPoints.append(temp[0])
ordering[temp[0]] = temp[1:]
steps, cycles = [], []
for startPoint in startPoints:
result = checkingForUnclosedLoop(start=startPoint, end="Z", mapping=ordering, instructions=instructionSet)
print(result)
if result:
result, cycle = result
cycles.append(cycle)
steps.append(result)
Paste your data, if you like?
LR
11A,11B,XXX
11B,XXX,11Z
11Z,11B,XXX
22A,22B,XXX
22B,22C,22C
22C,22Z,22Z
22Z,22B,22B
XXX,XXX,XXX
``` here's the data i used from the test case
File "C:\Users\ducke\PycharmProjects\adventofcode\2023\day8.py", line 68, in goto_new_location
goto_new_location(current_location, direction_index, total_steps)
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded
rip
U can color code the left and right like #1182550042091991180 message
circo?
did you figure this out?
@modest frost I didn't make them, I just used FireAlpaca (my art program of choice) to edit the existing images. Here's with that that colored one, again having to cheat with scaling to make everything fit.
AHA
if start in visited:
return False
You might need to be able to visit a node twice though, once left, once right
I removed those 2 lines and your code produced the correct answer on my input.
I'm using obsidian with the excalidraw plugin
but I also have a drawing tablet so ymmv
Paste over in the viz thread?
Iβve been playing around with animating it
@velvet bough did ya bash
sure
I mainly want to make sure you tag along π
I did split the scripts
you know what exists to share code?
github
π
well, we might actually be getting a new vcs frontend
but it's not a git-like one
github changing the UI again π
it's pretty neat ideas-wise, I'm using it for my aoc repo
The project is called "Jujutsu" because it matches "jj".
π©
so I was even lazier with input parsing
π
I did this
declare -A lookup
while read line; do
s=( $line )
lookup[${s[0]}L]=${s[1]}
lookup[${s[0]}R]=${s[2]}
done < <(tr -d '=(,)' | tr -s ' ')
tr the crap away
dump into an array
and notably you need to do the < version, and not a pipe
because a pipe runs in a subshell π€‘
it took me a bit until I realized why I was getting empty lookups
not having $somethingthatdoesn'texist fail is understandable, but so so annoying
so, my solution kinda accidentally depended on that fact that doing gcd from A->Z works
granted, I kinda don't like this problem in general because of the "lol, super specific special case"
why are you like this? π
printf "Part 2: %d\n" $p2
just echo "Part2: $p2" like normal people
yeah i kinda forgot you could do that
oh?
but as said, it ends up depending on the thing with the A-Z distances
oh that's nice. i coudn't figure out a way to avoid the duplication between p1 and p2
it's just the euclidean thing
day08/main.sh lines 29 to 31
function gcd() {
! (( $1 % $2 )) && echo $2 || gcd $2 $(( $1 % $2 ))
}```
oh your gcd. yeah
overall not too bad looking solution given that it's bash
I'm kinda curious what metric Kat is using to rank languages though π₯΄
good question...
given that this was in week 2
also it looks like github syntax highlighting is mad at my code
a bunch of the languages in week 1 is way safer than bash
especially performance-wise
well like, everything-wise
also thank god we didn't need to do float computations
can prolog do floats?
good question
meh, python is cheating
much readable
the first line is:
__import__('sys').setrecursionlimit(1_000_000)
it's cheating in a cheating language. π
let me a good citizen and make that screen shot a tad better
the whole joke was the long screen shot. I long since posted the code as code.
;-;
with less pixels:
dirs, paths = open(aocinput).read().split('\n\n')
paths = {v[:3]:{'L':v[7:10],'R':v[12:15]} for v in paths.splitlines()}
def steps(s,c=0):
return c if s[-1] == 'Z' else steps(paths[s][dirs[c%len(dirs)]], c+1 )
print('part1', steps('AAA'))
print('part2', lcm(*[steps(k) for k in paths if k.endswith('A')]))
lcm doesn't take an iterable?
no π¦
maybe that's for the better. max/min are horrifying
though i would have preferred only iterable vs only *args
I really do not enjoy the default= syntax of min/max. feels awkward
I recall gcd being a two argument function for a while
which was then generalized
indeed
and lcm mirrors gcd when it was added at the same time
ah that makes sense then
these are functions though
I knew that risk
I foolishly thought you better than that
(I would have done it, but I'm a terrible person)
it's because the roulette fr
in unrelated news I actually recieved my new phone
nice to actually see stuff again
is it true that part 2 has a very high number os is my code broken
part1 19XXX
part2 13663968XXXXXX
ah so my 600k is not suprising
its still running
600_000 ? when the answer is around 13_663_968_000_000
you might be here a while.
You might even want to consider an alternative approach. The correct answer is foud in under a second. Bruteforce is not the way to go here.
is there some sort of trick?
yes.
Ok finally finished code and tests
code
import dataclasses
import enum
import itertools
import math
import parse
START_ELEMENT = 'AAA'
END_ELEMENT = 'ZZZ'
START_NODES_LETTER = 'A'
END_NODES_LETTER = 'Z'
class Direction(enum.Enum):
LEFT = 'L'
RIGHT = 'R'
@dataclasses.dataclass(frozen=True)
class Node:
left: str
right: str
class Navigation:
def __init__(self, nav_input):
instrs, network = nav_input.split('\n\n')
self.instrs = [Direction(dir_) for dir_ in instrs]
self.network = {}
for node in network.splitlines():
parsed_node = parse.parse('{input_ele} = ({left_ele}, {right_ele})', node)
self.network[parsed_node['input_ele']] = Node(parsed_node['left_ele'], parsed_node['right_ele'])
@classmethod
def read_file(cls):
with open("input.txt") as f:
return cls(f.read().strip())
def navigate(nav: Navigation, ele=START_ELEMENT, end=True):
instr_map = {Direction.LEFT: 'left', Direction.RIGHT: 'right'}
for step_no, instr in enumerate(itertools.cycle(nav.instrs), start=1):
node = nav.network[ele]
ele = getattr(node, instr_map[instr])
yield ele
if end and ele == END_ELEMENT:
return step_no
def navigate_simultaneously(nav):
"""
Navigate simultaneously only where paths follow a loop with no offset
and only one end element is visited in the whole loop
"""
start_eles = [ele for ele in nav.network if ele.endswith(START_NODES_LETTER)]
end_ele_steps = []
for start_ele in start_eles:
nav_iter = navigate(nav, ele=start_ele, end=False)
end_nodes = []
for step in itertools.count(1):
next_ele = next(nav_iter)
if next_ele.endswith(END_NODES_LETTER):
end_nodes.append((next_ele, step))
if len(end_nodes) == 2:
break
end_node_1, end_node_2 = end_nodes
if end_node_1[0] != end_node_2[0]:
raise ValueError('More than one end element visited')
if divmod(end_node_2[1], end_node_1[1]) != (2, 0):
raise ValueError('Path to end element does not follow a loop with zero offset')
end_ele_steps.append(end_node_1[1])
return math.lcm(*end_ele_steps)
def main() -> None:
nav = Navigation.read_file()
nav_iter = navigate(nav)
while True:
try:
next(nav_iter)
except StopIteration as exc:
steps = exc.value
break
print(
f"Total number of steps required to reach {END_ELEMENT}:",
steps,
)
print(
f'Total number of steps required to end on a {END_NODES_LETTER} element '
f'starting from a {START_NODES_LETTER} element simultaneously:',
navigate_simultaneously(nav)
)
if __name__ == "__main__":
import timeit
print(timeit.timeit(main, number=1))
tests
import pytest
from year_2023.day_08 import process
def test_navigation_no_cycle():
nav_input = """\
RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)"""
nav = process.Navigation(nav_input)
nav_iter = process.navigate(nav)
assert next(nav_iter) == 'CCC'
assert next(nav_iter) == 'ZZZ'
try:
next(nav_iter)
except StopIteration as exc:
steps = exc.value
else:
pytest.fail()
assert steps == 2
def test_navigation_cycle():
nav_input = """\
LLR
AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)"""
nav = process.Navigation(nav_input)
nav_iter = process.navigate(nav)
exp_elements = ['BBB', 'AAA', 'BBB', 'AAA', 'BBB', 'ZZZ']
for ele in exp_elements:
assert next(nav_iter) == ele
try:
next(nav_iter)
except StopIteration as exc:
steps = exc.value
else:
pytest.fail()
assert steps == 6
def test_navigate_simultaneously():
nav_input = """\
LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)"""
nav = process.Navigation(nav_input)
assert process.navigate_simultaneously(nav) == 6
def test_navigate_simultaneously_fails_if_more_than_one_end_element_visited():
nav_input = """\
LR
11A = (11B, 11B)
11B = (XXX, 11Z)
11Z = (22B, XXX)
22B = (XXX, 22Z)
22Z = (11A, 11A)
XXX = (XXX, XXX)"""
nav = process.Navigation(nav_input)
with pytest.raises(ValueError, match='More than one end element visited'):
process.navigate_simultaneously(nav)
def test_navigate_simultaneously_fails_if_path_to_one_end_element_does_not_follow_loop_with_no_offset():
nav_input = """\
LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11C, XXX)
11C = (XXX, 11D)
11D = (11E, XXX)
11E = (XXX, 11A)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)"""
nav = process.Navigation(nav_input)
with pytest.raises(ValueError, match='Path to end element does not follow a loop with zero offset'):
process.navigate_simultaneously(nav)
there's only 6 paths, consider finding out how long each of them takes to finish. and then think about how you could calulate when they would all finish at the same time.
I have error handling in my code
Each of your tests is longer than my entire codebase.
I love it. π
If it's given an input which has a path which isn't a strict loop, or has an offset from the loop, or visits more than one element ending with Z in a single path, it kills the process
XD
I love to write these as if they are production code that requires full test coverage
why aren't the links in your dataclass to other nodes?
I do like reading your solutions
Yeah it's a fnatasic approach and great interview fodder if you put it on a github.
"here, this is how I write code".
eh I have the elements as strings and then the nodes are just a collection of two elements
the links are just a dict
I'm also getting better with it too. I have a better sense of when to use classes vs when not to
@velvet bough btw you should have seen my initial part 1, which re-read the file on every step
so what i need to do is find how many turns it takes for a path to reach a Z location and then i need to find if the other paths are on a Z location aswell on that turn?
grep for correct start, parse out left/right
π₯΄
at least i learned a lot from today. i'll probably use bash more now
less python scripts
Can you not just work out how long it takes for each start point to loop and then use that bus stop tehcnicque from before to work out when they intersect?
wait no, that wouldnt capture the end
it's not that terrible, yeah
something like that. like if you had 4 paths and they finished in 3,5,7, 9 turns. you could see that on turn 105 they're all at the finish together.
I thought that when I first learned bash and that turned out NOT to be the case lol
learning bash arrays in particular is quite helpful
it just turns so quickly into trying to figure out how to do something in bash and not actually doing the thing
that's kind of a skill issue though
is 105 a random example or is there an equation i dont see
working with arguments you want to pass? array
most of my bash scripts turned into a find exec
xargs is based though
When it comes to file parsing at least, I use bash for simple stuff like head command (if that even counts as parsing) - and then anything more complex I immediately go to Python
trade-offs
it's the correct answer for 3,5,7,9
see if you can figure out a way to get the answer and if you can't I'll gladly spoil it for you.
bash - low barrier for working with cli, great at some stuff
python - more of a barrier, but much more flexible
c++/rust/whatever - big barrier - speeeeeed
So which do we like more:
def steps(s):
c = 0
while s[-1] != 'Z':
d = dirs[c%len(dirs)]
s = paths[s][d]
c += 1
return c
vs for/count:
def steps(s):
for c in count():
if s[-1] == 'Z':
return c
s = paths[s][dirs[c%len(dirs)]]
vs recursive counting:
def steps(s,c=0):
return c if s[-1] == 'Z' else steps(paths[s][dirs[c%len(dirs)]], c+1 )
cough ignoring: steps = lambda s,c=0: c if s[-1] == 'Z' else steps(paths[s][dirs[c%len(dirs)]], c+1 )
i have no clue
counter-offer: enumerate(cycle(dirs))
well one hint is 3*5*7 = 105
that's not bad too.
I often wonder between the first two (never 3 though)
itertools.cycle() is convenient yet I don't like the idea of using for for an indefinite loop
it occurs to me my example is bad, the 9 should have been a 15. π
I went for convenience this time :)
it does have a wrongness feel to it.
but i didn't like having a while loop with a counter, that's just a for loop with more steps
I don't really see the wrongness
def steps(s):
for c, d in enumerate(cycle(dirs)):
if s[-1] == 'Z':
return c
s = paths[s][d]
Yeah I like that. Very clean.
when the abstractions hit just right
only thing i can think of is that 3 + 5+ 7 = 15
I've always felt that for should be for definite structures
but of course it doesn't necessarily have to be that way
so: 4 paths, they finish in 3,5,7,15 amounts.
after 35 loops the '3' path reaches 105
after 21 loops the '5' path reaches 105
after 15 loops the '7' path reaches 105
after 7 loops the '15' path reaches 105
there's a simple mathematical way to find when they all reach the same path.
the path reaches loops * amount
You could just do 3 * 5 * 7 * 15. But that would be too big. Even though they do all meet at that number as well.
so you want the least multiple of those numbers where they all meet at the same time.
how would you find the smallest multiple all these numbers have in common ?
its been a while since i had this at school lmao
AOC does use concepts I haven't seen for a long time
cough cough ||quadratic formula||
so what would you do when you deal with infinite iterables?
I tend to while them tbh
while with try except StopIteration?
would you make a function that keeps multiplying and then checks if all the numbers have that multiple?
ok, if it's for sure infinite then I guess that's fine
the annoying case is possibly infinite
where you would need to except StopIteration
which is just...ugly
That's one approach. here's another: https://en.wikipedia.org/wiki/Least_common_multiple
tl;dr:
lcm(a, b) = a*b/gcd(a, b)
lcm(a, b, c, d) = lcm(a, lcm(b, lcm(c, d)))
I was about to do lcm(a, b, ..., y, z)
but couldn't figure out where to put the ...
my approach may be slower but i dont even know what a gcd is so that may be a hard task
gcd is the answer to "all numbers have that multiple"
e.g. gcd of 12 and 30 is 6
i.e. 2*2*3 and 2*3*5 has 2*3 in common
and once you understand the concept, you'll be pleased to know that from math import lcm exists.
!e print(import('math').lcm(3,5,7,15))
@stuck inlet :white_check_mark: Your 3.12 eval job has completed with return code 0.
105
ah thats nice to know
but how will this work when my paths are all letters
ah ye
ok so i basicly split each path, find the lengths from a -> z and then find the lcm of those paths?
that's about the gist of it.
someone brute forced part2 in rust. took them 6 hours.
I like the top comment: "Wow.. I understand this even less than LCM"
OP's explanation:
So, I wrote a GPU solution that goes through all of the possible solutions up until the correct one is found. To get parallelism for this seemingly purely serial task, I prepare tables that contain powers of the movement table: since the instructions loop on a cycle of the length of the direction instructions, one loop of the instructions is just a permutation. This permutation can be applied to itself to apply a square (and cube and so on) of the original single cycle permutation, so you can quickly get from 0 to iteration N and then brute force each step from there per thread.
that's great!
Now, we run a bunch of threads (around 200k) on the GPU. Each thread uses the precomputed lists to fast forward to a different position of the simulation really quickly, and then goes through a large number (some tens of thousands) of simulation steps and if it finds a solution, it marks it in global memory. After this, we check if any thread found a solution; if not, we nudge all indices forward to jump further in the simulation for each thread (this is what the "scanning from" prints in the output mean) to check again.
ha. still very fast. seems like a lot of work.
I was about to let my brute force method run, but then someone mentioned lcm and π‘, now it's about a millisecond
Nah took a break my code works for the test case
yeah, not the cleanest solution but doable i guess ```py
import importlib.util
def importFromPath(path):
spec = importlib.util.spec_from_file_location("module.name", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
myModule = importFromPath(r"D:\juliu\Python.py\adventOfCode\inputParser.py")
orders = [("(", ""), (")", ""), ("=", ","), (" ", "")]
for char, nchar in orders:
myModule.removingCharacters("testCaseData.txt", char, nchar)
def findingStepCount(start, end, mapping, instructions):
stepCount = 0
while start[-1] != end:
for instruction in instructions:
#print(f"{mapping[start]}{instruction} -> {mapping[start][instruction]}")
start = mapping[start][instruction]
stepCount += 1
if start[-1] == end:
break
return stepCount
def greatestCommonDivisor(a, b):
while b != 0:
a, b = b, a % b
return a
def findingMinimumSteps(steps):
def leastCommonMultiple(a, b):
return a * b // greatestCommonDivisor(a, b)
result = steps[0]
for num in steps[1:]:
result = leastCommonMultiple(result, num)
return result
with open("challengeInput.txt", "r", encoding="utf-8") as file:
lines = file.readlines()
ordering, startPoints, instructionSet = {}, [], [0 if element == "L" else 1 for element in lines[0].replace("\n", "")]
for line in lines[2:]:
temp = line.replace("\n", "").split(",")
if temp[0][-1] == "A":
startPoints.append(temp[0])
ordering[temp[0]] = temp[1:]
steps = []
for startPoint in startPoints:
steps.append(findingStepCount(start=startPoint, end="Z", mapping=ordering, instructions=instructionSet))
print(findingMinimumSteps(steps))
Mind if I give a few comments?
im not sure if I understand day-8 part2, can sm1 let me know if my brute force is towards the right direction
instructions, network = aoc_input
nodes = [node for node in network if node.endswith("A")]
for i, instruction in enumerate(cycle(instructions)):
if all(node.endswith("Z") for node in nodes):
return i
nodes = [network[node][instruction == "R"] for node in nodes]
basically, im gonna traverse all starting nodes until they all end with Z
you cannot bruteforce this
I know, but i wanna make sure that i understand the question before thinking of a different solution
the example for part2 wasnt clear enough for me
you can but you need a GPU lol
and a clever way of handling inputs that I haven't gotten my head around π
honestly it's easier just to do it "correctly"
at least my repo is now purged after rebuilding it twice and forgetting to include .gitignore :/
The way I was thinking about part2 was that I needed to keep applying part1 cycling solution to each starting position until those positions meet. That kinda seems like what you're doing but I don't understand nodes = [network[node][instruction == "R"] for node in nodes]
(I'm just talking about the assignment not solution)
thats just progressing all nodes to the next node, depending on the instruction (left or right)
hoping they all end with Z at some point
oh cycle comes from itertools?
yea
ah okay, you have set up your nodes differently to what I did but I think you've got the right idea: take instruction, apply that instruction to each current node, check if all of the nodes are at the end, repeat for next instruction
yea my nodes are pretty much like
{"AAA": ["BBB" ,"CCC"], ...}
cool! well done!
looks like you went ahead and implemented lcm yourself. But you should know that there is an lcm function in the standard library. . After I removed yout 2 lines about if visited return false all I added to your code to make it work was:
from math import lcm
print(lcm(*steps))
This'll work, but it might take a month to finish running. π Think of a better way to see how long each path takes to finish and then calculate when they would all finish at the same time if they kept looping.
Huh, this was easier than expected
@potent ginkgo i thought the cycles had an offset (being the first time an end node is reached) and went crazy trying to figure out an algo for computing LCM with offsets (my spaghetti code didn't help :P) ... so i gave up and looked at a solution
you have to be kidding me. i was one stupid assumption away. if i had persisted, i might have found that the offset of every cycle is equal to the cycle period itself (i.e. the time to reach an end node from a start node is equal to the time for that end node to reach itself). so there was in reality no offsets and the solution was to LCM all distances from start to end π©
Yup π This is why a bunch of people didnβt like this day lol
i can't believe this. my assumptions were what most people came up with lmaooo
and i didn't trust it to the end
i doubted myself lmao
As for me I checked everything so I was good to go
I put error handling into my code to check if the assumption ended up not holding
nice nice
time to clean up my spaghetti
and call day 8 completed with partial assistance :P
@potent ginkgo do you know if there's a "real" solution? i think i heard recursion thrown around
I think if there was an offset you could use CRT? Not entirely sure though
you could use CRT if the offset is also divisible by the length of the steps
yeah, that's what i thought too, but i'm terrible at number theory and don't know what CRT actually means nor when to use it π
or well the smallest length of the steps that isn't itself just a cycle
i have a pseudocode outline of a general solution in my head, but it's not very efficient because of just how much variation a general case could have
someone did brute force this day iirc
but by a "real" solution i meant one that doesn't make use of any janky assumptions, namely:
- for every start node, traversal falls into a cycle, in which one unique corresponding end node is found once every period
- the distance from the start node to the end node is equal to the distance from the end node to itself
13_133_452_426_987 ... upwards of trillions of iterations ... madman?
if we assume that the cycle lengths are divisible by the length of the steps, then that's honestly all we need to make it tractable imo
isn't that like 2 days straight of computation lol
GPU and parallelism go brrrr
what do you mean the length of the steps?
ahhhhh
if it's not divisible, then the cycle is far more complicated to figure out
it's all complicated π₯΄
because you have to consider multiple paths being taken since you'd end cycle at different positions in that instruction set
if it's divisible, then afaics all you need to do is run CRT over multiple congruences for every cycle containing at least one Z (one congruence per Z in cycle, moduli running over cycle lengths) and then find the least one that actually works
if the starting offsets aren't divisible, that might actually be fine now that I think about it
you'd just have to shift the input over, but that shouldn't affect much in the cycle if the cycle length is divisible
and by cycle length I mean number of steps until you return back to the node you started from, regardless of position in input
I did it!
def traverse(
instructions: str,
network: dict[str, list[str]],
*,
start: str | None = None,
) -> int:
node = (
next(node for node in network if node.endswith(
"A")) if start is None else start
)
for i, instruction in enumerate(cycle(instructions)):
if node.endswith("Z"):
return i
node = network[node][instruction == "R"]
return 0xFor paying_respect
def part1(aoc_input: tuple[str, dict[str, list[str]]]) -> int:
return traverse(*aoc_input, start="AAA")
def part2(aoc_input: tuple[str, dict[str, list[str]]]) -> int:
_, network = aoc_input
nodes = [node for node in network if node.endswith("A")]
return lcm(*(traverse(*aoc_input, start=node) for node in nodes))
return 0xFor paying_respect O.o
:D
oh it's 0xF or paying_respect π the missing space got me confused
I still don't know what that does
saw it on esopy the other day
0xF evals to true so paying_respect is never executed
so you can put in undefined variable? or am I missing something else
yea
wow
.bm crazy number theory to read later π©
does anyone know of a "correct" algorithm for today? or is the hacky LCM solution the only way to go?
# A hacky solution based on two major assumptions
# that happen to hold true for all puzzle inputs:
#
# 1. for every start node, traversal falls into
# a cycle, in which there is one unique
# corresponding end node every period;
#
# 2. the number of steps from a start node to its
# corresponding end node is equal to that from
# the end node to itself (offset = period).
#
# Then it is simply a matter of synchronizing large
# loops, i.e. using LCM.
```this is the comment i wrote at the top of my sol, i think it's pretty hacky \:P
what if the cycles encountered two or more end nodes
what if the offset was not equal to the period
what if end nodes were encountered before a cycle starts
what if some fall into dead-end cycles (i.e. no end nodes traversed in them)
what if some don't fall into cycles at all
for this one, apparently some crazy number theory does the job (i think it's the chinese remainder theorem, no idea what that is lol)
but there's still all the other general cases i listed
that are not in any way forbidden by the problem statement
AOC guarantees fair inputs.
Like. It is solvable in reasonable time with the inputs given.
So if the puzzle is. "Find when they meet" they will meet.
And no inputs like 3 42 pizza 17 π 73
Why can't I give math.lcm a list? I couldn't figure out how to get all the values from the list separated as integers so I just copy-pasted from a previous run into my code
how am I supposed to do this if it doesn't take a list?
lcm(*list)
ohhh unpacked list, thank you!
I never use unpackers in my day to day, good to know π
true, but these inputs are under restrictions not described in the puzzle statement
having the starting offset be different to the cycle length would still be solvable in reasonable time
a cycle is basically guaranteed if there is an answer
Yeah, I think that wouldhave been ok. But that's differnt than some of the paths having no conclusion
you have some edge case where they all meet up before cycling
but given the other assumptions they would have to fall into cycles
They don't have to be guaranteed to cycle back to the beginning
I'm upset about this too
Can someone here explain why the lcm works for part 2? I'm just not really sure how the lcm of the number of steps it takes each XXA node to traverse to XXZ somehow gives you the number of steps it would take if they happening simultaneously
I can see how this would work if XXZ was guaranteed to lead back to XXA when given the next instruction, but that is not mentioned anywhere.
it's been a criticism of this day in particular that that is an assumption you have to make/find and isn't mentioned in the text anywhere
Ahh, gotcha. I guess I could have tested for it, it just didn't even cross my mind when working on the solution. I guess I should have taken notice of how the pattern in the example prompt could transfer over. Thanks for the response!
that's sort of how it works: #1182550042091991180 message
Essentially, XXA and XXZ have to be on the same phase (i.e. they meet at one point after some number of steps)
you cannot just walk the actual path one by one in a reasonable time
oh you're on part1
part1 shouldn't take very long
i looked up benchmark, and i think i'm doing something seriously wrong. i'll have to check tomorrow
my part1 takes 3.6ms
Ok, I got part 1.
I see why my other code in other language didn't work: ```
Starting at AAA
I can't read
is the answer to part 2 greater than 1*e18?
it's very large, but not that large
at least for me
I had ~1e14
possible that you just had bigger values tho
so I wouldn't discount your answer based on magnitude
it doesn't look like it can fit in double π¦
p2_ans = 922657
p2_ans = 67353961
p2_ans = 3973883699
p2_ans = -91983485980753
p2_ans = 1.9135324588576046e+18
see that negative? it doesn't fit in double i think
let me test something
with the values I got with the language I'm using (G'MIC), and using math.lcm in Python, I got the right answer.
i tested with a smaller ranges of value, seems correct for 3973883699, but after lcm with using one more value breaks the double limitation
I think I know how to fit in a double. I looked for an alternative to (a*b)/gcd(a,b)
@pastel falcon as you asked for my solution: https://pastebin.com/rmYBZ3af . It's not in Python though. It works with the alternative way of finding LCM.
doubles can precisely store all the integers from -2**53 to 2**53 (~9e15) but past that it just skips a few
they can go up to a bit above 1e308
you could always try a / gcd(a, b) * b :D
that won't work. tested it. this is the only one that'll work for double case: ```
#include <bits/stdc++.h>
using namespace std;
// Function to return LCM of two numbers
int LCM(int a, int b)
{
int greater = max(a, b);
int smallest = min(a, b);
for (int i = greater; ; i += greater) {
if (i % smallest == 0)
return i;
}
}
// Driver program to test above function
int main()
{
int a = 10, b = 5;
cout << "LCM of " << a << " and "
<< b << " is " << LCM(a, b);
return 0;
}
that's weird
a / gcd(a, b) is an integer so i'd assume we wouldn't lose anything to floating point shenanigans no?
yeah, that is a integer. but the language I use (G'MIC) only supports doubles in math evaluator.
there's functions with in it own types built-in behind the scenes though. there's no LCM() built-in
I had 1.86E+13
@potent ginkgo this is where I'm at: https://github.com/shenanigansd/scratchpad/commit/70477f4f8e309634d445f265a201bbb7f00150e1
Looks like bruteforcing p2 isn't going to work
correct
how fix??
eeeeh well, how big of a hint would you like?
I'll take all the hints I can get
I don't have any idea where to start here
I'm even reading the LCM stuff but I'm not understanding where the letters are getting converted to numbers.
Letβs say we just have AAA->AAZ and BBA->BBZ
oh sorry I didn't see the mention π¦ I see AoV is responding
Itβs fine if you want to continue! I need to go eat something anyway
so the idea is that we have several starting positions. The simple case is that from all of those positions it takes the same number of steps to reach the ending position, which would mean your brute-force solution works here as you just take like 20k steps or whatever your solution to part 1 was and you're done. This is not the case so it takes different number of steps from each starting position to get to the end. Now what would be the next easiest solution? Probably a case where the cycle has no displacement from the start (cycle starts on the first step after starting position). So we can find the length of the cycle for each starting position and assume that they were chosen such that their phases will eventually cause them to meet (we assume there is a point where all of them reach the end of the cycle at the same time). What is the number of steps we need to take to reach that position? lcm(*cycle lengths)
You can treat each letters from the left as the value of their appearance. That's how I solved the problem breaking it down to only numbers.
So, in the input, NNN = 0. But you don't want to start from NNN because then you'll go into a forever loop.