Dominosa: add a Hard difficulty which can do set analysis.

This is another thing I've been doing with my own brain for ages as a
more interesting alternative to scouring the grid for the simpler
deduction that must be there somewhere - but now the solver can
understand it too, so it can generate puzzles that _need_ it (or at
least need something beyond the simpler strategies it understands).
This commit is contained in:
Simon Tatham
2019-04-03 18:16:25 +01:00
parent 2ec6daee32
commit e2f52df5ec

View File

@ -23,25 +23,6 @@
* squares should be sufficient to determine the area parity * squares should be sufficient to determine the area parity
* of the region that any such placement would cut off. * of the region that any such placement would cut off.
* *
* * set analysis
* + look at all unclaimed squares containing a given number
* + for each one, find the set of possible numbers that it
* can connect to (i.e. each neighbouring tile such that
* the placement between it and that neighbour has not yet
* been ruled out)
* + now proceed similarly to Solo set analysis: try to find
* a subset of the squares such that the union of their
* possible numbers is the same size as the subset. If so,
* rule out those possible numbers for all other squares.
* * important wrinkle: the double dominoes complicate
* matters. Connecting a number to itself uses up _two_
* of the unclaimed squares containing a number. Thus,
* when finding the initial subset we must never
* include two adjacent squares; and also, when ruling
* things out after finding the subset, we must be
* careful that we don't rule out precisely the domino
* placement that was _included_ in our set!
*
* * identify 'forcing chains', in the sense of any path of cells * * identify 'forcing chains', in the sense of any path of cells
* each of which has only two possible dominoes to be part of, * each of which has only two possible dominoes to be part of,
* and each of those rules out one of the choices for the next * and each of those rules out one of the choices for the next
@ -78,6 +59,7 @@
#define DIFFLIST(X) \ #define DIFFLIST(X) \
X(TRIVIAL,Trivial,t) \ X(TRIVIAL,Trivial,t) \
X(BASIC,Basic,b) \ X(BASIC,Basic,b) \
X(HARD,Hard,h) \
X(AMBIGUOUS,Ambiguous,a) \ X(AMBIGUOUS,Ambiguous,a) \
/* end of list */ /* end of list */
#define ENUM(upper,title,lower) DIFF_ ## upper, #define ENUM(upper,title,lower) DIFF_ ## upper,
@ -340,6 +322,9 @@ struct solver_scratch {
struct solver_placement *placements; struct solver_placement *placements;
struct solver_square *squares; struct solver_square *squares;
struct solver_placement **domino_placement_lists; struct solver_placement **domino_placement_lists;
struct solver_square **squares_by_number;
bool squares_by_number_initialised;
int *wh_scratch;
}; };
static struct solver_scratch *solver_make_scratch(int n) static struct solver_scratch *solver_make_scratch(int n)
@ -458,6 +443,12 @@ static struct solver_scratch *solver_make_scratch(int n)
} }
} }
/* Lazily initialised by particular solver techniques that might
* never be needed */
sc->squares_by_number = NULL;
sc->squares_by_number_initialised = false;
sc->wh_scratch = NULL;
return sc; return sc;
} }
@ -478,6 +469,8 @@ static void solver_free_scratch(struct solver_scratch *sc)
sfree(sc->placements); sfree(sc->placements);
sfree(sc->squares); sfree(sc->squares);
sfree(sc->domino_placement_lists); sfree(sc->domino_placement_lists);
sfree(sc->squares_by_number);
sfree(sc->wh_scratch);
sfree(sc); sfree(sc);
} }
@ -524,6 +517,7 @@ static void solver_setup_grid(struct solver_scratch *sc, const int *numbers)
} }
sc->max_diff_used = DIFF_TRIVIAL; sc->max_diff_used = DIFF_TRIVIAL;
sc->squares_by_number_initialised = false;
} }
/* Given two placements p,q that overlap, returns si such that /* Given two placements p,q that overlap, returns si such that
@ -535,6 +529,15 @@ static int common_square_index(struct solver_placement *p,
p->squares[0] == q->squares[1]) ? 0 : 1; p->squares[0] == q->squares[1]) ? 0 : 1;
} }
/* Sort function used to set up squares_by_number */
static int squares_by_number_cmpfn(const void *av, const void *bv)
{
struct solver_square *a = *(struct solver_square *const *)av;
struct solver_square *b = *(struct solver_square *const *)bv;
return (a->number < b->number ? -1 : a->number > b->number ? +1 :
a->index < b->index ? -1 : a->index > b->index ? +1 : 0);
}
static void rule_out_placement( static void rule_out_placement(
struct solver_scratch *sc, struct solver_placement *p) struct solver_scratch *sc, struct solver_placement *p)
{ {
@ -740,7 +743,6 @@ static bool deduce_domino_must_overlap(struct solver_scratch *sc, int di)
return true; return true;
} }
/* /*
* If a placement of domino D overlaps the only remaining placement * If a placement of domino D overlaps the only remaining placement
* for some square S which is not also for domino D, then placing D * for some square S which is not also for domino D, then placing D
@ -787,6 +789,246 @@ static bool deduce_local_duplicate(struct solver_scratch *sc, int pi)
return false; return false;
} }
/*
* If we can find a set S of mutually non-adjacent squares all
* containing the same number, such that the set of possible dominoes
* for all those squares put together has the same size as S, then all
* the dominoes in that set _must_ overlap a square of S and we can
* rule out any other placements for them.
*/
static bool deduce_set_simple(struct solver_scratch *sc)
{
struct solver_square **sqs, **sqp, **sqe;
int num, nsq, i, j;
unsigned long domino_sets[16], adjacent[16];
struct solver_domino *ds[16];
bool done_something = false;
if (!sc->squares_by_number)
sc->squares_by_number = snewn(sc->wh, struct solver_square *);
if (!sc->wh_scratch)
sc->wh_scratch = snewn(sc->wh, int);
if (!sc->squares_by_number_initialised) {
/*
* If this is the first call to this function for a given
* grid, start by sorting the squares by their containing
* number.
*/
for (i = 0; i < sc->wh; i++)
sc->squares_by_number[i] = &sc->squares[i];
qsort(sc->squares_by_number, sc->wh, sizeof(*sc->squares_by_number),
squares_by_number_cmpfn);
}
sqp = sc->squares_by_number;
sqe = sc->squares_by_number + sc->wh;
for (num = 0; num <= sc->n; num++) {
unsigned long squares;
unsigned long squares_done;
/* Find the bounds of the subinterval of squares_by_number
* containing squares with this particular number. */
sqs = sqp;
while (sqp < sqe && (*sqp)->number == num)
sqp++;
nsq = sqp - sqs;
/*
* Now sqs[0], ..., sqs[nsq-1] are the squares containing 'num'.
*/
if (nsq > sizeof(domino_sets)) {
/*
* Abort this analysis if we're trying to enumerate all
* the subsets of a too-large base set.
*
* This _shouldn't_ happen, at the time of writing this
* code, because the largest puzzle we support is only
* supposed to have 10 instances of each number, and part
* of our input grid validation checks that each number
* does appear the right number of times. But just in case
* weird test input makes its way to this function, or the
* puzzle sizes are expanded later, it's easy enough to
* just rule out doing this analysis for overlarge sets of
* numbers.
*/
continue;
}
/*
* Index the squares in wh_scratch, which we're using as a
* lookup table to map the official index of a square back to
* its value in our local indexing scheme.
*/
for (i = 0; i < nsq; i++)
sc->wh_scratch[sqs[i]->index] = i;
/*
* For each square, make a bit mask of the dominoes that can
* overlap it, by finding the number at the other end of each
* one.
*
* Also, for each square, make a bit mask of other squares in
* the current list that might occupy the _same_ domino
* (because a possible placement of a double overlaps both).
* We'll need that for evaluating whether sets are properly
* exhaustive.
*/
for (i = 0; i < nsq; i++)
adjacent[i] = 0;
for (i = 0; i < nsq; i++) {
struct solver_square *sq = sqs[i];
unsigned long mask = 0;
for (j = 0; j < sq->nplacements; j++) {
struct solver_placement *p = sq->placements[j];
int othernum = p->domino->lo + p->domino->hi - num;
mask |= 1UL << othernum;
ds[othernum] = p->domino; /* so we can find them later */
if (othernum == num) {
/*
* Special case: this is a double, so it gives
* rise to entries in adjacent[].
*/
int i2 = sc->wh_scratch[p->squares[0]->index +
p->squares[1]->index - sq->index];
adjacent[i] |= 1UL << i2;
adjacent[i2] |= 1UL << i;
}
}
domino_sets[i] = mask;
}
squares_done = 0;
for (squares = 0; squares < (1UL << nsq); squares++) {
unsigned long dominoes = 0;
int bitpos, nsquares, ndominoes;
bool reported = false;
/*
* We don't do set analysis on the same square of the grid
* more than once in this loop. Otherwise you generate
* pointlessly overcomplicated diagnostics for simpler
* follow-up deductions. For example, suppose squares
* {A,B} must go with dominoes {X,Y}. So you rule out X,Y
* elsewhere, and then it turns out square C (from which
* one of those was eliminated) has only one remaining
* possibility Z. What you _don't_ want to do is
* triumphantly report a second case of set elimination
* where you say 'And also, squares {A,B,C} have to be
* {X,Y,Z}!' You'd prefer to give 'now C has to be Z' as a
* separate deduction later, more simply phrased.
*/
if (squares & squares_done)
continue;
nsquares = 0;
/* Make the set of dominoes that these squares can inhabit. */
for (bitpos = 0; bitpos < nsq; bitpos++) {
if (!(1 & (squares >> bitpos)))
continue; /* this bit isn't set in the mask */
if (adjacent[bitpos] & squares)
goto skip_subset; /* contains two adjacent squares */
dominoes |= domino_sets[bitpos];
nsquares++;
}
/* Count them. */
ndominoes = 0;
for (bitpos = 0; bitpos < nsq; bitpos++)
ndominoes += 1 & (dominoes >> bitpos);
/* Are the two sets equal in size? */
if (nsquares != ndominoes)
continue;
/* Skip sets of size 1, or whose complement has size 1.
* Those can be handled by a simpler analysis, and should
* be, for more sensible solver diagnostics. */
if (nsquares <= 1 || nsquares >= nsq-1)
continue;
/*
* We've found a set! That means we can rule out any
* placement of any of the dominoes in that set which do
* not include one of our squares.
*
* We may or may not actually end up ruling anything out
* here. But even if we don't, we should record that these
* squares form a self-contained set, so that we don't
* pointlessly report a superset of them later which could
* instead be reported as just the other ones.
*/
squares_done |= squares;
for (bitpos = 0; bitpos < nsq; bitpos++) {
struct solver_domino *d;
if (!(1 & (dominoes >> bitpos)))
continue;
d = ds[bitpos];
for (i = d->nplacements; i-- > 0 ;) {
struct solver_placement *p = d->placements[i];
int si;
for (si = 0; si < 2; si++) {
struct solver_square *sq2 = p->squares[si];
if (sq2->number == num &&
(1 & (squares >> sc->wh_scratch[sq2->index]))) {
/* This placement uses one of our squares.
* Leave it in. */
goto skip_placement;
}
}
if (!reported) {
reported = true;
done_something = true;
#ifdef SOLVER_DIAGNOSTICS
if (solver_diagnostics) {
int b;
const char *sep;
printf("squares {");
for (sep = "", b = 0; b < nsq; b++)
if (1 & (squares >> b)) {
printf("%s%s", sep, sqs[b]->name);
sep = ",";
}
printf("} have to contain dominoes {");
for (sep = "", b = 0; b < nsq; b++)
if (1 & (dominoes >> b)) {
printf("%s%s", sep, ds[b]->name);
sep = ",";
}
printf("}\n");
}
#endif
}
rule_out_placement(sc, p);
skip_placement:;
}
}
skip_subset:;
}
}
return done_something;
}
/* /*
* Run the solver until it can't make any more progress. * Run the solver until it can't make any more progress.
* *
@ -861,6 +1103,16 @@ static int run_solver(struct solver_scratch *sc, int max_diff_allowed)
continue; continue;
} }
if (max_diff_allowed <= DIFF_BASIC)
continue;
if (deduce_set_simple(sc))
done_something = true;
if (done_something) {
sc->max_diff_used = max(sc->max_diff_used, DIFF_HARD);
continue;
}
} while (done_something); } while (done_something);
#ifdef SOLVER_DIAGNOSTICS #ifdef SOLVER_DIAGNOSTICS