shithub: puzzles

ref: 92ac45fe240b2063455b0b01dedc6ef6996f18af
dir: /divvy.c/

View raw version
/*
 * Library code to divide up a rectangle into a number of equally
 * sized ominoes, in a random fashion.
 * 
 * Could use this for generating solved grids of
 * http://www.nikoli.co.jp/ja/puzzles/block_puzzle/
 * or for generating the playfield for Jigsaw Sudoku.
 */

/*
 * This code is restricted to simply connected solutions: that is,
 * no single polyomino may completely surround another (not even
 * with a corner visible to the outside world, in the sense that a
 * 7-omino can `surround' a single square).
 * 
 * It's tempting to think that this is a natural consequence of
 * all the ominoes being the same size - after all, a division of
 * anything into 7-ominoes must necessarily have all of them
 * simply connected, because if one was not then the 1-square
 * space in the middle could not be part of any 7-omino - but in
 * fact, for sufficiently large k, it is perfectly possible for a
 * k-omino to completely surround another k-omino. A simple
 * example is this one with two 25-ominoes:
 * 
 *   +--+--+--+--+--+--+--+
 *   |                    |
 *   +  +--+--+--+--+--+  +
 *   |  |              |  |
 *   +  +              +  +
 *   |  |              |  |
 *   +  +              +  +--+
 *   |  |              |     |
 *   +  +              +  +--+
 *   |  |              |  |
 *   +  +              +  +
 *   |  |              |  |
 *   +  +--+--+--+--+--+  +
 *   |                    |
 *   +--+--+--+--+--+--+--+
 * 
 * I claim the smallest k which can manage this is 23. More
 * formally:
 * 
 *   If a k-omino P is completely surrounded by another k-omino Q,
 *   such that every edge of P borders on Q, then k >= 23.
 * 
 * Proof:
 * 
 * It's relatively simple to find the largest _rectangle_ a
 * k-omino can enclose. So I'll construct my proof in two parts:
 * firstly, show that no 22-omino or smaller can enclose a
 * rectangle as large as itself, and secondly, show that no
 * polyomino can enclose a larger non-rectangle than a rectangle.
 * 
 * The first of those claims:
 * 
 * To surround an m x n rectangle, a polyomino must have 2m
 * squares along the two m-sides of the rectangle, 2n squares
 * along the two n-sides, and must fill in at least three of the
 * corners in order to be connected. Thus, 2(m+n)+3 <= k. We wish
 * to find the largest value of mn subject to that constraint, and
 * it's clear that this is achieved when m and n are as close to
 * equal as possible. (If they aren't, WLOG suppose m < n; then
 * (m+1)(n-1) = mn + n - m - 1 >= mn, with equality only when
 * m=n-1.)
 * 
 * So the area of the largest rectangle which can be enclosed by a
 * k-omino is given by floor(k'/2) * ceil(k'/2), where k' =
 * (k-3)/2. This is a monotonic function in k, so there will be a
 * unique point at which it goes from being smaller than k to
 * being larger than k. That point is between 22 (maximum area 20)
 * and 23 (maximum area 25).
 * 
 * The second claim:
 * 
 * Suppose we have an inner polyomino P surrounded by an outer
 * polyomino Q. I seek to show that if P is non-rectangular, then
 * P is also non-maximal, in the sense that we can transform P and
 * Q into a new pair of polyominoes in which P is larger and Q is
 * at most the same size.
 * 
 * Consider walking along the boundary of P in a clockwise
 * direction. (We may assume, of course, that there is only _one_
 * boundary of P, i.e. P has no hole in the middle. If it does
 * have a hole in the middle, it's _trivially_ non-maximal because
 * we can just fill the hole in!) Our walk will take us along many
 * edges between squares; sometimes we might turn left, and
 * certainly sometimes we will turn right. Always there will be a
 * square of P on our right, and a square of Q on our left.
 * 
 * The net angle through which we turn during the entire walk must
 * add up to 360 degrees rightwards. So if there are no left
 * turns, then we must turn right exactly four times, meaning we
 * have described a rectangle. Hence, if P is _not_ rectangular,
 * then there must have been a left turn at some point. A left
 * turn must mean we walk along two edges of the same square of Q.
 * 
 * Thus, there is some square X in Q which is adjacent to two
 * diagonally separated squares in P. Let us call those two
 * squares N and E; let us refer to the other two neighbours of X
 * as S and W; let us refer to the other mutual neighbour of S and
 * W as D; and let us refer to the other mutual neighbour of S and
 * E as Y. In other words, we have named seven squares, arranged
 * thus:
 * 
 *     N
 *   W X E
 *   D S Y
 * 
 * where N and E are in P, and X is in Q.
 * 
 * Clearly at least one of W and S must be in Q (because otherwise
 * X would not be connected to any other square in Q, and would
 * hence have to be the whole of Q; and evidently if Q were a
 * 1-omino it could not enclose _anything_). So we divide into
 * cases:
 * 
 * If both W and S are in Q, then we take X out of Q and put it in
 * P, which does not expose any edge of P. If this disconnects Q,
 * then we can reconnect it by adding D to Q.
 * 
 * If only one of W and S is in Q, then wlog let it be W. If S is
 * in _P_, then we have a particularly easy case: we can simply
 * take X out of Q and add it to P, and this cannot disconnect X
 * since X was a leaf square of Q.
 * 
 * Our remaining case is that W is in Q and S is in neither P nor
 * Q. Again we take X out of Q and put it in P; we also add S to
 * Q. This ensures we do not expose an edge of P, but we must now
 * prove that S is adjacent to some other existing square of Q so
 * that we haven't disconnected Q by adding it.
 * 
 * To do this, we recall that we walked along the edge XE, and
 * then turned left to walk along XN. So just before doing all
 * that, we must have reached the corner XSE, and we must have
 * done it by walking along one of the three edges meeting at that
 * corner which are _not_ XE. It can't have been SY, since S would
 * then have been on our left and it isn't in Q; and it can't have
 * been XS, since S would then have been on our right and it isn't
 * in P. So it must have been YE, in which case Y was on our left,
 * and hence is in Q.
 * 
 * So in all cases we have shown that we can take X out of Q and
 * add it to P, and add at most one square to Q to restore the
 * containment and connectedness properties. Hence, we can keep
 * doing this until we run out of left turns and P becomes
 * rectangular. []
 * 
 * ------------
 * 
 * Anyway, that entire proof was a bit of a sidetrack. The point
 * is, although constructions of this type are possible for
 * sufficiently large k, divvy_rectangle() will never generate
 * them. This could be considered a weakness for some purposes, in
 * the sense that we can't generate all possible divisions.
 * However, there are many divisions which we are highly unlikely
 * to generate anyway, so in practice it probably isn't _too_ bad.
 * 
 * If I wanted to fix this issue, I would have to make the rules
 * more complicated for determining when a square can safely be
 * _removed_ from a polyomino. Adding one becomes easier (a square
 * may be added to a polyomino iff it is 4-adjacent to any square
 * currently part of the polyomino, and the current test for loop
 * formation may be dispensed with), but to determine which
 * squares may be removed we must now resort to analysis of the
 * overall structure of the polyomino rather than the simple local
 * properties we can currently get away with measuring.
 */

