shithub: puzzles

Download patch

ref: 191843e0c71da265df20e387913c22900b2b8ca7
parent: 7f00725c978bf79de91951f143cdfd2f9173041d
author: Simon Tatham <anakin@pobox.com>
date: Fri Apr 5 15:40:42 EDT 2019

Dominosa: add an Extreme difficulty, with forcing chains.

This technique borrows its name from the Solo / Map deduction in which
you find a path of vertices in your graph each of which has two
possible values, such that a choice for one end vertex of the chain
forces everything along it. In Dominosa, an approximate analogue is a
path of squares each of which has only two possible domino placements
remaining, and it has the extra-useful property that it's
bidirectional - once you've identified such a path, either all the odd
domino placements along it must be right, or all the even ones. So if
you can find an inconsistency in either set, you can rule out the
whole lot and settle on the other set.

Having done that basic analysis (which turns out to be surprisingly
easy with an edsf to help), there are multiple ways you can actually
rule out one of the same-parity chains. One is if the same domino
would have to appear twice in it; another is if the set of dominoes
that the chain would place would rule out all the choices for some
completely different square. There are probably others too, but those
are the ones I've implemented.

--- a/dominosa.R
+++ b/dominosa.R
@@ -1,6 +1,6 @@
 # -*- makefile -*-
 
-DOMINOSA_EXTRA = laydomino
+DOMINOSA_EXTRA = laydomino dsf sort
 
 dominosa : [X] GTK COMMON dominosa DOMINOSA_EXTRA dominosa-icon|no-icon
 
--- a/dominosa.c
+++ b/dominosa.c
@@ -7,15 +7,6 @@
 /*
  * Further possible deduction types in the solver:
  *
- *  * identify 'forcing chains', in the sense of any path of cells
- *    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 cell.
- *    Such a chain has the property that either all the odd dominoes
- *    are placed, or all the even ones are placed; so if either set of
- *    those introduces a conflict (e.g. a dupe within the chain, or
- *    using up all of some other square's choices), then the whole set
- *    can be ruled out, and the other set played immediately.
- *
  *  * rule out a domino placement if it would divide an unfilled
  *    region such that at least one resulting region had an odd area
  *     + Tarjan's bridge-finding algorithm would be a way to find
@@ -82,6 +73,7 @@
     X(TRIVIAL,Trivial,t)                        \
     X(BASIC,Basic,b)                            \
     X(HARD,Hard,h)                              \
+    X(EXTREME,Extreme,e)                        \
     X(AMBIGUOUS,Ambiguous,a)                    \
     /* end of list */
 #define ENUM(upper,title,lower) DIFF_ ## upper,
@@ -266,6 +258,8 @@
 #ifdef STANDALONE_SOLVER
 #define SOLVER_DIAGNOSTICS
 bool solver_diagnostics = false;
+#elif defined SOLVER_DIAGNOSTICS
+const bool solver_diagnostics = true;
 #endif
 
 struct solver_domino;
@@ -275,8 +269,8 @@
  * Information about a particular domino.
  */
 struct solver_domino {
-    /* The numbers on the domino. */
-    int lo, hi;
+    /* The numbers on the domino, and its index in the dominoes array. */
+    int lo, hi, index;
 
     /* List of placements not yet ruled out for this domino. */
     int nplacements;
@@ -293,6 +287,9 @@
  * that a domino might go in).
  */
 struct solver_placement {
+    /* The index of this placement in sc->placements. */
+    int index;
+
     /* The two squares that make up this placement. */
     struct solver_square *squares[2];
 
@@ -346,7 +343,7 @@
     struct solver_placement **domino_placement_lists;
     struct solver_square **squares_by_number;
     bool squares_by_number_initialised;
-    int *wh_scratch;
+    int *wh_scratch, *pc_scratch, *pc_scratch2, *dc_scratch;
 };
 
 static struct solver_scratch *solver_make_scratch(int n)
@@ -373,6 +370,7 @@
             assert(di == DINDEX(hi, lo));
             sc->dominoes[di].hi = hi;
             sc->dominoes[di].lo = lo;
+            sc->dominoes[di].index = di;
 
 #ifdef SOLVER_DIAGNOSTICS
             {
@@ -465,11 +463,17 @@
         }
     }
 
+    /* Fill in the index field of the placements */
+    for (pi = 0; pi < pc; pi++)
+        sc->placements[pi].index = pi;
+
     /* 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;
+    sc->pc_scratch = sc->pc_scratch2 = NULL;
+    sc->dc_scratch = NULL;
 
     return sc;
 }
@@ -493,6 +497,9 @@
     sfree(sc->domino_placement_lists);
     sfree(sc->squares_by_number);
     sfree(sc->wh_scratch);
+    sfree(sc->pc_scratch);
+    sfree(sc->pc_scratch2);
+    sfree(sc->dc_scratch);
     sfree(sc);
 }
 
@@ -860,7 +867,7 @@
          * Now sqs[0], ..., sqs[nsq-1] are the squares containing 'num'.
          */
 
