chiark / gitweb /
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.