/*
 * Possible improvements which might cut the fail rate:
 * 
 *  - instead of picking one omino to extend in an iteration, try
 *    them all in succession (in a randomised order)
 * 
 *  - (for real rigour) instead of bfsing over ominoes, bfs over
 *    the space of possible _removed squares_. That way we aren't
 *    limited to randomly choosing a single square to remove from
 *    an omino and failing if that particular square doesn't
 *    happen to work.
 * 
 * However, I don't currently think it's necessary to do either of
 * these, because the failure rate is already low enough to be
 * easily tolerable, under all circumstances I've been able to
 * think of.
 */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>

#include "puzzles.h"

/*
 * Subroutine which implements a function used in computing both
 * whether a square can safely be added to an omino, and whether
 * it can safely be removed.
 * 
 * We enumerate the eight squares 8-adjacent to this one, in
 * cyclic order. We go round that loop and count the number of
 * times we find a square owned by the target omino next to one
 * not owned by it. We then return success iff that count is 2.
 * 
 * When adding a square to an omino, this is precisely the
 * criterion which tells us that adding the square won't leave a
 * hole in the middle of the omino. (If it did, then things get
 * more complicated; see above.)
 * 
 * When removing a square from an omino, the _same_ criterion
 * tells us that removing the square won't disconnect the omino.
 * (This only works _because_ we've ensured the omino is simply
 * connected.)
 */
