DEV Community

Cover image for Advent of Code 2020 Solution Megathread - Day 17: Conway Cubes
Ryan Palo
Ryan Palo

Posted on

Advent of Code 2020 Solution Megathread - Day 17: Conway Cubes

Yesterday was a big one! I ended up just writing and writing and writing and looping and looping and looping. And, if I'm being totally honest and transparent, my original solution before refactoring had a goto in it. Not great. But it came out pretty good after some refactoring and pulling logic out into functions.

The Puzzle

In today’s puzzle, we're back to Conway. This time in the third dimension. We've got cubes that turn on and off depending on how many of their 26 neighbors are also active. And the rules for transition are pretty low, so I think this is going to be one blinky cube!

The Leaderboards

As always, this is the spot where I’ll plug any leaderboard codes shared from the community.

Ryan's Leaderboard: 224198-25048a19 
Enter fullscreen mode Exit fullscreen mode

If you want to generate your own leaderboard and signal boost it a little bit, send it to me either in a DEV message or in a comment on one of these posts and I'll add it to the list above.

Yesterday’s Languages

Updated 02:04PM 12/17/2020 PST.

Language Count
Haskell 2
JavaScript 2
Rust 2
C 1
Ruby 1

Merry Coding!

Top comments (10)

Collapse
 
shalvah profile image
Shalvah • Edited

Ruby solution. It's an eternity of eachs (Ruby's for loop), but it runs in a few seconds.

Today's main problem was the example. Instructions were pretty clear, but the example had me thinking I misunderstood. Had to check Reddit, where a lot of other folks were confused too, and realised I was correct, the example had just been cropped weirdly.

$actives = {0 => {0 => {}}} x_bounds = {max: 0, min: 0} y_bounds = {max: 0, min: 0} z_bounds = {max: 0, min: 0} w_bounds = {max: 0, min: 0} File.readlines("input.txt").each_with_index do |line, y| line.each_char.with_index do |char, x| if char == "#" $actives[0][0][y] ||= {} $actives[0][0][y][x] = true x_bounds[:max] = x if x > x_bounds[:max] x_bounds[:min] = x if x < x_bounds[:min] end end y_bounds[:max] = y if y > y_bounds[:max] y_bounds[:min] = y if y < y_bounds[:min] end def get_cube_status(x, y, z, w) if ($actives[w][z][y][x] rescue false) == true :active else :inactive end end def get_active_neighbours(x, y, z, w) active_neighbours = [] (w - 1..w + 1).each do |current_w| (z - 1..z + 1).each do |current_z| (y - 1..y + 1).each do |current_y| (x - 1..x + 1).each do |current_x| next if [x, y, z, w] == [current_x, current_y, current_z, current_w] if get_cube_status(current_x, current_y, current_z, current_w) == :active active_neighbours << [current_x, current_y, current_z, current_w] end end end end end active_neighbours end 6.times do w_bounds[:max] += 1 w_bounds[:min] -= 1 z_bounds[:max] += 1 z_bounds[:min] -= 1 x_bounds[:max] += 1 x_bounds[:min] -= 1 y_bounds[:max] += 1 y_bounds[:min] -= 1 next_state = Marshal.load(Marshal.dump($actives)) (w_bounds[:min]..w_bounds[:max]).each do |w| (z_bounds[:min]..z_bounds[:max]).each do |z| (y_bounds[:min]..y_bounds[:max]).each do |y| (x_bounds[:min]..x_bounds[:max]).each do |x| cube_status = get_cube_status(x, y, z, w) active_neighbours = get_active_neighbours(x, y, z, w) case cube_status when :active next_state[w][z][y][x] = nil unless (active_neighbours.size == 2 || active_neighbours.size == 3) when :inactive if active_neighbours.size == 3 next_state[w] ||= {} next_state[w][z] ||= {} next_state[w][z][y] ||= {} next_state[w][z][y][x] = true end end end end end end $actives = next_state end def count_active(actives, x_bounds, y_bounds, z_bounds, w_bounds) active_count = 0 (w_bounds[:min]..w_bounds[:max]).each do |w| (z_bounds[:min]..z_bounds[:max]).each do |z| (y_bounds[:min]..y_bounds[:max]).each do |y| (x_bounds[:min]..x_bounds[:max]).each do |x| active = (actives[w][z][y][x] rescue false) == true active_count += 1 if active end end end end active_count end # Helper function for when you need to visualise the layout def print_grid(actives, x_bounds, y_bounds, z_bounds, w_bounds) (w_bounds[:min]..w_bounds[:max]).each do |w| (z_bounds[:min]..z_bounds[:max]).each do |z| print "z = #{z}, w = #{w}\n" (y_bounds[:min]..y_bounds[:max]).each do |y| (x_bounds[:min]..x_bounds[:max]).each do |x| active = (actives[z][y][x] rescue false) == true print(active ? '#' : '.') end print "\n" end print "\n" end print "\n\n" end end # print_grid($actives, x_bounds, y_bounds, z_bounds, w_bounds) p count_active($actives, x_bounds, y_bounds, z_bounds, w_bounds) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rpalo profile image
Ryan Palo

