Skip to main content
Commonmark migration
Source Link

###Invocation

Invocation

###Output

Output

###Invocation

###Output

Invocation

Output

update with more timing info, and result for n=15
Source Link
Miles
  • 436
  • 2
  • 6

1:. Using the AdoptOpenJDK8 distribution, on a 2-core Amber Lake Intel Core i5-based Mac. On an Amazon EC2 m5.xlarge, takes 1m16s.

This takes an inductive approach where, for each rank, it builds off all the buildings of the previous rank by placing cubes in all legal positions (on top of and next to existing cubes), and then removing buildings that are (possibly transformed) duplicates of other buildings. This means that to enumerate the buildings in a rank, all previous ranks must be also be computed. Both compute time and memory are constrained resources — this algorithm relies on keeping millions of Building objects in memory — so I've so far been unable to computehad a hard time computing beyond \$n=15\$\$n=14\$ on my machine even without the 10 minute time limit.

javac Building.java java -XX:+UseParallelGC -Xms12g -Xmx12g -Dparallel=true Building 14 
javac Building.java java -XX:+UseParallelGC -Xms14g -Xmx14g -Dparallel=true Building 14 

ParallelGC is the default on Java 8, but is included in case you are using a later version JDK where G1GC is the default.

(For reference, \$15 \rightarrow 92{,}038{,}062\$)

1: Using the AdoptOpenJDK8 distribution, on a 2-core Amber Lake Intel Core i5-based Mac

This takes an inductive approach where, for each rank, it builds off all the buildings of the previous rank by placing cubes in all legal positions (on top of and next to existing cubes), and then removing buildings that are (possibly transformed) duplicates of other buildings. This means that to enumerate the buildings in a rank, all previous ranks must be also be computed. Both compute time and memory are constrained resources — this algorithm relies on keeping millions of Building objects in memory — so I've so far been unable to compute beyond \$n=15\$ on my machine even without the 10 minute time limit.

javac Building.java java -XX:+UseParallelGC -Xms12g -Xmx12g -Dparallel=true Building 14 

1. Using the AdoptOpenJDK8 distribution, on a 2-core Amber Lake Intel Core i5-based Mac. On an Amazon EC2 m5.xlarge, takes 1m16s.

This takes an inductive approach where, for each rank, it builds off all the buildings of the previous rank by placing cubes in all legal positions (on top of and next to existing cubes), and then removing buildings that are (possibly transformed) duplicates of other buildings. This means that to enumerate the buildings in a rank, all previous ranks must be also be computed. Both compute time and memory are constrained resources — this algorithm relies on keeping millions of Building objects in memory — so I've had a hard time computing beyond \$n=14\$ on my machine even without the 10 minute time limit.

javac Building.java java -XX:+UseParallelGC -Xms14g -Xmx14g -Dparallel=true Building 14 

ParallelGC is the default on Java 8, but is included in case you are using a later version JDK where G1GC is the default.

(For reference, \$15 \rightarrow 92{,}038{,}062\$)

Source Link
Miles
  • 436
  • 2
  • 6

Java 8, \$n=14\$ in 2m31s1

1: Using the AdoptOpenJDK8 distribution, on a 2-core Amber Lake Intel Core i5-based Mac

This takes an inductive approach where, for each rank, it builds off all the buildings of the previous rank by placing cubes in all legal positions (on top of and next to existing cubes), and then removing buildings that are (possibly transformed) duplicates of other buildings. This means that to enumerate the buildings in a rank, all previous ranks must be also be computed. Both compute time and memory are constrained resources — this algorithm relies on keeping millions of Building objects in memory — so I've so far been unable to compute beyond \$n=15\$ on my machine even without the 10 minute time limit.

This solution includes a parallel-Stream-based approach (which can be enabled with the parallel JVM system property) which is faster on a multi-core system but also more memory-hungry. This approach was used for the timing results above. The non-parallel approach takes almost twice as long to count \$n=14\$ but is able to do so using only a third of the memory.

Garbage collector settings and tuning can have a significant impact on the runtime and ability of the process to complete. I've also tested with OpenJDK 13, but for whatever reason have seen the best results on 8 so far.