static bool addremcommon(int w, int h, int x, int y, int *own, int val)
{
    int neighbours[8];
    int dir, count;

    for (dir = 0; dir < 8; dir++) {
	int dx = ((dir & 3) == 2 ? 0 : dir > 2 && dir < 6 ? +1 : -1);
	int dy = ((dir & 3) == 0 ? 0 : dir < 4 ? -1 : +1);
	int sx = x+dx, sy = y+dy;

	if (sx < 0 || sx >= w || sy < 0 || sy >= h)
	    neighbours[dir] = -1;      /* outside the grid */
	else
	    neighbours[dir] = own[sy*w+sx];
    }

    /*
     * To begin with, check 4-adjacency.
     */
    if (neighbours[0] != val && neighbours[2] != val &&
	neighbours[4] != val && neighbours[6] != val)
	return false;

    count = 0;

    for (dir = 0; dir < 8; dir++) {
	int next = (dir + 1) & 7;
        bool gotthis = (neighbours[dir] == val);
	bool gotnext = (neighbours[next] == val);

	if (gotthis != gotnext)
	    count++;
    }

    return (count == 2);
}

/*
 * w and h are the dimensions of the rectangle.
 * 
 * k is the size of the required ominoes. (So k must divide w*h,
 * of course.)
 * 
 * The returned result is a w*h-sized dsf.
 * 
 * In both of the above suggested use cases, the user would
 * probably want w==h==k, but that isn't a requirement.
 */