I did mine in Python. Not enough brain this week to write all those loops in C. Part 2 isn't very exciting because it's exactly the same thing with one layer more of nested looping for everything. I had to just turn my brain off and write the loops, or my brain would have eeped out of my ear.

from copy import deepcopy class Grid3D: def __init__(self, start_width, cycles): self.grid = [] self.total_width = start_width + 2 * cycles self.total_depth = 1 + 2 * cycles self.load_start = self.total_width // 2 - start_width // 2 self.cycles = cycles for z in range(self.total_depth): self.grid.append([]) for y in range(self.total_width): self.grid[-1].append([]) for x in range(self.total_width): self.grid[-1][-1].append(0) def load(self, plane): for y, row in enumerate(plane, start=self.load_start): for x, value in enumerate(row, start=self.load_start): if value == "#": self.grid[self.load_start][y][x] = 1 def tick(self): next_grid = deepcopy(self.grid) for z, plane in enumerate(self.grid): for y, row in enumerate(plane): for x, value in enumerate(row): count = self.count_neighbors(x, y, z) if value == 1 and count not in [2, 3]: next_grid[z][y][x] = 0 elif value == 0 and count == 3: next_grid[z][y][x] = 1 self.grid = next_grid def tick_all(self): for t in range(self.cycles): self.tick() def count_neighbors(self, x, y, z): total = 0 for zi in range(max(z - 1, 0), min(z + 2, self.total_depth)): for yi in range(max(y - 1, 0), min(y + 2, self.total_width)): for xi in range(max(x - 1, 0), min(x + 2, self.total_width)): if x == xi and y == yi and z == zi: continue total += self.grid[zi][yi][xi] return total def count_alive(self): return sum(cell for plane in self.grid for row in plane for cell in row) def part1(plane): grid = Grid3D(len(plane), 6) grid.load(plane) grid.tick_all() print(grid.count_alive()) class Grid4D: def __init__(self, start_width, cycles): self.grid = [] self.total_width = start_width + 2 * cycles self.total_depth = 1 + 2 * cycles self.load_start = self.total_width // 2 - start_width // 2 self.cycles = cycles for w in range(self.total_depth): self.grid.append([]) for z in range(self.total_depth): self.grid[-1].append([]) for y in range(self.total_width): self.grid[-1][-1].append([]) for x in range(self.total_width): self.grid[-1][-1][-1].append(0) def load(self, plane): for y, row in enumerate(plane, start=self.load_start): for x, value in enumerate(row, start=self.load_start): if value == "#": self.grid[self.load_start][self.load_start][y][x] = 1 def tick(self): next_grid = deepcopy(self.grid) for w, cube in enumerate(self.grid): for z, plane in enumerate(cube): for y, row in enumerate(plane): for x, value in enumerate(row): count = self.count_neighbors(x, y, z, w) if value == 1 and count not in [2, 3]: next_grid[w][z][y][x] = 0 elif value == 0 and count == 3: next_grid[w][z][y][x] = 1 self.grid = next_grid def tick_all(self): for t in range(self.cycles): self.tick() def count_neighbors(self, x, y, z, w): total = 0 for wi in range(max(w - 1, 0), min(w + 2, self.total_depth)): for zi in range(max(z - 1, 0), min(z + 2, self.total_depth)): for yi in range(max(y - 1, 0), min(y + 2, self.total_width)): for xi in range(max(x - 1, 0), min(x + 2, self.total_width)): if x == xi and y == yi and z == zi and wi == w: continue total += self.grid[wi][zi][yi][xi] return total def count_alive(self): return sum(cell for cube in self.grid for plane in cube for row in plane for cell in row) def part2(plane): grid = Grid4D(len(plane), 6) grid.load(plane) grid.tick_all() print(grid.count_alive()) if __name__ == "__main__": with open("data/day17.txt") as f: plane = f.read().splitlines() part1(plane) part2(plane) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bgaster profile image
Benedict Gaster

