ref: d724e136663ed028aa6429f0bc5cbf93585f5aa0
parent: 4f2f8a9d173f34097a1636a3205ca0d50a39efee
author: Simon Tatham <anakin@pobox.com>
date: Wed Feb 26 01:18:51 EST 2020
Tracks: new parity-based deduction. This is another deduction I've known about in principle for ages but the game didn't implement. In the simplest case, it's obvious: if you can draw a line across the grid that separates the track endpoints from each other, and the track doesn't yet cross that line at all, then it's going to have to cross it at _some_ point. So when you've narrowed down to only one possible crossing place, you can fill it in as definite. IF the track already crosses your line and goes back again, the same rule still applies: _some_ part of your track is on one side of the line, and needs to get to the other. A more sensible way of expressing this is to say that the track must cross the boundary an _odd_ number of times if the two endpoints are on opposite sides of it. And conversely, if you've drawn a line across the grid that both endpoints are on the _same_ side of, then the track must cross it an _even_ number of times - every time it goes to the 'wrong' side (where the endpoints aren't), it will have to come back again eventually. But this doesn't just apply to a _line_ across the grid. You can pick any subset you like of the squares of the grid, and imagine the closed loop bounding that subset as your 'line'. (Or the _set_ of closed loops, in the most general case, because your subset doesn't _have_ to be simply connected - or even connected at all - to make this argument valid.) If your boundary is a closed loop, then both endpoints are always on the same side of that boundary - namely, the outside - and so the track has to cross the boundary an even number of times. So any time you can identify such a subset in which all but one boundary edge is already filled in, you can fill in the last one by parity. (This most general boundary-based system takes in all the previous cases as special cases. In the original case where it looks as if you need odd parity for a line across the grid separating the endpoints, what you're really doing is drawing a closed loop around one half of the grid, and considering the actual endpoint itself to be the place where the track leaves that region again - so, looked at that way, the parity is back to even.) The tricky part of implementing this is to avoid having to iterate over all subsets of the grid looking for one whose boundary has the right property. Luckily, we don't have to: a nice way to look at it is to define a graph whose vertices are grid squares, with neighbouring squares joined by a _graph_ edge if the _grid_ edge between those squares is not yet in a known state. Then we're looking for an edge of that graph which if removed would break it up into more separate components than it's already in. That is, we want a _bridge_ in the graph - which we can find all of in linear time using Tarjan's bridge-finding algorithm, conveniently implemented already in this collection in findloop.c. Having found a bridge edge of that graph, you imagine removing it, and find one of the two connected components it's just broken its previous component up into. That's your subset of grid squares, and now you can count track crossings around the boundary and fill in the bridge edge by parity. When I actually came to implement this, it turned out that the very first puzzle it generated was actually hard for me to solve, because as it turns out, this general analysis is much better at identifying opportunities to use this deduction than I am. A straight line right across the grid is often obvious: a few squares tucked into a complicated half-solved piece of the worldl, not so much. So I'm introducing a new Hard difficulty level, and putting this solution technique in there.
--- a/tracks.c
+++ b/tracks.c
@@ -30,9 +30,11 @@
* Difficulty levels. I do some macro ickery here to ensure that my
* enum and the various forms of my name list always match up.
*/
-#define DIFFLIST(A) \
- A(EASY,Easy,e) \
- A(TRICKY,Tricky,t)
+#define DIFFLIST(A) \
+ A(EASY,Easy,e) \
+ A(TRICKY,Tricky,t) \
+ A(HARD,Hard,h) \
+ /* end of list */
#define ENUM(upper,title,lower) DIFF_ ## upper,
#define TITLE(upper,title,lower) #title,
@@ -66,10 +68,12 @@
{10, 8, DIFF_TRICKY, 1 },
{10, 10, DIFF_EASY, 1},
{10, 10, DIFF_TRICKY, 1},
+ {10, 10, DIFF_HARD, 1},
{15, 10, DIFF_EASY, 1},
{15, 10, DIFF_TRICKY, 1},
{15, 15, DIFF_EASY, 1},
{15, 15, DIFF_TRICKY, 1},
+ {15, 15, DIFF_HARD, 1},
};
static bool game_fetch_preset(int i, char **name, game_params **params)
@@ -898,6 +902,10 @@
return state;
}
+struct solver_scratch {
+ int *dsf;
+};
+
static int solve_set_sflag(game_state *state, int x, int y,
unsigned int f, const char *why)
{
@@ -1379,11 +1387,146 @@
solve_set_eflag(state, x, y, d, E_NOTRACK, "outer edge");
}
+static int solve_bridge_sub(game_state *state, int x, int y, int d,
+ struct solver_scratch *sc)
+{
+ /*
+ * Imagine a graph on the squares of the grid, with an edge
+ * connecting neighbouring squares only if it's not yet known
+ * whether there's a track between them.
+ *
+ * This function is called if the edge between x,y and X,Y is a
+ * bridge in that graph: that is, it's not part of any loop in the
+ * graph, or equivalently, removing it would increase the number
+ * of connected components in the graph.
+ *
+ * In that situation, we can fill in the edge by a parity
+ * argument. Construct a closed loop of edges in the grid, all of
+ * whose states are known except this one. The track starts and
+ * ends outside this loop, so it must cross the boundary of the
+ * loop an even number of times. So if we count up how many times
+ * the track is known to cross the edges of our loop, then we can
+ * fill in the last edge in whichever way makes that number even.
+ *
+ * In fact, there's not even any need to go to the effort of
+ * constructing a _single_ closed loop. The simplest thing is to
+ * delete the bridge edge from the graph, find a connected
+ * component of the reduced graph whose boundary includes that
+ * edge, and take every edge separating that component from
+ * another. This may not lead to _exactly one_ cycle - the
+ * component could be non-simply connected and have a hole in the
+ * middle - but that doesn't matter, because the same parity
+ * constraint applies just as well with more than one disjoint
+ * loop.
+ */
+ int w = state->p.w, h = state->p.h, wh = w*h;
+ int X = x + DX(d), Y = y + DY(d);
+ int xi, yi, di;
+
+ assert(d == D || d == R);
+
+ if (!sc->dsf)
+ sc->dsf = snew_dsf(wh);
+ dsf_init(sc->dsf, wh);
+
+ for (xi = 0; xi < w; xi++) {
+ for (yi = 0; yi < h; yi++) {
+ /* We expect to have been called with X,Y either to the
+ * right of x,y or below it, not the other way round. If
+ * that were not true, the tests in this loop to exclude
+ * the bridge edge would have to be twice as annoying. */
+
+ if (yi+1 < h && !S_E_FLAGS(state, xi, yi, D) &&
+ !(xi == x && yi == y && xi == X && yi+1 == Y))
+ dsf_merge(sc->dsf, yi*w+xi, (yi+1)*w+xi);
+
+ if (xi+1 < w && !S_E_FLAGS(state, xi, yi, R) &&
+ !(xi == x && yi == y && xi+1 == X && yi == Y))
+ dsf_merge(sc->dsf, yi*w+xi, yi*w+(xi+1));
+ }
+ }
+
+ int component = dsf_canonify(sc->dsf, y*w+x);
+ int parity = 0;
+ for (xi = 0; xi < w; xi++) {
+ for (yi = 0; yi < h; yi++) {
+ if (dsf_canonify(sc->dsf, yi*w+xi) != component)
+ continue;
+ for (di = 1; di < 16; di *= 2) {
+ int Xi = xi + DX(di), Yi = yi + DY(di);
+ if ((Xi < 0 || Xi >= w || Yi < 0 || Yi >= h ||
+ dsf_canonify(sc->dsf, Yi*w+Xi) != component) &&
+ (S_E_DIRS(state, xi, yi, E_TRACK) & di))
+ parity ^= 1;
+ }
+ }
+ }
+
+ solve_set_eflag(state, x, y, d, parity ? E_TRACK : E_NOTRACK, "parity");
+ return 1;
+}
+
+struct solve_bridge_neighbour_ctx {
+ game_state *state;
+ int x, y, dirs;
+};
+static int solve_bridge_neighbour(int vertex, void *vctx)
+{
+ struct solve_bridge_neighbour_ctx *ctx =
+ (struct solve_bridge_neighbour_ctx *)vctx;
+ int w = ctx->state->p.w;
+
+ if (vertex >= 0) {
+ ctx->x = vertex % w;
+ ctx->y = vertex / w;
+ ctx->dirs = ALLDIR
+ & ~S_E_DIRS(ctx->state, ctx->x, ctx->y, E_TRACK)
+ & ~S_E_DIRS(ctx->state, ctx->x, ctx->y, E_NOTRACK);
+ }
+ unsigned dir = ctx->dirs & -ctx->dirs; /* isolate lowest set bit */
+ if (!dir)
+ return -1;
+ ctx->dirs &= ~dir;
+ int xr = ctx->x + DX(dir), yr = ctx->y + DY(dir);
+ assert(0 <= xr && xr < w);
+ assert(0 <= yr && yr < ctx->state->p.h);
+ return yr * w + xr;
+}
+
+static int solve_check_bridge_parity(game_state *state,
+ struct solver_scratch *sc)
+{
+ int w = state->p.w, h = state->p.h, wh = w*h;
+ struct findloopstate *fls;
+ struct solve_bridge_neighbour_ctx ctx[1];
+ int x, y, did = 0;
+
+ ctx->state = state;
+ fls = findloop_new_state(wh);
+ findloop_run(fls, wh, solve_bridge_neighbour, ctx);
+
+ for (x = 0; x < w; x++) {
+ for (y = 0; y < h; y++) {
+ if (y+1 < h && !findloop_is_loop_edge(fls, y*w+x, (y+1)*w+x))
+ did += solve_bridge_sub(state, x, y, D, sc);
+ if (x+1 < w && !findloop_is_loop_edge(fls, y*w+x, y*w+(x+1)))
+ did += solve_bridge_sub(state, x, y, R, sc);
+ }
+ }
+
+ findloop_free_state(fls);
+
+ return did;
+}
+
static int tracks_solve(game_state *state, int diff, int *max_diff_out)
{
int x, y, w = state->p.w, h = state->p.h;
+ struct solver_scratch sc[1];
int max_diff = DIFF_EASY;
+ sc->dsf = NULL;
+
debug(("solve..."));
state->impossible = false;
@@ -1415,11 +1558,15 @@
TRY(DIFF_TRICKY, solve_check_loose_ends(state));
TRY(DIFF_TRICKY, solve_check_neighbours(state));
+ TRY(DIFF_HARD, solve_check_neighbours(state, true));
+ TRY(DIFF_HARD, solve_check_bridge_parity(state, sc));
#undef TRY
break;
}
+
+ sfree(sc->dsf);
if (max_diff_out)
*max_diff_out = max_diff;