DSF *divvy_rectangle_attempt(int w, int h, int k, random_state *rs)
{
    int *order, *queue, *tmp, *own, *sizes, *addable;
    DSF *retdsf, *tmpdsf;
    bool *removable;
    int wh = w*h;
    int i, j, n, x, y, qhead, qtail;

    n = wh / k;
    assert(wh == k*n);

    order = snewn(wh, int);
    tmp = snewn(wh, int);
    own = snewn(wh, int);
    sizes = snewn(n, int);
    queue = snewn(n, int);
    addable = snewn(wh*4, int);
    removable = snewn(wh, bool);
    retdsf = tmpdsf = NULL;

    /*
     * Permute the grid squares into a random order, which will be
     * used for iterating over the grid whenever we need to search
     * for something. This prevents directional bias and arranges
     * for the answer to be non-deterministic.
     */
    for (i = 0; i < wh; i++)
	order[i] = i;
    shuffle(order, wh, sizeof(*order), rs);

    /*
     * Begin by choosing a starting square at random for each
     * omino.
     */
    for (i = 0; i < wh; i++) {
	own[i] = -1;
    }
    for (i = 0; i < n; i++) {
	own[order[i]] = i;
	sizes[i] = 1;
    }

    /*
     * Now repeatedly pick a random omino which isn't already at
     * the target size, and find a way to expand it by one. This
     * may involve stealing a square from another omino, in which
     * case we then re-expand that omino, forming a chain of
     * square-stealing which terminates in an as yet unclaimed
     * square. Hence every successful iteration around this loop
     * causes the number of unclaimed squares to drop by one, and
     * so the process is bounded in duration.
     */
    while (1) {

#ifdef DIVVY_DIAGNOSTICS
	{
	    int x, y;
	    printf("Top of loop. Current grid:\n");
	    for (y = 0; y < h; y++) {
		for (x = 0; x < w; x++)
		    printf("%3d", own[y*w+x]);
		printf("\n");
	    }
	}
#endif

	/*
	 * Go over the grid and figure out which squares can
	 * safely be added to, or removed from, each omino. We
	 * don't take account of other ominoes in this process, so
	 * we will often end up knowing that a square can be
	 * poached from one omino by another.
	 * 
	 * For each square, there may be up to four ominoes to
	 * which it can be added (those to which it is
	 * 4-adjacent).
	 */
	for (y = 0; y < h; y++) {
	    for (x = 0; x < w; x++) {
		int yx = y*w+x;
		int curr = own[yx];
		int dir;

		if (curr < 0) {
		    removable[yx] = false; /* can't remove if not owned! */
		} else if (sizes[curr] == 1) {
		    removable[yx] = true; /* can always remove a singleton */
		} else {
		    /*
		     * See if this square can be removed from its
		     * omino without disconnecting it.
		     */
		    removable[yx] = addremcommon(w, h, x, y, own, curr);
		}

		for (dir = 0; dir < 4; dir++) {
		    int dx = (dir == 0 ? -1 : dir == 1 ? +1 : 0);
		    int dy = (dir == 2 ? -1 : dir == 3 ? +1 : 0);
		    int sx = x + dx, sy = y + dy;
		    int syx = sy*w+sx;

		    addable[yx*4+dir] = -1;

		    if (sx < 0 || sx >= w || sy < 0 || sy >= h)
			continue;      /* no omino here! */
		    if (own[syx] < 0)
			continue;      /* also no omino here */
		    if (own[syx] == own[yx])
			continue;      /* we already got one */
		    if (!addremcommon(w, h, x, y, own, own[syx]))
			continue;      /* would non-simply connect the omino */
		    
		    addable[yx*4+dir] = own[syx];
		}
	    }
	}

	for (i = j = 0; i < n; i++)
	    if (sizes[i] < k)
		tmp[j++] = i;
	if (j == 0)
	    break;		       /* all ominoes are complete! */
	j = tmp[random_upto(rs, j)];
#ifdef DIVVY_DIAGNOSTICS
	printf("Trying to extend %d\n", j);
#endif

	/*
	 * So we're trying to expand omino j. We breadth-first
	 * search out from j across the space of ominoes.
	 * 
	 * For bfs purposes, we use two elements of tmp per omino:
	 * tmp[2*i+0] tells us which omino we got to i from, and
	 * tmp[2*i+1] numbers the grid square that omino stole
	 * from us.
	 * 
	 * This requires that wh (the size of tmp) is at least 2n,
	 * i.e. k is at least 2. There would have been nothing to
	 * stop a user calling this function with k=1, but if they
	 * did then we wouldn't have got to _here_ in the code -
	 * we would have noticed above that all ominoes were
	 * already at their target sizes, and terminated :-)
	 */
	assert(wh >= 2*n);
	for (i = 0; i < n; i++)
	    tmp[2*i] = tmp[2*i+1] = -1;
	qhead = qtail = 0;
	queue[qtail++] = j;
	tmp[2*j] = tmp[2*j+1] = -2;    /* special value: `starting point' */

	while (qhead < qtail) {
	    int tmpsq;

	    j = queue[qhead];

	    /*
	     * We wish to expand omino j. However, we might have
	     * got here by omino j having a square stolen from it,
	     * so first of all we must temporarily mark that
	     * square as not belonging to j, so that our adjacency
	     * calculations don't assume j _does_ belong to us.
	     */
	    tmpsq = tmp[2*j+1];
	    if (tmpsq >= 0) {
		assert(own[tmpsq] == j);
		own[tmpsq] = -3;
	    }

	    /*
	     * OK. Now begin by seeing if we can find any
	     * unclaimed square into which we can expand omino j.
	     * If we find one, the entire bfs terminates.
	     */
	    for (i = 0; i < wh; i++) {
		int dir;

		if (own[order[i]] != -1)
		    continue;	       /* this square is claimed */

		/*
		 * Special case: if our current omino was size 1
		 * and then had a square stolen from it, it's now
		 * size zero, which means it's valid to `expand'
		 * it into _any_ unclaimed square.
		 */
		if (sizes[j] == 1 && tmpsq >= 0)
		    break;	       /* got one */

		/*
		 * Failing that, we must do the full test for
		 * addability.
		 */
		for (dir = 0; dir < 4; dir++)
		    if (addable[order[i]*4+dir] == j) {
			/*
			 * We know this square is addable to this
			 * omino with the grid in the state it had
			 * at the top of the loop. However, we
			 * must now check that it's _still_
			 * addable to this omino when the omino is
			 * missing a square. To do this it's only
			 * necessary to re-check addremcommon.
			 */
			if (!addremcommon(w, h, order[i]%w, order[i]/w,
					  own, j))
			    continue;
			break;
		    }
		if (dir == 4)
		    continue;	       /* we can't add this square to j */

		break;		       /* got one! */
	    }
	    if (i < wh) {
		i = order[i];

		/*
		 * Restore the temporarily removed square _before_
		 * we start shifting ownerships about.
		 */
		if (tmpsq >= 0)
		    own[tmpsq] = j;

		/*
		 * We are done. We can add square i to omino j,
		 * and then backtrack along the trail in tmp
		 * moving squares between ominoes, ending up
		 * expanding our starting omino by one.
		 */
#ifdef DIVVY_DIAGNOSTICS
		printf("(%d,%d)", i%w, i/w);
#endif
		while (1) {
		    own[i] = j;
#ifdef DIVVY_DIAGNOSTICS
		    printf(" -> %d", j);
#endif
		    if (tmp[2*j] == -2)
			break;
		    i = tmp[2*j+1];
		    j = tmp[2*j];
#ifdef DIVVY_DIAGNOSTICS
		    printf("; (%d,%d)", i%w, i/w);
#endif
		}
#ifdef DIVVY_DIAGNOSTICS
		printf("\n");
#endif

		/*
		 * Increment the size of the starting omino.
		 */
		sizes[j]++;

		/*
		 * Terminate the bfs loop.
		 */
		break;
	    }

	    /*
	     * If we get here, we haven't been able to expand
	     * omino j into an unclaimed square. So now we begin
	     * to investigate expanding it into squares which are
	     * claimed by ominoes the bfs has not yet visited.
	     */
	    for (i = 0; i < wh; i++) {
		int dir, nj;

		nj = own[order[i]];
		if (nj < 0 || tmp[2*nj] != -1)
		    continue;	       /* unclaimed, or owned by wrong omino */
		if (!removable[order[i]])
		    continue;	       /* its omino won't let it go */

		for (dir = 0; dir < 4; dir++)
		    if (addable[order[i]*4+dir] == j) {
			/*
			 * As above, re-check addremcommon.
			 */
			if (!addremcommon(w, h, order[i]%w, order[i]/w,
					  own, j))
			    continue;

			/*
			 * We have found a square we can use to
			 * expand omino j, at the expense of the
			 * as-yet unvisited omino nj. So add this
			 * to the bfs queue.
			 */
			assert(qtail < n);
			queue[qtail++] = nj;
			tmp[2*nj] = j;
			tmp[2*nj+1] = order[i];

			/*
			 * Now terminate the loop over dir, to
			 * ensure we don't accidentally add the
			 * same omino twice to the queue.
			 */
			break;
		    }
	    }

	    /*
	     * Restore the temporarily removed square.
	     */
	    if (tmpsq >= 0)
		own[tmpsq] = j;

	    /*
	     * Advance the queue head.
	     */
	    qhead++;
	}

	if (qhead == qtail) {
	    /*
	     * We have finished the bfs and not found any way to
	     * expand omino j. Panic, and return failure.
	     * 
	     * FIXME: or should we loop over all ominoes before we
	     * give up?
	     */
#ifdef DIVVY_DIAGNOSTICS
	    printf("FAIL!\n");
#endif
	    retdsf = NULL;
	    goto cleanup;
	}
    }

#ifdef DIVVY_DIAGNOSTICS
    {
	int x, y;
	printf("SUCCESS! Final grid:\n");
	for (y = 0; y < h; y++) {
	    for (x = 0; x < w; x++)
		printf("%3d", own[y*w+x]);
	    printf("\n");
	}
    }
#endif

    /*
     * Construct the output dsf.
     */
    for (i = 0; i < wh; i++) {
	assert(own[i] >= 0 && own[i] < n);
	tmp[own[i]] = i;
    }
    retdsf = dsf_new(wh);
    for (i = 0; i < wh; i++) {
	dsf_merge(retdsf, i, tmp[own[i]]);
    }

    /*
     * Construct the output dsf a different way, to verify that
     * the ominoes really are k-ominoes and we haven't
     * accidentally split one into two disconnected pieces.
     */
    tmpdsf = dsf_new(wh);
    for (y = 0; y < h; y++)
	for (x = 0; x+1 < w; x++)
	    if (own[y*w+x] == own[y*w+(x+1)])
		dsf_merge(tmpdsf, y*w+x, y*w+(x+1));
    for (x = 0; x < w; x++)
	for (y = 0; y+1 < h; y++)
	    if (own[y*w+x] == own[(y+1)*w+x])
		dsf_merge(tmpdsf, y*w+x, (y+1)*w+x);
    for (i = 0; i < wh; i++) {
	j = dsf_canonify(retdsf, i);
	assert(dsf_equivalent(tmpdsf, j, i));
    }

    cleanup:

    /*
     * Free our temporary working space.
     */
    sfree(order);
    sfree(tmp);
    dsf_free(tmpdsf);
    sfree(own);
    sfree(sizes);
    sfree(queue);
    sfree(addable);
    sfree(removable);

    /*
     * And we're done.
     */
    return retdsf;
}

DSF *divvy_rectangle(int w, int h, int k, random_state *rs)
{
    DSF *ret;

    do {
	ret = divvy_rectangle_attempt(w, h, k, rs);
    } while (!ret);

    return ret;
}