Tried for ages to get a generic solution for an dim size, but in the end failed in the most part! Anywhy here's a Haskell soution:

type Grid a = S.Set a class Ord a => GridN a where neighbours :: S.Set a -> M.Map a Int instance GridN (Int,Int,Int) where neighbours s = let m = M.fromSet (const 1) s idxs = Prelude.filter (\(x,y,z) -> x /= 0 || y /= 0 || z /= 0) $ [(-1,,), (0,,), (1,,)] <*> [-1, 0, 1] <*> [-1, 0, 1] keys = map (\(dx,dy,dz) -> M.mapKeys (\(x,y,z) -> (x+dx, y+dy, z+dz)) m) idxs in M.unionsWith (+) keys instance GridN (Int,Int,Int,Int) where neighbours s = let m = M.fromSet (const 1) s idxs = Prelude.filter (\(x,y,z,w) -> x /= 0 || y /= 0 || z /= 0 || w /= 0) $ ((,,,) <$> [-1, 0, 1]) <*> [-1, 0, 1] <*> [-1, 0, 1] <*> [-1, 0, 1] keys = map (\(dx,dy,dz,dw) -> M.mapKeys (\(x,y,z,w) -> (x+dx, y+dy, z+dz, w+dw)) m) idxs in M.unionsWith (+) keys parse :: String -> Grid (Int,Int,Int) parse = parse' 0 0 S.empty where parse' x y s (c:cs) = case c of '#' -> parse' (x+1) y (S.insert (x, y, 0::Int) s) cs '.' -> parse' (x+1) y s cs '\n' -> parse' 0 (y+1) s cs parse' _ _ s [] = s step nbours s = let ns = nbours s in S.union (M.keysSet $ M.filter (`elem` [2, 3]) (ns `M.restrictKeys` s)) (M.keysSet $ M.filter (== 3) (ns `M.withoutKeys` s)) task1 :: Grid (Int,Int,Int) -> Int task1 = S.size . (!! 6) . iterate (step neighbours) task2 :: Grid (Int,Int,Int,Int) -> Int task2 = S.size . (!! 6) . iterate (step neighbours) main = do is <- readFile "day17_input" <&> parse print (task1 is) print (task2 $ S.map (\(x, y, z) -> (x, y, z, 0)) is) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
choroba profile image
E. Choroba

It's funny how (at least some of) the solutions are similar to each other, even if written in different languages. Here's my Perl solution:

#!/usr/bin/perl use warnings; use strict; use feature qw{ say }; use ARGV::OrDATA; { package Grid; use Moo; has grid => (is => 'ro'); has width => (is => 'lazy'); has height => (is => 'lazy'); has depth => (is => 'lazy'); has time => (is => 'lazy'); has _neighbours => (is => 'lazy'); sub at { my ($self, $x, $y, $z, $w) = @_; return 0 if $x < 0 || $y < 0 || $z < 0 || $w < 0; return ($self->grid->[$w] && $self->grid->[$w][$z] && $self->grid->[$w][$z][$y] && $self->grid->[$w][$z][$y][$x]) ? 1 : 0 } sub iter { my ($self, $code, $m) = @_; $m //= 0; for my $w (0 .. $self->time - 1 + $m) { for my $z (0 .. $self->depth - 1 + $m) { for my $y (0 .. $self->height - 1 + $m) { for my $x (0 .. $self->width - 1 + $m) { $code->($x, $y, $z, $w); } } } } } sub count { my ($self) = @_; my $c = 0; $self->iter(sub { my ($x, $y, $z, $w) = @_; ++$c if $self->at($x, $y, $z, $w); }); return $c } sub neighbours { my ($self, $x, $y, $z, $w) = @_; $self->_neighbours->[ $w + 1 ][ $z + 1 ][ $y + 1 ][ $x + 1 ] } sub next { my ($self) = @_; my @next; $self->iter(sub { my ($x, $y, $z, $w) = @_; my $neighbours = $self->neighbours($x - 1, $y - 1, $z - 1, $w - 1); my $is_active = $self->at($x - 1, $y - 1, $z - 1, $w - 1); $next[$w][$z][$y][$x] = $is_active ? ($neighbours == 2 || $neighbours == 3) : ($neighbours == 3); }, 2); return \@next } sub _build_time { scalar @{ $_[0]->grid } } sub _build_depth { scalar @{ $_[0]->grid->[0] } } sub _build_height { scalar @{ $_[0]->grid->[0][0] } } sub _build_width { scalar @{ $_[0]->grid->[0][0][0] } } sub _build__neighbours { my ($self) = @_; my @n; $self->iter(sub { my ($x, $y, $z, $w) = @_; for my $i (-1 .. 1) { for my $j (-1 .. 1) { for my $k (-1 .. 1) { for my $l (-1 .. 1) { next unless $i || $j || $k || $l; $n[ $w + $l + 1 ] [ $z + $i + 1 ] [ $y + $j + 1 ] [ $x + $k + 1 ] += $self->at($x, $y, $z, $w); } } } } }); return \@n } } my @in; while (<>) { chomp; push @in, [map $_ eq '#', split //]; } my $grid = 'Grid'->new(grid => [[\@in]]); for (1 .. 6) { $grid = 'Grid'->new(grid => $grid->next); } say $grid->count; __DATA__ .#. ..# ### 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
cappe987 profile image
Casper

My Haskell solution. I created a matrix the size of the input + 7 in each direction and dimension. So the input matrix is in the very middle. Since on 6 cycles it can only grow a maximum of 6 in each direction, I chose 7 because then I can get away with not doing bounds checking. I'm not looping over the outer layer anyways. I also got to use mutable arrays in Haskell, which I recently learned how to use. The part 2 solution was just a copy-paste and adding an extra coordinate. Although I could just have made part 1 take a fourth coordinate but keep it at 0. I also learned that Haskell arrays can handle negative indices just fine, so it was easy to just set the input array at index 0 and then just set the array to be to -7.

import Data.List import Data.Array import Data.Array.MArray import Data.Array.IO import Data.Foldable import Control.Monad type Point = (Int, Int, Int) sumTuple :: Point -> Point -> Point sumTuple (z1,y1,x1) (z2,y2,x2) = (z1+z2, y1+y2, x1+x2) dirs :: [Point] dirs = [(z,y,x) | z <- [-1..1], y <- [-1..1], x <- [-1..1], (z,y,x) /= (0,0,0)] neighbours :: Point -> [Point] neighbours p = map (sumTuple p) dirs count :: Char -> String -> Int count x = length . filter (==x) newState :: Array Point Char -> Point -> Maybe Char newState arr pos = let alive = count '#' $ map (arr !) $ neighbours pos curr = arr ! pos in if curr == '#' && (alive > 3 || alive < 2) then Just '.' else if curr == '.' && alive == 3 then Just '#' else Nothing update :: Array Point Char -> IOUArray Point Char -> Point -> IO () update immutarr mutarr p = forM_ (newState immutarr p) (writeArray mutarr p) pointsBetween :: Point -> Point -> [Point] pointsBetween (lz,ly,lx) (uz,uy,ux) = [(z,y,x) | z <- [lz..uz], y <- [ly..uy], x <- [lx..ux]] generation :: IOUArray Point Char -> IO () generation mutarr = do immutarr <- freeze mutarr let (l,u) = bounds immutarr mapM_ (update immutarr mutarr) $ pointsBetween (sumTuple (1,1,1) l) (sumTuple (-1,-1,-1) u) countAlive :: Array Point Char -> Int countAlive immutarr = let (l,u) = bounds immutarr in foldl (\acc p -> if immutarr ! p == '#' then acc+1 else acc) 0 $ pointsBetween (sumTuple (1,1,1) l) (sumTuple (-1,-1,-1) u) main :: IO () main = do input <- lines <$> readFile "input.txt" -- Part 1: 448 let c = 7 width = length (head input) + 1 height = length input + 1 -- Using (z,y,x) format since that makes it print correctly mutarr <- newArray ((-c,-c,-c), (c,height+c,width+c)) '.' :: IO (IOUArray Point Char) let init = [(e, (x,y)) | (y,es) <- zip [0..] input, (x,e) <- zip [0..] (input !! y)] mapM_ (\(e, (x,y)) -> writeArray mutarr (0,y, x) e) init replicateM_ 6 (generation mutarr) immutarr <- freeze mutarr :: IO (Array Point Char) -- print immutarr print $ countAlive immutarr 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
neilgall profile image
Neil Gall