import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; public final class Building { /** * A flattened two-dimensional matrix of heights (see toIndex). * Buildings are always assumed to be "aligned", such that they exactly * fit within their (width, height) bounding box. */ private final byte[] stacks; private final int hashCode; private final byte width; private final byte height; public Building() { this(new byte[]{1}, 1); } private Building(byte[] stacks, int width) { assert stacks.length % width == 0; this.stacks = stacks; this.width = (byte) width; this.height = (byte) (stacks.length / width); this.hashCode = 31 * width + Arrays.hashCode(stacks); } /** * Return the building created by adding a cube at the specified coordinates. * The coordinates must be within the current building bounds or else * directly adjacent to one of the sides, but this is not validated. */ Building add(int x, int y) { if (x < 0) { byte[] newStacks = widen(true); newStacks[y * (width + 1)]++; return new Building(newStacks, width + 1); } else if (x < width) { byte[] newStacks; if (y < 0) { newStacks = new byte[stacks.length + width]; System.arraycopy(stacks, 0, newStacks, width, stacks.length); y = 0; } else if (y * width < stacks.length) { newStacks = Arrays.copyOf(stacks, stacks.length); } else { newStacks = Arrays.copyOf(stacks, stacks.length + width); } newStacks[toIndex(x, y)]++; return new Building(newStacks, width); } else { byte[] newStacks = widen(false); newStacks[x + y * (width + 1)]++; return new Building(newStacks, width + 1); } } byte[] widen(boolean shift) { byte[] newStacks = new byte[stacks.length + height]; int writeIndex = shift ? 1 : 0; for (int i = 0; i < stacks.length; i++) { newStacks[writeIndex++] = stacks[i]; if (i % width == width - 1) { writeIndex++; } } return newStacks; } int toIndex(int x, int y) { return x + y * width; } boolean inBounds(int x, int y) { return x >= 0 && y >= 0 && x < width && y < height; } /** * Return a stream of all legal buildings that can be created by adding a * cube to this building. */ Stream<Building> grow() { int wider = width + 2; int max = (height + 2) * wider; return StreamSupport.stream(new Spliterators.AbstractSpliterator<Building>(max, 0) { int i = -1; @Override public boolean tryAdvance(Consumer<? super Building> action) { while ((++i) < max) { // Try adding a cube to every position on the grid, // as well as adjacent to it int x = i % wider - 1; int y = i / wider - 1; int index = toIndex(x, y); if (x < 0) { if (y >= 0 && y < height) { if (stacks[index + 1] > 0) { action.accept(add(x, y)); return true; } } } else if (x < width) { if (y < 0) { if (stacks[index + width] > 0) { action.accept(add(x, y)); return true; } } else if (y < height) { // it is on the existing grid if (stacks[index] > 0) { action.accept(add(x, y)); return true; } else { // is it adjacent to a stack? for (Direction d : Direction.values()) { int x2 = x + d.x, y2 = y + d.y; if (inBounds(x2, y2) && stacks[toIndex(x2, y2)] > 0) { action.accept(add(x, y)); return true; } } } } else if (stacks[index - width] > 0) { action.accept(add(x, y)); return true; } } else if (y >= 0 && y < height) { if (stacks[index - 1] > 0) { action.accept(add(x, y)); return true; } } } return false; } }, false); } Building reflect() { byte[] newStacks = new byte[stacks.length]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { newStacks[y + x * height] = stacks[toIndex(x, y)]; } } return new Building(newStacks, height); } Building rotate() { byte[] newStacks = new byte[stacks.length]; for (int x = 0; x < width; x++) { for (int y = 0, x2 = height - 1; y < height; y++, x2--) { newStacks[x2 + x * height] = stacks[toIndex(x, y)]; } } return new Building(newStacks, height); } Collection<Building> transformations() { List<Building> bs = new ArrayList<>(7); Building b1 = this, b2 = this.reflect(); bs.add(b2); for (int i = 0; i < 3; i++) { bs.add((b1 = b1.rotate())); bs.add((b2 = b2.rotate())); } return bs; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Building building = (Building) o; return width == building.width && Arrays.equals(stacks, building.stacks); } @Override public int hashCode() { return hashCode; } private enum Direction { N(0, 1), E(1, 0), S(0, -1), W(-1, 0); final int x; final int y; Direction(int x, int y) { this.x = x; this.y = y; } } public static int count(int rank) { long start = System.nanoTime(); Collection<Building> buildings = new HashSet<>(); for (int i = 1; i <= rank; i++) { if (i == 1) { buildings = Arrays.asList(new Building()); } else if (Boolean.getBoolean("parallel")) { // Using parallel streams is generally faster, but requires // more memory since more Buildings are retained before being // discarded ConcurrentMap<Building, Integer> map = new ConcurrentHashMap<>(buildings.size() * 4); AtomicInteger atomicInt = new AtomicInteger(); buildings.parallelStream() .flatMap(Building::grow) .forEach(b -> { map.putIfAbsent(b, atomicInt.incrementAndGet()); }); // Keep only the buildings that do not have a transformation // with a lower index buildings = map.entrySet().parallelStream() .filter(entry -> { int index = entry.getValue(); for (Building b2 : entry.getKey().transformations()) { Integer index2 = map.get(b2); if (index2 != null && index2 < index) { return false; } } return true; }) .map(Map.Entry::getKey) .collect(Collectors.toList()); } else { Set<Building> set = new HashSet<>(buildings.size() * 4); // Only add a building to the set if it doesn't already have a // transformation in it. buildings.stream() .flatMap(Building::grow) .forEach(b -> { if (!set.contains(b)) { for (Building t : b.transformations()) { if (set.contains(t)) return; } set.add(b); } }); buildings = set; } System.err.println(i + " --> " + buildings.size()); long now = System.nanoTime(); double ms = ((double) now - start) / 1000000; System.err.println("time: " + (ms < 1000 ? ms + " ms" : ms / 1000 + " s")); } return buildings.size(); } public static void main(String[] args) { System.out.println(Building.count(Integer.parseInt(args[0]))); } } 

Try it online! (non-parallel, up to n=12)

###Invocation

javac Building.java java -XX:+UseParallelGC -Xms12g -Xmx12g -Dparallel=true Building 14 

###Output

1 --> 1 time: 0.410181 ms 2 --> 2 time: 97.807367 ms 3 --> 4 time: 99.648279 ms 4 --> 12 time: 101.00362 ms 5 --> 35 time: 102.4856 ms 6 --> 129 time: 105.723149 ms 7 --> 495 time: 113.747058 ms 8 --> 2101 time: 130.012756 ms 9 --> 9154 time: 193.924776 ms 10 --> 41356 time: 436.551396 ms 11 --> 189466 time: 991.984875 ms 12 --> 880156 time: 3.899371721 s 13 --> 4120515 time: 18.794214388 s 14 --> 19425037 time: 151.782854829 s 19425037