Build a top-front-side puzzle solver

15

A top-front-side puzzle is a puzzle where you are required to construct a 3-D shape of (usually cubic) blocks given three orthogonal views: a top view, a front view, and a side view.

For example, given a top, front, and side view as follows:

Top:        Front:      Side:
. . . .     . . . .     . . . .
. x x .     . x x .     . x x .
. x x .     . x x .     . x x .
. . . .     . . . .     . . . .

In this problem, the side view is taken from the right.

A 2x2x2 cube (with volume 8) would satisfy this solution, but it's doable in volume 4, if we have the following layer structure:

. . . .     . . . .
. x . .     . . x .
. . x .     . x . .
. . . .     . . . .

There are also some unsolvable arrangements. Take, for example:

Top:        Front:      Side:
. . . .     . . . .     . . . .
. . . .     . . x .     . . . .
. x . .     . . . .     . x . .
. . . .     . . . .     . . . .

If the top view says the block is second from the left, there's no way that can match the front view that says the block must be third from the left. So this arrangement is impossible.


Your task is to build a program that, given an arbitrary 4x4 top-front-side puzzle, attempts to solve it in the fewest number of cubes, or declares it unsolvable.

Your program will take as input a series of 48 bits, representing the top, front, and side views. They may be in any format you want (a 6-byte string, a string of 0's and 1's, a 12-digit hex number, etc.), but the order of the bits must map as such:

Top: 0x00   Front: 0x10 Side: 0x20
0 1 2 3     0 1 2 3     0 1 2 3
4 5 6 7     4 5 6 7     4 5 6 7
8 9 a b     8 9 a b     8 9 a b
c d e f     c d e f     c d e f

In other words, the bits go in a left-to-right, top-to-bottom order, in the top, then front, then side view.

Your program will then output either a series of 64 bits indicating the cubes in the 4x4x4 grid that are filled in, or indicate that the grid is unsolvable.


Your program will be scored by running a battery of 1,000,000 test cases.

The test data will be generated by taking the MD5 hashes of the integers "000000" through "999999" as strings, extracting the first 48 bits (12 hexits) of each of these hashes, and using them as input for the top-front-side puzzle. As an example, here are some of the test inputs and the puzzles they generate:

Puzzle seed: 000000   hash: 670b14728ad9
Top:        Front:      Side:
. x x .     . . . x     x . . .
x x x x     . x . x     x . x .
. . . .     . x x x     x x . x
x . x x     . . x .     x . . x

Puzzle seed: 000001   hash: 04fc711301f3
Top:        Front:      Side:
. . . .     . x x x     . . . .
. x . .     . . . x     . . . x
x x x x     . . . x     x x x x
x x . .     . . x x     . . x x

Puzzle seed: 000157   hash: fe88e8f9b499
Top:        Front:      Side:
x x x x     x x x .     x . x x
x x x .     x . . .     . x . .
x . . .     x x x x     x . . x
x . . .     x . . x     x . . x

The first two are unsolvable, while the last one has a solution with the following layers, front to back:

x . . .   . . . .   x x x .   x x x .
. . . .   x . . .   . . . .   . . . .
x . . .   . . . .   . . . .   x x x x
x . . .   . . . .   . . . .   x . . x

There are a total of 16 blocks here, but it can probably be done in less.

Your program's score will be determined by the following criteria, in descending order of priority:

  • The highest number of solved cases.
  • The lowest number of blocks required to solve those cases.
  • The shortest code in bytes.

You must submit and calculate the score by yourself, which requires your program to be able to run through all 1,000,000 test cases.

Joe Z.

Posted 2015-03-28T04:55:42.570

Reputation: 30 589

I learned while trying to generate test cases for this problem that more cases are unsolvable than not. I wonder how this will turn out. – Joe Z. – 2015-03-28T04:57:49.100

If it's an optimization problem, there should be a time limit, so people don't brute-force it. – isaacg – 2015-03-28T05:28:33.140

The time limit is however long they take to test it. A solution that takes forever to run will never produce a result that can be posted here. That's how all the optimization problems I write work. – Joe Z. – 2015-03-28T05:30:28.547