With hindsight, a more dynamic approach of using a variable-length vector for a coordinate would have been better, but it was a chance to play with the Rust type system if nothing else.

use std::cmp::{max, min}; use std::collections::HashMap; use std::fmt; use std::hash::Hash; use std::ops::{AddAssign, RangeInclusive}; // --- model #[derive(Eq, PartialEq, Copy, Clone)] enum Cube { Inactive, Active } impl From<char> for Cube { fn from(c: char) -> Self { match c { '#' => Cube::Active, _ => Cube::Inactive } } } trait Position: Eq + Hash { fn neighbours(&self) -> Box<dyn Iterator<Item = Self> + '_>; } #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] struct Pos3(i64, i64, i64); #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] struct Pos4(i64, i64, i64, i64); impl Position for Pos3 { fn neighbours(&self) -> Box<dyn Iterator<Item = Self> + '_> { let it = (-1..=1).flat_map( move |z| (-1..=1).flat_map( move |y| (-1..=1).map( move |x| Pos3(self.0+x, self.1+y, self.2+z) ) ) ).filter(move |p| p != self); Box::new(it) } } impl Position for Pos4 { fn neighbours(&self) -> Box<dyn Iterator<Item = Self> + '_> { let it = (-1..=1).flat_map( move |w| (-1..=1).flat_map( move |z| (-1..=1).flat_map( move |y| (-1..=1).map( move |x| Pos4(self.0+x, self.1+y, self.2+z, self.3+w) ) ) ) ).filter(move |p| p != self); Box::new(it) } } struct Bounds3 { x: RangeInclusive<i64>, y: RangeInclusive<i64>, z: RangeInclusive<i64> } struct Bounds4 { x: RangeInclusive<i64>, y: RangeInclusive<i64>, z: RangeInclusive<i64>, w: RangeInclusive<i64> } impl Default for Bounds3 { fn default() -> Self { Bounds3 { x: 0..=0, y: 0..=0, z: 0..=0 } } } impl Default for Bounds4 { fn default() -> Self { Bounds4 { x: 0..=0, y: 0..=0, z: 0..=0, w: 0..=0 } } } impl AddAssign<Pos3> for Bounds3 { fn add_assign(&mut self, pos: Pos3) { self.x = min(*self.x.start(), pos.0) ..= max(*self.x.end(), pos.0); self.y = min(*self.y.start(), pos.1) ..= max(*self.y.end(), pos.1); self.z = min(*self.z.start(), pos.2) ..= max(*self.z.end(), pos.2); } } impl AddAssign<Pos4> for Bounds4 { fn add_assign(&mut self, pos: Pos4) { self.x = min(*self.x.start(), pos.0) ..= max(*self.x.end(), pos.0); self.y = min(*self.y.start(), pos.1) ..= max(*self.y.end(), pos.1); self.z = min(*self.z.start(), pos.2) ..= max(*self.z.end(), pos.2); self.w = min(*self.w.start(), pos.3) ..= max(*self.w.end(), pos.3); } } trait Dimension<Pos: Position + Copy> where Self: Sized { fn grid(&self) -> &HashMap<Pos, Cube>; fn iter(&self) -> Box<dyn Iterator<Item = Pos> + '_>; fn at(&self, p: &Pos) -> &Cube; fn next_generation(&self) -> Self; fn occupied_neighbours(&self, p: &Pos) -> usize { p.neighbours() .filter(|p| self.at(p) == &Cube::Active ).count() } fn bounds<Bounds: Default + AddAssign<Pos>>(&self) -> Bounds { let mut bounds = Bounds::default(); for pos in self.grid().keys() { bounds += *pos; } bounds } fn active_cubes(&self) -> usize { self.grid().values().filter(|c| *c == &Cube::Active).count() } fn next_generation_grid(&self) -> HashMap<Pos, Cube> { self.iter().map(|pos| { let occupied = self.occupied_neighbours(&pos); let new_state = match self.at(&pos) { Cube::Active => if occupied == 2 || occupied == 3 { Cube::Active } else { Cube::Inactive } Cube::Inactive => if occupied == 3 { Cube::Active } else { Cube::Inactive } }; (pos, new_state) }).collect() } } #[derive(Clone)] struct PocketDimension<Pos: Position> { grid: HashMap<Pos, Cube> } impl PartialEq for PocketDimension<Pos3> { fn eq(&self, other: &Self) -> bool { let mut bounds: Bounds3 = self.bounds(); for pos in other.iter() { bounds += pos; } for z in bounds.z { for y in (&bounds.y).clone() { for x in (&bounds.x).clone() { let pos = Pos3(x, y, z); if self.at(&pos) != other.at(&pos) { return false; } } } } true } } impl PartialEq for PocketDimension<Pos4> { fn eq(&self, other: &Self) -> bool { let mut bounds: Bounds4 = self.bounds(); for pos in other.iter() { bounds += pos; } for w in bounds.w { for z in bounds.z.clone() { for y in (&bounds.y).clone() { for x in (&bounds.x).clone() { let pos = Pos4(x, y, z, w); if self.at(&pos) != other.at(&pos) { return false; } } } } } true } } impl PocketDimension<Pos3> { fn new3(origin: &Pos3, s: &str) -> Self { let mut grid = HashMap::new(); for (z, zs) in s.split("\n\n").enumerate() { for (y, ys) in zs.lines().enumerate() { for (x, xs) in ys.trim().chars().enumerate() { grid.insert(Pos3(origin.0 + x as i64, origin.1 + y as i64, origin.2 + z as i64), Cube::from(xs)); } } } PocketDimension { grid } } } impl Dimension<Pos3> for PocketDimension<Pos3> { fn grid(&self) -> &HashMap<Pos3, Cube> { &self.grid } fn at(&self, p: &Pos3) -> &Cube { self.grid.get(p).unwrap_or(&Cube::Inactive) } fn iter(&self) -> Box<dyn Iterator<Item = Pos3> + '_> { let bounds: Bounds3 = self.bounds(); let (xmin, xmax) = (*bounds.x.start() - 1, *bounds.x.end() + 1); let (ymin, ymax) = (*bounds.y.start() - 1, *bounds.y.end() + 1); let (zmin, zmax) = (*bounds.z.start() - 1, *bounds.z.end() + 1); let it = (zmin..=zmax).flat_map(move |z| (ymin..=ymax).flat_map(move |y| (xmin..=xmax).map(move |x| Pos3(x, y, z) ) ) ); Box::new(it) } fn next_generation(&self) -> Self { PocketDimension { grid: self.next_generation_grid() } } } impl PocketDimension<Pos4> { fn new4(origin: &Pos4, s: &str) -> Self { let mut grid = HashMap::new(); for (z, zs) in s.split("\n\n").enumerate() { for (y, ys) in zs.lines().enumerate() { for (x, xs) in ys.trim().chars().enumerate() { grid.insert(Pos4(origin.0 + x as i64, origin.1 + y as i64, origin.2 + z as i64, 0), Cube::from(xs)); } } } PocketDimension { grid } } } impl Dimension<Pos4> for PocketDimension<Pos4> { fn grid(&self) -> &HashMap<Pos4, Cube> { &self.grid } fn at(&self, p: &Pos4) -> &Cube { self.grid.get(p).unwrap_or(&Cube::Inactive) } fn iter(&self) -> Box<dyn Iterator<Item = Pos4> + '_> { let bounds: Bounds4 = self.bounds(); let (xmin, xmax) = (*bounds.x.start() - 1, *bounds.x.end() + 1); let (ymin, ymax) = (*bounds.y.start() - 1, *bounds.y.end() + 1); let (zmin, zmax) = (*bounds.z.start() - 1, *bounds.z.end() + 1); let (wmin, wmax) = (*bounds.w.start() - 1, *bounds.w.end() + 1); let it = (wmin..=wmax).flat_map(move |w| (zmin..=zmax).flat_map(move |z| (ymin..=ymax).flat_map(move |y| (xmin..=xmax).map(move |x| Pos4(x, y, z, w) ) ) ) ); Box::new(it) } fn next_generation(&self) -> Self { PocketDimension { grid: self.next_generation_grid() } } } impl fmt::Debug for Cube { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Cube::Inactive => write!(f, "."), Cube::Active => write!(f, "#") } } } impl fmt::Debug for PocketDimension<Pos3> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bounds: Bounds3 = self.bounds(); write!(f, "zs={:?} ys={:?} xs={:?}\n", bounds.z, bounds.y, bounds.x)?; for z in bounds.z { write!(f, "z={:?}\n", z)?; for y in (&bounds.y).clone() { for x in (&bounds.x).clone() { write!(f, "{:?}", self.at(&Pos3(x,y,z)))?; } write!(f, " {}\n", y)?; } } Ok(()) } } // --- problems fn part1(input: &str) -> usize { let mut p = PocketDimension::new3(&Pos3(0,0,0), input); for _ in 0..6 { p = p.next_generation(); } p.active_cubes() } fn part2(input: &str) -> usize { let mut p = PocketDimension::new4(&Pos4(0,0,0,0), input); for _ in 0..6 { p = p.next_generation(); } p.active_cubes() } fn main() { let input = std::fs::read_to_string("./input.txt").unwrap(); println!("part1 {:?}", part1(&input)); println!("part2 {:?}", part2(&input)); } #[cfg(test)] mod tests { use super::*; fn test_grid() -> &'static str { ".#. ..# ###" } #[test] fn test_init() { let pd = PocketDimension::new3(&Pos3(0,0,0), test_grid()); assert_eq!(pd.at(&Pos3(0,0,0)), &Cube::Inactive); assert_eq!(pd.at(&Pos3(1,0,0)), &Cube::Active); assert_eq!(pd.at(&Pos3(3,6,9)), &Cube::Inactive); assert_eq!(pd.at(&Pos3(2,1,0)), &Cube::Active); } #[test] fn test_neighbours_3d() { assert_eq!(Pos3(0,0,0).neighbours().count(), 26); } #[test] fn test_neighbours_4d() { assert_eq!(Pos4(0,0,0,0).neighbours().count(), 80); } #[test] fn test_occupied_neighbours() { let pd = PocketDimension::new3(&Pos3(0,0,0), test_grid()); assert_eq!(pd.occupied_neighbours(&Pos3(0,0,0)), 1); assert_eq!(pd.occupied_neighbours(&Pos3(1,2,0)), 3); } #[test] fn test_generations() { let pd = PocketDimension::new3(&Pos3(0,0,0), test_grid()); let gen1 = pd.next_generation(); assert_eq!(gen1, PocketDimension::new3(&Pos3(0,1,-1), "#.. ..# .#. #.# .## .#. #.. ..# .#." )); let gen2 = gen1.next_generation(); assert_eq!(gen2, PocketDimension::new3(&Pos3(-1,0,-2), "..... ..... ..#.. ..... ..... ..#.. .#..# ....# .#... ..... ##... ##... #.... ....# .###. ..#.. .#..# ....# .#... ..... ..... ..... ..#.. ..... ....." )); } #[test] fn test_six_generations_v1() { let mut p = PocketDimension::new3(&Pos3(0,0,0), test_grid()); for _ in 0..6 { p = p.next_generation(); } assert_eq!(p.active_cubes(), 112); } } 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
meseta profile image
Yuan Gao

