#AoC 2022 | Day 14 | Solutions & Spoilers
1229 messages ยท Page 2 of 2 (latest)
(why?)
you just want to make your part 1 faster?
Yeah
did you try the thing I mentioned earlier? you don't have to simulate from the beginning every time
You basically can't do it with an algorithmic change, it's a code optimisation you need
you can backtrack one step and simulate from there
Yeah I'm trying to adopt that strategy now
i think simulating from one step above might work
no, it won't, pits can be deep
you have to backtrack as far as a pit in the middle could be
no, you only have to backtrack one step at most
nothing changed above the sand you just placed
I think he means working backwards from p2
the next sand will follow the same path up until that point

yes, working backwards from p2
I'm like 90% sure it's impossible but I don't know how to prove that
I thought @ornate forge meant that when you place a sand grain then you can start simulating from the last recorded position of that last grain
You can
I'm also sure it's impossible to work from p2
Salt's talking about something else
Whenever you successfully land a grain, you can remember the last position. The next grain is guaranteed to fall to at least there
looks pretty cool tho
You might need a stack actually
i mean line-by-line does work
welp, the line based one is faster but gives me the wrong answer
I can improve on it even further: part_one = part_two = lambda *i: 0
Now you can get the wrong answer even faster ๐
I bet it will be even faster ported to rust
True
ported to nim
#include <sys/syscall.h>
.globl _start
_start:
mov $SYS_exit,%eax
xor %edi,%edi
syscall
Fastest version
This looks like Powder Toy
Fair
they're really fun to implement though
i create a separate task for each particle in my implementation --- but the particles can put themselves to sleep if they don't change
Presumably you have some kind of particle update system, like Minecraft block updates?
i dunno how minecraft updates
I'm not too familiar with the internals but I know that most updates are done via block updates
I.E. when a block changes (e.g. being placed/destroyed) it tells its neighbours to update
(e.g. to break or fall)
like when there's unsuported sand from world gen but it doesn't fall until you update it
Or the entire category of contraption that is block update detectors
still slightly mad they just turned that into a block, BUDs were cool
oh, particles will wake up neighbors when they update -- in case the neighbors were sleeping -- but other than that tasks are just executed in whatever order asyncio decides
Yeah that's what I meant
is it open source so we can check it out? 
This is also what I did for part 2 and it works great
neat
neat, I ported my rust code to python and it's instant on part 1
ignoring the parsing it's <1ms
with parsing and everything 3.5ms
0.023s for part 2
faster than I expected from python 
I did it faster ๐
But I suck in the first part, I probably did backtracking the wrong way
I just kept a list with the positions that the grains had. I append a position when I move the grain and I pop a position when I place a grain
yes
Yeah i feel you lol
I did that but it's still pretty slow, it takes 0.03s
res = 0
stack = [(500, 0)]
while stack:
x, y = stack[-1]
if not grid[y + 1][x]:
stack.append((x, y + 1))
elif not grid[y + 1][x - 1]:
stack.append((x - 1, y + 1))
elif not grid[y + 1][x + 1]:
stack.append((x + 1, y + 1))
else:
res += 1
grid[y][x] = True
stack.pop()
part 2 takes 0.005
wait
this is part 2
let me fix
there
res = 0
stack = [(500, 0)]
while True:
x, y = stack[-1]
if y >= max_y - 1:
break
if not grid[y + 1][x]:
stack.append((x, y + 1))
elif not grid[y + 1][x - 1]:
stack.append((x - 1, y + 1))
elif not grid[y + 1][x + 1]:
stack.append((x + 1, y + 1))
else:
res += 1
grid[y][x] = True
stack.pop()
where grid is a 1000 x something grid of booleans
True if occupied
the nice thing about this code is that the same code with a very small change solves both quickly
def main(input):
cave, max = parse_cave(input)
max = max + 2
step=1
count=0
current:set[int]={500}
while step<=max:
attempt:set[int]=set()
count+=len(current)
for x in current:
attempt.add(x)
attempt.add(x-1)
attempt.add(x+1)
if step in cave:
attempt.difference_update(cave[step])
current=attempt
step+=1
return count```
This is the main for my part 2. parse_cave gives the y of the lowest rock, and it gives a dict which associates a y with the various xs that are occupied
cave:dict[int,list[int]]
cave is something like {6:[496,497,498,502]}
the set stuff would look so neat with C++ bitsets
attempt = current << 1 | current | current >> 1
I really like this solution, I think it's a smart algo
Alex: [ 0.003729 ] [ 0.005533 ]
Winners: [ Alex ] [ Alex ]```
I'm finally satisfied with the times I get
I can go to sleep now
Parsing is included
parsing is probably the majority of time on p1
Yeah, also my parsing function looks like shit
parsing in a nutshell
lol
I cheat and use my util to read all ints from the input
makes life a bunch easier
or, all ints in a line of input
newlines actually matter a lot here
[y,x][bool(x)]
lol
I suppose if you want to allow other False values you'd need the is not None in there.
ok, finally got a good impl of the lines based part 2 in rust
x = 0
[y,x][x is not None]
You're already optimizing stuff for tonight? You're way ahead of the game. ๐
do you guys deal with resizing the grid or just start out with a huge map
why even have a grid
neither, just populated a set
set gang
board = set()
for row in [[eval(s) for s in line.split(' -> ')] for line in aocdata.splitlines()]:
for (x,y),(i,j) in zip(row,row[1:]):
board.update((a,y) for a in range(x,i+1) or range(i+1,x))
board.update((x,b) for b in range(j,y+1) or range(y,j+1))
probably
board = {(l,m)
for row in [[eval(s) for s in line.split(' -> ')] for line in aocdata.splitlines()]
for (x,y),(i,j) in zip(row,row[1:])
for l,m in {(a,y) for a in range(x,i+1) or range(i+1,x)}|{(x,b) for b in range(j,y+1) or range(y,j+1)}}
jesus that's a lot of comprehension in one line
but it works... ๐
that's actually really smart
let's steal it for golf ๐
note my actual code has the update() version, not the single comprehension.
I usually go for a long comprehension if I see it but I couldn't see a way to do the ranges without if statements
yeah, finding range() or range() worked was a feels good moment
the rest of my code was a bit more boring I'm afraid.
# drop some sand
for c in range(H*R):
a,b = (500,0)
while (a,b) not in board:
if (a, b+1) not in board: a,b = (a, b+1)
elif (a-1,b+1) not in board: a,b = (a-1,b+1)
elif (a+1,b+1) not in board: a,b = (a+1,b+1)
else: board.add((a,b))
if b>=H:
print(c);H*=H
if b==0:
print(c+1);
break
i wonder how feasible it would be for python to use bitwise nots as a set complement operator
maybe there's enough fors there that we can finally implement the eval("".replace('@',' for ')) optimization
obviously you wouldn't be able to iterate over it
but membership, union, intersect, etc would all be possible
what would be the universal set tho
๐ค
complement of empty?
_ in ~{} would always be true
or wait that's a dict
but you get the point
maybe i'll do something like this in my own lang
Is there a cool way to use match/case here? I tried it and it ended up just being a slow if/elif block. But maybe I missed something match can do?
while True:
sand = (500,0)
while True:
match sand:
case (a,b) if (a,b+1) not in board:
sand = (a,b+1)
case (a,b) if (a-1,b+1) not in board:
sand = (a-1,b+1)
case (a,b) if (a+1,b+1) not in board:
sand = (a+1,b+1)
case _:
c+=1
board.add(sand)
break
?
actually maybe it's a mobile thing
the indentation isn't the question.
looks like the while was indented
Apparently if you're using if in a match you're already doing it wrong ๐
it works, it's just slower than if/elif
agreed
there's no point if you're just going to use conditionals anyways
which is why I swapped to if/elif, the question remains though, is there something match provides that would be a good choice here?
not really
conditionals aren't bad, but if every case is a conditional you haven't gained anything.
for else ๐
do it to flex on the <= 3.9 nerds
Nothing I'm aware of. patma is more about the shape of things than literal values.
I had a cool next(s + d for d in (1j,-1+1j, 1+1j) if s + d not in block_set) approach but I had to scrap that because next() was too slow
#
a, b = sand
for p in ((a, b+1), (a-1, b+1), (a+1, b+1):
if p not in board:
sand = p
break
else:
c+=1
board.add(sand)
break
surprising that it was too slow.
I wonder if a queue approach that just flood fills might be better? for any sand you always add the 3 beneath it if they're not already there.
that's basically the line based thing people have discussed
take that further and add the whole triangle
cavity
I'm curious, did you all do anything smart except simulating it out? I can see a way of doing part 2 by just looking at the triangle underneath the start point and just counting how many spots would be filled
but my brute force is like 4.4 seconds on CPython and 1 second on pypy so didn't bother ๐
yup, salt-die did that
@brazen sorrel
... and I just realize that the board is not big enough to warrant me not just creating a grid which would probably make this all a lot faster
basically DFS I am assuming?
lmao now that I say it out loud the problem even hinted at pathfinding
goddammit lol
if you store the path the sand falls down you can just go 1 step up after setting the sand down
yeah it basically just seems like a DFS
you're describing the "backtracking" part
but yeah I guess you can structure it more nicely because of the restriction, I see what you're saying
cool, not gonna bother implementing it klol
yeah i feel like so far this years aoc hasnt been as harsh on naive solutions as it has been in the past
Yeah mostly been able to brute force
With the exception of that LCM the other day but pretty trivial change
haha definitely trivial (it did not take me two hours)
i think my solution for part 2 is correct but its incredibly inefficient for the large input, it was running for about half an hour and still didnt return
# counts rest sand units until overflow, with infinitely wider ground
def simulate_p2(self):
sand_units = 0
while True: # void
curr_pos = self.start
self.sand.append(curr_pos)
left = (curr_pos[0] - 1, curr_pos[1] + 1)
middle = (curr_pos[0], curr_pos[1] + 1)
right = (curr_pos[0] + 1, curr_pos[1] + 1)
if all(x in self.sand for x in [left,middle,right]): # overflow
return sand_units + 1
while True:
for dx, dy in self.directions:
next_pos = (curr_pos[0] + dx, curr_pos[1] + dy)
if not any(next_pos in pos for pos in [self.rocks, self.sand]):
curr_pos = next_pos
self.sand[sand_units] = curr_pos
break
else: # rest
self.sand[sand_units] = curr_pos
sand_units += 1
break
if (curr_pos[1] >= self.height): # floor
self.sand[sand_units] = curr_pos
sand_units += 1
break
# self.visualize()
# sleep(0.1)
;-;
Don't use a list
At least use a set
Using a set would probably optimise your code enough, it gets rid of a linear search you're seeing every single simulation step
My part 1 was initially incredibly wasteful as I simulated each grain of sand starting from the source each time
Thank god I fixed that before moving to Part 2 or I would have been super screwed for that
as it was, my part 2 does spit out a result ... in 3 minutes XD
รฝeah i figured that, and removed some for loops, takes a bit but much better, thanks!
at least im not alone xD
starting now day 15, and im scared
ahahahahhaha same
i did a visualizer but it would take days for it to render on the actual input
but works well with the example
nice
ive been trying to keep up too
i do have a print out of the actual input with the floor
takes a good few minutes to spit out but it's there
my "floor" isnt there in the actual print out but it is in the logic, so it works
this is the bottom part of my input with the floor
i used a scroll view to show everything in the terminal
cause it was O(n^2) for each frame
oh, i had to slow down the animation on purpose
jeez thats awesome
thanks
I've got part A done, and my tests are passing for part B...
well, test
but I'm over shooting on the final answer :/
my modification to how i'm generating the rock space doesn't seem to change anything on the small scale
i am perplexed...
is there somewhere people share extra test inputs?
well, you are discouraged to share your inputs
:nods: I was wondering if someone had made extra test cases
ok, i guess I was counting the final sand glob in a way that overshoots in the real case, but not the test case
oh, no, i wasn't, i just have shit eyes
How did you capture the animation?
I just printed to the terminal, which needs to run quite slowly to avoid excessive flicker, but it's rather meditative to watch ๐
i don't clear screen between frames, i move the cursor to a spot that needs to change and change it
How do u do that kind of animation without clearing the screen?
There are libraries that'll do that kind of thing like curses, but any library that does that relies on ANSI escape codes, which are special characters that when printed to the terminal, most modern terminals will interpret as things like color text, cursor movement, or changing the cursor style (flashing, invisible, etc).