State that explicitly then, so people don't get confused. Something like A submission is only valid once the battery is complete. – isaacg – 2015-03-28T06:34:36.237

This is a more general form of that challenge. – Joe Z. – 2015-03-28T07:58:11.450

I actually thought about asking a continuous version of this after seeing this question on Mathematica.SE. :)

– Martin Ender – 2015-03-28T12:44:13.780

Because I deleted my shown to be incorrect answer, I'd like to copy some comments by Jakube from it to here. The first solvable seed is "00143". The first seed with '6-letter' seeds is "000157", and there are 3326 solvable test-cases out of the million. – orlp – 2015-03-28T21:09:12.037

So only about 1 in 300 TFS puzzles are solvable. Interesting. – Joe Z. – 2015-03-28T21:18:00.287

@JoeZ. I get 1 in 200 with my code for counting.

– orlp – 2015-03-28T22:06:35.813

1@JoeZ. orlp is right. I had a bug in my md5-to-puzzle conversion. 551 solvable puzzles in 00000-99999 and 5360 solvable puzzles in 000000-999999. – Jakube – 2015-03-28T23:03:31.397

I spent days playing this game once :) – TheNumberOne – 2015-03-29T02:14:17.030

You can remove 4 blocks from the back layer. – TheNumberOne – 2015-03-30T18:14:56.663

cool project. where/how did you come up with this? – qwr – 2015-03-31T03:30:22.790

It was a game I came across a while back. A Java app, probably still available somewhere on the Internet. – Joe Z. – 2015-03-31T03:33:08.103

@JoeZ. Is it allowed to exclude optimizations in your golfed submissions that do not affect correctness, compared to the less golfed submission? If not, how would you enforce this? – orlp – 2015-03-31T08:50:44.883

Answers

5

Python: 5360 test-cases solved using 69519 blocks, 594 bytes

This should be the optimal values.

Approach:

First I'll test if the test-case is actual solvable. I do this by initializing a list of length 64 by ones and set all positions to zero, if there the corresponding pixel of the three views is zero. Then I simple view at the puzzle from all 3 directions, and look if the views are the same as the input views. If there're equal, the puzzle is solvable (we already found the worst solution), otherwise it's unsolvable.

Then I do a branch-and-bound approach for finding the optimal solution.

Branching: I have a recursive function. The recursion depth tells how many values are fixed already. In each call of the function I call itself twice, if the value at the current index is 1 (this value can be 0 or 1 in the optimal solution), or once, if the value is 0 (it has to be zero in the optimal solution).

Bounding: I use two different strategies here.

  1. I calculate the views from the 3 sides in each function call and look If they still match the input values. If they don't match, I don't call the function recursively.

  2. I keep the best solution in memory. It there already more fixed ones in the current branch than in the best solution, I can already close that branch. Additionally I can estimate a lower bound for the number of activated blocks, which are not fixed. And the condition becomes #number of activated fixed blocks + #lower bound of activated blocks (under the not fixed blocks) < #number of activated blocks in the best solution.

Here's the Python code. It defines a function f which expects 3 lists containing 1s and 0s and returns either 0 (not solvable) or a list of 1s and 0s representing the optimal solution.