As with my previous cellular automata, for this problem, I decided to just let TensorFlow handle the heavy lifting of a 4D convolution. However, TensorFlow only has 3D convolution (which I used for the first part) so I also had to write/adapt a 4D convolution script to handle it.

It's very fast though, running on the GPU

import numpy as np import tensorflow as tf data = np.loadtxt("input.txt", 'U', comments=None) bool_data = data.view('U1') == '#' bool_data = bool_data.reshape((1, 1, data.size, -1)) winit, zinit, yinit, xinit = bool_data.shape wdim, xdim, ydim, zdim = 40, 40, 40, 40 padding = tf.constant([[1, 1], [1, 1], [1, 1], [1, 1]]) neighbors_np = np.ones([3, 3, 3, 3]) neighbors_np[1, 1, 1, 1] = 0 conv_filt = tf.reshape(tf.cast(neighbors_np, tf.float32), [3, 3, 3, 3, 1, 1]) init_data_np = np.zeros([wdim, zdim, xdim, ydim], dtype=np.bool) ystart = xstart = (xdim-xinit)//2 yend = xend = xstart+xinit wstart = zstart = zdim//2 wend = zend = zstart+1 init_data_np[wstart:wend, zstart:zend, ystart:yend, xstart:xend] = bool_data init_data = tf.convert_to_tensor(init_data_np) @tf.function def conv4d(data, conv_filt): # input, kernel, and output sizes  (b, wi, zi, yi, xi, c) = data.shape.as_list() (wk, zk, yk, xk, ik, ok) = conv_filt.shape.as_list() # output size and tensor  wo = wi - wk + 1 results = [ None ]*wo # convolve each kernel frame i with each input frame j  for i in range(wk): for j in range(wi): # add results to this output frame  out_frame = j - (i - wk//2) - (wi - wo)//2 if out_frame < 0 or out_frame >= wo: continue # convolve input frame j with kernel frame i  frame_conv3d = tf.nn.convolution(tf.reshape(data[:,:,j,:,:], (b, zi, yi, xi, c)), conv_filt[:,:,:,i]) if results[out_frame] is None: results[out_frame] = frame_conv3d else: results[out_frame] += frame_conv3d return tf.stack(results, axis=2) @tf.function def generate(this_gen): padded = tf.pad(this_gen, padding) padded = tf.reshape(padded, [1, wdim+2, zdim+2, ydim+2, xdim+2, 1]) # need for tf.nn.convolution  convolved = conv4d(tf.cast(padded, tf.float32), conv_filt) neighbors = tf.reshape(convolved, [wdim, zdim, xdim, ydim]) three_neighbors = tf.math.equal(neighbors, 3) two_or_three_neighbors = tf.math.logical_or(tf.math.equal(neighbors, 2), three_neighbors) next_gen = tf.math.logical_or(this_gen, three_neighbors) next_gen = tf.math.logical_and(next_gen, two_or_three_neighbors) return next_gen generation = init_data for _ in range(6): generation = generate(generation) print("total", tf.math.reduce_sum(tf.cast(generation, tf.int32))) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
paddy3118 profile image
Paddy3118