-        if (nsq > sizeof(domino_sets)) {
+        if (nsq > lenof(domino_sets)) {
             /*
              * Abort this analysis if we're trying to enumerate all
              * the subsets of a too-large base set.
@@ -1080,6 +1087,247 @@
     return done_something;
 }
 
+static int forcing_chain_dup_cmp(const void *av, const void *bv, void *ctx)
+{
+    struct solver_scratch *sc = (struct solver_scratch *)ctx;
+    int a = *(const int *)av, b = *(const int *)bv;
+    int ac, bc;
+
+    ac = sc->pc_scratch[a];
+    bc = sc->pc_scratch[b];
+    if (ac != bc) return ac > bc ? +1 : -1;
+
+    ac = sc->placements[a].domino->index;
+    bc = sc->placements[b].domino->index;
+    if (ac != bc) return ac > bc ? +1 : -1;
+
+    return 0;
+}
+
+static int forcing_chain_sq_cmp(const void *av, const void *bv, void *ctx)
+{
+    struct solver_scratch *sc = (struct solver_scratch *)ctx;
+    int a = *(const int *)av, b = *(const int *)bv;
+    int ac, bc;
+
+    ac = sc->placements[a].domino->index;
+    bc = sc->placements[b].domino->index;
+    if (ac != bc) return ac > bc ? +1 : -1;
+
+    ac = sc->pc_scratch[a];
+    bc = sc->pc_scratch[b];
+    if (ac != bc) return ac > bc ? +1 : -1;
+
+    return 0;
+}
+
+static bool deduce_forcing_chain(struct solver_scratch *sc)
+{
+    int si, pi, di, j, k, m;
+    bool done_something = false;
+
+    if (!sc->wh_scratch)
+        sc->wh_scratch = snewn(sc->wh, int);
+    if (!sc->pc_scratch)
+        sc->pc_scratch = snewn(sc->pc, int);
+    if (!sc->pc_scratch2)
+        sc->pc_scratch2 = snewn(sc->pc, int);
+    if (!sc->dc_scratch)
+        sc->dc_scratch = snewn(sc->dc, int);
+
+    /*
+     * Start by identifying chains of placements which must all occur
+     * together if any of them occurs. We do this by making
+     * pc_scratch2 an edsf binding the placements into an equivalence
+     * class for each entire forcing chain, with the two possible sets
+     * of dominoes for the chain listed as inverses.
+     */
+    dsf_init(sc->pc_scratch2, sc->pc);
+    for (si = 0; si < sc->wh; si++) {
+        struct solver_square *sq = &sc->squares[si];
+        if (sq->nplacements == 2)
+            edsf_merge(sc->pc_scratch2,
+                       sq->placements[0]->index,
+                       sq->placements[1]->index, true);
+    }
+    /*
+     * Now read out the whole dsf into pc_scratch, flattening its
+     * structured data into a simple integer id per chain of dominoes
+     * that must occur together.
+     */
+    for (pi = 0; pi < sc->pc; pi++) {
+        bool inv;
+        int c = edsf_canonify(sc->pc_scratch2, pi, &inv);
+        sc->pc_scratch[pi] = c * 2 + (inv ? 1 : 0);
+    }
+
+    /*
+     * Identify chains that contain a duplicate domino, and rule them
+     * out. We do this by making a list of the placement indices in
+     * pc_scratch2, sorted by (chain id, domino id), so that dupes
+     * become adjacent.
+     */
+    for (pi = 0; pi < sc->pc; pi++)
+        sc->pc_scratch2[pi] = pi;
+    arraysort(sc->pc_scratch2, sc->pc, forcing_chain_dup_cmp, sc);
+
+    for (j = 0; j < sc->pc ;) {
+        struct solver_domino *duplicated_domino = NULL;
+
+        /*
+         * This loop iterates once per contiguous segment of the same
+         * value in pc_scratch2, i.e. once per chain.
+         */
+        int ci = sc->pc_scratch[sc->pc_scratch2[j]];
+        int climit, cstart = j;
+        while (j < sc->pc && sc->pc_scratch[sc->pc_scratch2[j]] == ci)
+            j++;
+        climit = j;
+
+        /*
+         * Now look for a duplicate domino within that chain.
+         */
+        for (k = cstart; k + 1 < climit; k++) {
+            struct solver_placement *p = &sc->placements[sc->pc_scratch2[k]];
+            struct solver_placement *q = &sc->placements[sc->pc_scratch2[k+1]];
+            if (p->domino == q->domino) {
+                duplicated_domino = p->domino;
+                break;
+            }
+        }
+
+        if (!duplicated_domino)
+            continue;
+
+#ifdef SOLVER_DIAGNOSTICS
+        if (solver_diagnostics) {
+            printf("domino %s occurs more than once in forced chain:",
+                   duplicated_domino->name);
+            for (k = cstart; k < climit; k++)
+                printf(" %s", sc->placements[sc->pc_scratch2[k]].name);
+            printf("\n");
+        }
+#endif
+
+        for (k = cstart; k < climit; k++)
+            rule_out_placement(sc, &sc->placements[sc->pc_scratch2[k]]);
+
+        done_something = true;
+    }
+
+    if (done_something)
+        return true;
+
+    /*
+     * A second way in which a whole forcing chain can be ruled out is
+     * if it contains all the dominoes that can occupy some other
+     * square, so that if the domnioes in the chain were all laid, the
+     * other square would be left without any choices.
+     *
+     * To detect this, we sort the placements again, this time by
+     * (domino index, chain index), so that we can easily find a
+     * sorted list of chains per domino. That allows us to iterate
+     * over the squares and check for a chain id common to all the
+     * placements of that square.
+     */
+    for (pi = 0; pi < sc->pc; pi++)
+        sc->pc_scratch2[pi] = pi;
+    arraysort(sc->pc_scratch2, sc->pc, forcing_chain_sq_cmp, sc);
+
+    /* Store a lookup table of the first entry in pc_scratch2
+     * corresponding to each domino. */
+    for (di = j = 0; j < sc->pc; j++) {
+        while (di <= sc->placements[sc->pc_scratch2[j]].domino->index) {
+            assert(di < sc->dc);
+            sc->dc_scratch[di++] = j;
+        }
+    }
+    assert(di == sc->dc);
+
+    for (si = 0; si < sc->wh; si++) {
+        struct solver_square *sq = &sc->squares[si];
+        int listpos = 0, listsize = 0, listout = 0;
+        int exclude[4];
+        int n_exclude;
+
+        if (sq->nplacements < 2)
+            continue;              /* too simple to be worth trying */
+
+        /*
+         * Start by checking for chains this square can actually form
+         * part of. We won't consider those. (The aim is to find a
+         * completely _different_ square whose placements are all
+         * ruled out by a chain.)
+         */
+        assert(sq->nplacements <= lenof(exclude));
+        for (j = n_exclude = 0; j < sq->nplacements; j++)
+            exclude[n_exclude++] = sc->pc_scratch[sq->placements[j]->index];
+
+        for (j = 0; j < sq->nplacements; j++) {
+            struct solver_domino *d = sq->placements[j]->domino;
+
+            listout = listpos = 0;
+
+            for (k = sc->dc_scratch[d->index];
+                 k < sc->pc && sc->placements[sc->pc_scratch2[k]].domino == d;
+                 k++) {
+                int chain = sc->pc_scratch[sc->pc_scratch2[k]];
+                bool keep;
+
+                if (!sc->placements[sc->pc_scratch2[k]].active)
+                    continue;
+
+                if (j == 0) {
+                    keep = true;
+                } else {
+                    while (listpos < listsize &&
+                           sc->wh_scratch[listpos] < chain)
+                        listpos++;
+                    keep = (listpos < listsize &&
+                            sc->wh_scratch[listpos] == chain);
+                }
+
+                for (m = 0; m < n_exclude; m++)
+                    if (chain == exclude[m])
+                        keep = false;
+
+                if (keep)
+                    sc->wh_scratch[listout++] = chain;
+            }
+
+            listsize = listout;
+            if (listsize == 0)
+                break; /* ruled out all chains; terminate loop early */
+        }
+
+        for (listpos = 0; listpos < listsize; listpos++) {
+            int chain = sc->wh_scratch[listpos];
+
+            /*
+             * We've found a chain we can rule out.
+             */
+#ifdef SOLVER_DIAGNOSTICS
+            if (solver_diagnostics) {
+                printf("all choices for square %s would be ruled out "
+                       "by forced chain:", sq->name);
+                for (pi = 0; pi < sc->pc; pi++)
+                    if (sc->pc_scratch[pi] == chain)
+                        printf(" %s", sc->placements[pi].name);
+                printf("\n");
+            }
+#endif
+
+            for (pi = 0; pi < sc->pc; pi++)
+                if (sc->pc_scratch[pi] == chain)
+                    rule_out_placement(sc, &sc->placements[pi]);
+
+            done_something = true;
+        }
+    }
+
+    return done_something;
+}
+
 /*
  * Run the solver until it can't make any more progress.
  *
@@ -1161,6 +1409,16 @@
             done_something = true;
         if (done_something) {
             sc->max_diff_used = max(sc->max_diff_used, DIFF_HARD);
+            continue;
+        }
+
+        if (max_diff_allowed <= DIFF_HARD)
+            continue;
+
+        if (deduce_forcing_chain(sc))
+            done_something = true;
+        if (done_something) {
+            sc->max_diff_used = max(sc->max_diff_used, DIFF_EXTREME);
             continue;
         }