S=sum;r=range
def f(t,f,s):
 for i in r(4):s[4*i:4*i+4]=s[4*i:4*i+4][::-1]
 c=[min(t[i%16],f[(i//16)*4+i%4],s[i//4])for i in r(64)]
 m=lambda:([int(S(c[i::16])>0)for i in r(16)],[int(S(c[i+j:i+j+16:4])>0)for i in r(0,64,16)for j in r(4)],[int(S(c[i+j:i+j+4])>0)for i in r(0,64,16)for j in r(0,16,4)])==(t,f,s)
 Z=[65,0];p=[]
 def g(k):
  if k>63and S(c)<Z[0]:Z[:]=[S(c),c[:]]
  if k>63or S(c[:k])+p[k]>=Z[0]:return
  if c[k]:c[k]=0;m()and g(k+1);c[k]=1
  m()and g(k+1)
 for i in r(64):h,R=(i//16)*4+4,(i//4)%4;p+=[max(S(f[h:]),S(s[h:]))+max((R<1)*S(f[h+i%4-3:h]),S(s[h+R-3:h]))]
 g(0);return Z[1]

The code length is 596 bytes/chars. And here's the test framework:

from hashlib import md5
from time import time

N = 1000000
start=time();count=blocks=0
for n in range(N):
 bits = list(map(int,"{:048b}".format(int(md5("{:06}".format(n).encode("utf-8")).hexdigest()[:12], 16))))
 result = f(bits[:16], bits[16:32], bits[32:])
 if result:
  count += 1
  blocks += sum(result)
  print("Seed: {:06}, blocks: {}, cube: {}".format(n, sum(result), result))
print()
print("{} out of {} puzzles are solvable using {} blocks.".format(count, N, blocks))
print("Total time: {:.2f} seconds".format(time()-start))

You can find an ungolfed version of the program here. It's also a little bit faster.

Results:

5360 out of 1000000 puzzles are solvable. In total we need 69519 blocks. The number of blocks varies from 6 blocks to 18 blocks. The hardest puzzle took about 1 second to solve. It's the puzzle with the seed "347177", which looks like

Top:      Front:    Side:
x x . .   x x x x   x . x .
x x x x   x x x x   x x x x
x x x x   x x x x   x x x x
x x . x   x x x x   x . x x

and has an optimal solution with 18 cubes. The following is a few from top for each of the layers:

Top 1:    Top 2:    Top 3:    Top 4:
. . . .   . x . .   . x . .   x . . .
. . x x   . . x .   x . . .   . x x .
. . . .   . . . x   x x x .   . . . .
x x . .   x . . .   . . . x   . . . x

The total running time for all test-cases was about 90 seconds. I used PyPy to execute the my program. CPython (the default Python interpreter) is a bit slower, but also solves all puzzles in only 7 minutes.

You can find the complete output here: The output is self-explanatory. E.g. the output for the puzzle above is:

Seed: 347177, blocks: 18, cube: [0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1]

Jakube

Posted 2015-03-28T04:55:42.570

Reputation: 21 462

3

5360 cases solved with 69519 blocks; 923 bytes

This is also optimal. Call with an array of ones and zeroes. Throws a NullPointerException for invalid input. Some efficiency is sacrificed to golf it. It still completes within a reasonable time for all 1000000 test inputs.

import java.util.*;int[]a(int[]a){a b=new a(a);b=b.b(64);return b.d();}class a{List<int[]>a=new ArrayList();List b=new ArrayList();int c;a(int[]d){int e=0,f,g,h,i[]=new int[48];for(;e<64;e++){f=e/16;g=(e%16)/4;h=e%4;if(d[f+12-4*h]+d[16+f+g*4]+d[32+h+g*4]>2){i[f+12-4*h]=i[16+f+g*4]=i[32+h+g*4]=1;a.add(new int[]{f,g,h});c++;}}if(!Arrays.equals(d,i))throw null;b=f();}a(){}a b(int d){if(c-b.size()>d|b.size()<1)return this;a e=c();e.a.remove(b.get(0));e.b.retainAll(e.f());e.c--;e=e.b(d);d=Math.min(e.c,d);a f=c();f=f.b(d);return e.c>f.c?f:e;}a c(){a c=new a();c.a=new ArrayList(a);c.b=new ArrayList(b);c.b.remove(0);c.c=this.c;return c;}int[]d(){int[]d=new int[64];for(int[]e:a)d[e[2]*16+e[1]*4+e[0]]=1;return d;}List f(){List d=new ArrayList();int e,f,g;for(int[]h:a){e=0;f=0;g=0;for(int[]p:a)if(p!=h){e|=h[0]==p[0]&h[1]==p[1]?1:0;f|=h[0]==p[0]&h[2]==p[2]?1:0;g|=h[1]==p[1]&h[2]==p[2]?1:0;}if(e+f+g>2)d.add(h);}return d;}}

Strategy:

I actually used to play this puzzle quite a bit when I was 10. This uses my approach.

Step 1:

Form the cube with the most blocks that fits the given views.

Step 2:

Create a list of removable pieces. (Any piece with that has another piece on the row its in, the column its in, and the beam its in.)

Step 3:

Test every possible way to keep or remove each removable piece. (Via recursive brute-force with pruning.)

Step 4:

Keep the best valid cube.

Ungolfed:

int[] main(int[] bits) {
    Cube cube = new Cube(bits);
    cube = cube.optimize(64);
    return cube.bits();
}

class Cube {

    List<int[]> points = new ArrayList();
    List removablePoints = new ArrayList();
    int size;

    Cube(int[] bits) {
        int i = 0,x,y,z,placed[] = new int[48];
        for (; i < 64; i++) {
            x = i / 16;
            y = (i % 16) / 4;
            z = i % 4;
            if (bits[x + 12 - 4 * z] + bits[16 + x + y * 4] + bits[32 + z + y * 4] > 2) {
                placed[x + 12 - 4 * z] = placed[16 + x + y * 4] = placed[32 + z + y * 4] = 1;
                points.add(new int[]{x, y, z});
                size++;
            }
        }

        if (!Arrays.equals(bits, placed))
            throw null;

        removablePoints = computeRemovablePoints();
    }

    Cube() {
    }

    Cube optimize(int smallestFound) {
        if (size - removablePoints.size() > smallestFound | removablePoints.size() < 1)
            return this;

        Cube cube1 = duplicate();
        cube1.points.remove(removablePoints.get(0));

        cube1.removablePoints.retainAll(cube1.computeRemovablePoints());
        cube1.size--;

        cube1 = cube1.optimize(smallestFound);
        smallestFound = Math.min(cube1.size, smallestFound);

        Cube cube2 = duplicate();

        cube2 = cube2.optimize(smallestFound);

        return cube1.size > cube2.size ? cube2 : cube1;

    }

    Cube duplicate() {
        Cube c = new Cube();
        c.points = new ArrayList(points);
        c.removablePoints = new ArrayList(removablePoints);
        c.removablePoints.remove(0);
        c.size = size;
        return c;
    }

    int[] bits() {
        int[] bits = new int[64];
        for (int[] point : points)
            bits[point[2] * 16 + point[1] * 4 + point[0]] = 1;
        return bits;
    }

    List computeRemovablePoints(){
        List removablePoints = new ArrayList();
        int removableFront, removableTop, removableSide;
        for (int[] point : points) {
            removableFront = 0;
            removableTop = 0;
            removableSide = 0;
            for (int[] p : points)
                if (p != point) {
                    removableFront |= point[0] == p[0] & point[1] == p[1] ? 1 : 0;
                    removableTop |= point[0] == p[0] & point[2] == p[2] ? 1 : 0;
                    removableSide |= point[1] == p[1] & point[2] == p[2] ? 1 : 0;
                }
            if (removableFront + removableTop + removableSide > 2)
                removablePoints.add(point);
        }
        return removablePoints;
    }

    public String toString() {

        String result = "";
        int bits[] = bits(),x,y,z;

        for (z = 0; z < 4; z++) {
            for (y = 0; y < 4; y++) {
                for (x = 0; x < 4; x++) {
                    result += bits[x + 4 * y + 16 * z] > 0 ? 'x' : '.';
                }
                result += System.lineSeparator();
            }
            result += System.lineSeparator();
        }

        return result;

    }
}

Full program:

import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Example cube:
 *
 * origin
 * |   ........
 * |  .      ..
 * | . top  . .
 * v.      .  .
 * ........   .  <- side
 * .      .  .
 * . front. .
 * .      ..
 * ........
 *
 *     / z
 *    /
 *  /
 * .-----> x
 * |
 * |
 * |
 * V y
 */

public class PPCG48247 {

    public static void main(String[] args) throws Exception{
        MessageDigest digest = MessageDigest.getInstance("MD5");
        int totalSolved = 0;
        int totalCubes = 0;

        for (int i = 0; i < 1000000; i++){
            byte[] input = String.format("%06d", i).getBytes();

            byte[] hash = digest.digest(input);
            int[] bits = new int[48];

            for (int j = 0, k = 0; j < 6; j++, k += 8){
                byte b = hash[j];
                bits[k] = (b >> 7) & 1;
                bits[k + 1] = (b >> 6) & 1;
                bits[k + 2] = (b >> 5) & 1;
                bits[k + 3] = (b >> 4) & 1;
                bits[k + 4] = (b >> 3) & 1;
                bits[k + 5] = (b >> 2) & 1;
                bits[k + 6] = (b >> 1) & 1;
                bits[k + 7] = b & 1;
            }

            try {
                int[] solution = new PPCG48247().a(bits);
                totalSolved++;
                for (int b : solution){
                    totalCubes += b;
                }
            } catch (NullPointerException ignored){}

        }
        System.out.println(totalSolved);
        System.out.println(totalCubes);
    }

    int[] main(int[] bits) {
        Cube cube = new Cube(bits);
        cube = cube.optimize(64);
        return cube.bits();
    }

    class Cube {

        List<int[]> points = new ArrayList();
        List removablePoints = new ArrayList();
        int size;

        Cube(int[] bits) {
            int i = 0,x,y,z,placed[] = new int[48];
            for (; i < 64; i++) {
                x = i / 16;
                y = (i % 16) / 4;
                z = i % 4;
                if (bits[x + 12 - 4 * z] + bits[16 + x + y * 4] + bits[32 + z + y * 4] > 2) {
                    placed[x + 12 - 4 * z] = placed[16 + x + y * 4] = placed[32 + z + y * 4] = 1;
                    points.add(new int[]{x, y, z});
                    size++;
                }
            }

            if (!Arrays.equals(bits, placed))
                throw null;

            removablePoints = computeRemovablePoints();
        }

        Cube() {
        }

        Cube optimize(int smallestFound) {
            if (size - removablePoints.size() > smallestFound | removablePoints.size() < 1)
                return this;

            Cube cube1 = duplicate();
            cube1.points.remove(removablePoints.get(0));

            cube1.removablePoints.retainAll(cube1.computeRemovablePoints());
            cube1.size--;

            cube1 = cube1.optimize(smallestFound);
            smallestFound = Math.min(cube1.size, smallestFound);

            Cube cube2 = duplicate();

            cube2 = cube2.optimize(smallestFound);

            return cube1.size > cube2.size ? cube2 : cube1;

        }

        Cube duplicate() {
            Cube c = new Cube();
            c.points = new ArrayList(points);
            c.removablePoints = new ArrayList(removablePoints);
            c.removablePoints.remove(0);
            c.size = size;
            return c;
        }

        int[] bits() {
            int[] bits = new int[64];
            for (int[] point : points)
                bits[point[2] * 16 + point[1] * 4 + point[0]] = 1;
            return bits;
        }

        List computeRemovablePoints(){
            List removablePoints = new ArrayList();
            int removableFront, removableTop, removableSide;
            for (int[] point : points) {
                removableFront = 0;
                removableTop = 0;
                removableSide = 0;
                for (int[] p : points)
                    if (p != point) {
                        removableFront |= point[0] == p[0] & point[1] == p[1] ? 1 : 0;
                        removableTop |= point[0] == p[0] & point[2] == p[2] ? 1 : 0;
                        removableSide |= point[1] == p[1] & point[2] == p[2] ? 1 : 0;
                    }
                if (removableFront + removableTop + removableSide > 2)
                    removablePoints.add(point);
            }
            return removablePoints;
        }

        public String toString() {

            String result = "";
            int bits[] = bits(),x,y,z;

            for (z = 0; z < 4; z++) {
                for (y = 0; y < 4; y++) {
                    for (x = 0; x < 4; x++) {
                        result += bits[x + 4 * y + 16 * z] > 0 ? 'x' : '.';
                    }
                    result += System.lineSeparator();
                }
                result += System.lineSeparator();
            }

            return result;

        }
    }

}

TheNumberOne

Posted 2015-03-28T04:55:42.570

Reputation: 10 855