Clarification request

we are given:

Before any cycles: z=0 .#. ..# ### 
Enter fullscreen mode Exit fullscreen mode

Lets put some coordinates around this and expand it so I can easily refer to things

Before any cycles: z=0 ABC 0 .#. 1 ..# 2 ### After 1 cycle: z=-1 ABC 0 #.. 1 ..# 2 .#. z=0 ABC 0 #.# 1 .## 2 .#. z=1 ABC 0 #.. 1 ..# 2 .#. 
Enter fullscreen mode Exit fullscreen mode

Now concentrate on the initial state, before any cycles: the cell at x=C, y=2 , z=0 or C,2,0 for short.
It is bottom-right. It has two active neighbours C,1,0 and B,2,0 That should mean that it stays active in cycle 1 but is shown inacative.

The rule that applies is:

*If a cube is active and exactly 2 or 3 of its neighbors are also active, the cube remains active. Otherwise, the cube becomes inactive.

That first cycle does not seem to fit the task explanation.

Could someone explain how they understand the explanation please, Thanks.

P.S. C,2,0 is marked 'X' below to help explain the coordinates

Before any cycles: z=0 ABC 0 .#. 1 ..# 2 ##X 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rpalo profile image
Ryan Palo

This is confusing to look at too, but there's one important phrase that makes it correct:

and the frame of view follows the active cells in each cycle

This thread, makes it more clear. At time t=1, the possible grid is actually 5x5x3. The sample input is just shifted and shrunk to follow the boundary of actual cells.

In other words, for your diagram's time step 0, cell 2C is the same cell as cell 1, C, 0 in time step 1. Try it out yourself but fill it in in a 5x5x3 grid. Let me know if this makes sense! I agree that it's confusing and he would have probably been better off showing the whole potential state space and not shifting to follow the active cells. :)

Collapse
 
thibpat profile image
Thibaut Patel

My JavaScript walkthrough:

4D messed with my head but it ended up ok 😅