This makes a lot more sense. With the way I’m currently doing it, there are too many valid, but “useless” states considered, like just moving the character random directions w/o pushing a box, and that’s probably why my node count is so high. Guess this requires a whole rewrite of everything I’ve got.
Figuring out unreachable positions and reconstructing the character path sounds really expensive though?
If you ignore the character position and just generate box pushes, how can you tell if a push is unreachable without also knowing where the character is? Do you keep some kind of character reachable area info with each state?
Also, do you do a separate BFS within each iteration to scan for reachable positions and then one at the end to reconstruct the character path to each box push?
Djoum’s approach is very interesting but not mandatory at all, in fact I wanted to try this but finally didn’t had too.
If you want to go Djoum’s way, just keep track of the player position after each push. At each step you can use a small DFS or BFS (doesn’t matter) to find the next possible pushes.
EDIT: Disregard the previous post. I finally started tracking how many iterations my BFS was doing vs the actual size of my visited nodes. I was iterating more times than I was actually finding unique nodes, despite pruning before appending to the queue. Added a check at the top for uniqueness (before any computation work was done) and suddenly the whole things runs much faster and passes all tests. No idea why as I’m very much an Algo noob. Thanks for the help everyone.
Reconstructing the character path is a chained BFS itself, but we’re talking a few hundred iterations at worst so the cost is minimal.
For the unreachable cells, I number areas on the map (each one is separate from the other) and I check the moves that have the same number as the character. Any time a box is moved, you know the cell where the character would be and you can include it in your BFS state. Doing this incurs some cost, but it dropped my number of iterations so much that it ended up being a massive gain.
Solved in JS with simplest BFS (just a tree and a hash for already visited states), with map in a wxh long string ( ), no heuristics, no metrics, just “don’t push in a corner please”.
Test cases timings:
3-5: 312k nodes, 2.0s
3-7: 194k nodes, 1.3s
3-10: 263k nodes, 1.7s
Great puzzle!! It was very fun and the puzzle is very good for learning BFS,
both for searching in maze and generating possible states.
With a good pruning you don’t have to use the second given in the first turn, I didn’t use it.
Very nice puzzle! Took me quite a long time to optimise and understand why my “alreadySeen” cache wasn’t working properly (coded a custom hash for state, without handling collisions, in go).
Also thanks for the hint that boxes order should have been ignored when hashing.
To torture myself practice, I translated my original solution to several languages.
As these version use exactly the same (deterministic) search order, it can be used as a kind of language benchmark. Of course, relative speed depends on MANY things, so these results should not be generalized or taken too seriously.
These are the average running times for IDE TEST #25 in turn 0 (when the complete solution is found) on my local machine:
language
msec
relative
C++ (optimize pragma)
85
1.00
C++ (default options)
242
2.85
Java
421
4.95
PHP
3,747
44.08
Python
1,776
20.89
Notes:
My code searches 226,877 nodes for this input.
The pragma line for the optimized C++ run was: #pragma GCC optimize "O3,omit-frame-pointer,inline,unroll-loops"
All solutions use standard library data types (vector + unordered_map, Vector + HashMap, array, list + dict), and don’t use language specific code optimizations. The game state representation is encoded to a single int64, involving lots of bitwise operations.
Online results in CG IDE was quite similar. However Java seems to be twice as fast locally. I don’t know if this comes from Java v8 vs Java v17, or from different default settings.)
While other languages running times are fairly stable, Java result fluctuated by ±15%, most likely because of the garbage collector.
I optimized my code as much as possible, but i don’t think this is the problem.
I saw that some of you need less than 100,000 iterations to find a solution, and i’m over 1,000,000 (2,429,671 on scoria 2 level 10 and i don’t timeout).
Is there a heuristic to change the BFS into A* ? Or did I miss something else ?
For reference, I’m finding the solution (85 moves) for scoria 2 level 10 with BFS in 49,894 iterations.
I think that “ignoring the boxes order” is referring to if you have 2 boxes on positions x1;y1 & x2;y2 it doesn’t matter if it’s box ID 1 on x1;y1 and box ID 2 on x2;y2 or if it’s ID 2 on x1;y1 and box ID 1 on x2;y2
I tryied to sort, but it was too long (timeout on the very first test). Maybe i don’t know the good way to sort.
I think I’ll try to use an int128 to bitboard boxes with “0” for empty cell and “1” with a cell with a box.
Similar to avoiding corners you can avoid segments when you know that the box would be stuck on that segment.
A segment is a part of a row with only consecutive empty cells (no target) with a wall on the left and the right. If at the row below or above all the positions of the segment are walls then no boxes should be pushed on any of the positions of the segment.
On line 1 you have a segment with positions: 1, 2 & 3, since at line 0 there’s a wall for the positions 1, 2 & 3 you know you can forbid the position 2;1 (positions 1;1 & 3;1 are already excluded because they are corners)
On line 5 you have a segment with positions: 1, 2, 3, 4 & 5, since at line 6 there’s a wall for the positions 1, 2, 3, 4 & 5 you know you can forbid the position 2;5 & 3;5 & 4;5 (positions 1;5 & 5;5 are already excluded because they are corners)
The same logic applies to segments on columns, so you can also forbid the positions 5;3 & 5;4
Sorting 5 numbers should not take too much time… When (and only when) any box is moved, I re-sort the box positions before re-packing them into an int64 game state. It is good not just for the hashmap lookup in the BFS to avoid revisiting same game state twice, but also to make checking for the goal easier, i.e. a simple int comparison.
Pruning the state space, as cerdicdd described, is also essential.
Depending on language choice, turn off garbage collection if possible.
Pre-allocating big vectors, instead of letting them grow only as needed also saves some time.