mirror of
git://git.tartarus.org/simon/puzzles.git
synced 2025-04-20 15:41:30 -07:00
Remove the long comment at the end of findloop.c.
This week I expanded that comment into a blog post: https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/findloop/ which improves on the comment in three ways: 1. diagrams 2. adds a further reason why the footpath-dsf algorithm was unsatisfactory, pointed out by a Mastodon comment after I published the original version of the blog post 3. adds the punchline that the loop tracing approach _could_ have been made to work after all! So I've deleted the comment and replaced it with a link to the article.
This commit is contained in:
176
findloop.c
176
findloop.c
@ -9,6 +9,10 @@
|
|||||||
* are precisely those which _wouldn't_ disconnect anything if removed
|
* are precisely those which _wouldn't_ disconnect anything if removed
|
||||||
* (individually) - but of course flipping the sense of the output is
|
* (individually) - but of course flipping the sense of the output is
|
||||||
* easy.
|
* easy.
|
||||||
|
*
|
||||||
|
* For some fun background reading about all the _wrong_ ways the
|
||||||
|
* Puzzles code base has tried to solve this problem in the past:
|
||||||
|
* https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/findloop/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "puzzles.h"
|
#include "puzzles.h"
|
||||||
@ -356,175 +360,3 @@ bool findloop_run(struct findloopstate *pv, int nvertices,
|
|||||||
*/
|
*/
|
||||||
return nbridges < nedges;
|
return nbridges < nedges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Appendix: the long and painful history of loop detection in these puzzles
|
|
||||||
* =========================================================================
|
|
||||||
*
|
|
||||||
* For interest, I thought I'd write up the five loop-finding methods
|
|
||||||
* I've gone through before getting to this algorithm. It's a case
|
|
||||||
* study in all the ways you can solve this particular problem
|
|
||||||
* wrongly, and also how much effort you can waste by not managing to
|
|
||||||
* find the existing solution in the literature :-(
|
|
||||||
*
|
|
||||||
* Vertex dsf
|
|
||||||
* ----------
|
|
||||||
*
|
|
||||||
* Initially, in puzzles where you need to not have any loops in the
|
|
||||||
* solution graph, I detected them by using a dsf to track connected
|
|
||||||
* components of vertices. Iterate over each edge unifying the two
|
|
||||||
* vertices it connects; but before that, check if the two vertices
|
|
||||||
* are _already_ known to be connected. If so, then the new edge is
|
|
||||||
* providing a second path between them, i.e. a loop exists.
|
|
||||||
*
|
|
||||||
* That's adequate for automated solvers, where you just need to know
|
|
||||||
* _whether_ a loop exists, so as to rule out that move and do
|
|
||||||
* something else. But during play, you want to do better than that:
|
|
||||||
* you want to _point out_ the loops with error highlighting.
|
|
||||||
*
|
|
||||||
* Graph pruning
|
|
||||||
* -------------
|
|
||||||
*
|
|
||||||
* So my second attempt worked by iteratively pruning the graph. Find
|
|
||||||
* a vertex with degree 1; remove that edge; repeat until you can't
|
|
||||||
* find such a vertex any more. This procedure will remove *every*
|
|
||||||
* edge of the graph if and only if there were no loops; so if there
|
|
||||||
* are any edges remaining, highlight them.
|
|
||||||
*
|
|
||||||
* This successfully highlights loops, but not _only_ loops. If the
|
|
||||||
* graph contains a 'dumb-bell' shaped subgraph consisting of two
|
|
||||||
* loops connected by a path, then we'll end up highlighting the
|
|
||||||
* connecting path as well as the loops. That's not what we wanted.
|
|
||||||
*
|
|
||||||
* Vertex dsf with ad-hoc loop tracing
|
|
||||||
* -----------------------------------
|
|
||||||
*
|
|
||||||
* So my third attempt was to go back to the dsf strategy, only this
|
|
||||||
* time, when you detect that a particular edge connects two
|
|
||||||
* already-connected vertices (and hence is part of a loop), you try
|
|
||||||
* to trace round that loop to highlight it - before adding the new
|
|
||||||
* edge, search for a path between its endpoints among the edges the
|
|
||||||
* algorithm has already visited, and when you find one (which you
|
|
||||||
* must), highlight the loop consisting of that path plus the new
|
|
||||||
* edge.
|
|
||||||
*
|
|
||||||
* This solves the dumb-bell problem - we definitely now cannot
|
|
||||||
* accidentally highlight any edge that is *not* part of a loop. But
|
|
||||||
* it's far from clear that we'll highlight *every* edge that *is*
|
|
||||||
* part of a loop - what if there were multiple paths between the two
|
|
||||||
* vertices? It would be difficult to guarantee that we'd always catch
|
|
||||||
* every single one.
|
|
||||||
*
|
|
||||||
* On the other hand, it is at least guaranteed that we'll highlight
|
|
||||||
* _something_ if any loop exists, and in other error highlighting
|
|
||||||
* situations (see in particular the Tents connected component
|
|
||||||
* analysis) I've been known to consider that sufficient. So this
|
|
||||||
* version hung around for quite a while, until I had a better idea.
|
|
||||||
*
|
|
||||||
* Face dsf
|
|
||||||
* --------
|
|
||||||
*
|
|
||||||
* Round about the time Loopy was being revamped to include non-square
|
|
||||||
* grids, I had a much cuter idea, making use of the fact that the
|
|
||||||
* graph is planar, and hence has a concept of faces.
|
|
||||||
*
|
|
||||||
* In Loopy, there are really two graphs: the 'grid', consisting of
|
|
||||||
* all the edges that the player *might* fill in, and the solution
|
|
||||||
* graph of the edges the player actually *has* filled in. The
|
|
||||||
* algorithm is: set up a dsf on the *faces* of the grid. Iterate over
|
|
||||||
* each edge of the grid which is _not_ marked by the player as an
|
|
||||||
* edge of the solution graph, unifying the faces on either side of
|
|
||||||
* that edge. This groups the faces into connected components. Now,
|
|
||||||
* there is more than one connected component iff a loop exists, and
|
|
||||||
* moreover, an edge of the solution graph is part of a loop iff the
|
|
||||||
* faces on either side of it are in different connected components!
|
|
||||||
*
|
|
||||||
* This is the first algorithm I came up with that I was confident
|
|
||||||
* would successfully highlight exactly the correct set of edges in
|
|
||||||
* all cases. It's also conceptually elegant, and very easy to
|
|
||||||
* implement and to be confident you've got it right (since it just
|
|
||||||
* consists of two very simple loops over the edge set, one building
|
|
||||||
* the dsf and one reading it off). I was very pleased with it.
|
|
||||||
*
|
|
||||||
* Doing the same thing in Slant is slightly more difficult because
|
|
||||||
* the set of edges the user can fill in do not form a planar graph
|
|
||||||
* (the two potential edges in each square cross in the middle). But
|
|
||||||
* you can still apply the same principle by considering the 'faces'
|
|
||||||
* to be diamond-shaped regions of space around each horizontal or
|
|
||||||
* vertical grid line. Equivalently, pretend each edge added by the
|
|
||||||
* player is really divided into two edges, each from a square-centre
|
|
||||||
* to one of the square's corners, and now the grid graph is planar
|
|
||||||
* again.
|
|
||||||
*
|
|
||||||
* However, it fell down when - much later - I tried to implement the
|
|
||||||
* same algorithm in Net.
|
|
||||||
*
|
|
||||||
* Net doesn't *absolutely need* loop detection, because of its system
|
|
||||||
* of highlighting squares connected to the source square: an argument
|
|
||||||
* involving counting vertex degrees shows that if any loop exists,
|
|
||||||
* then it must be counterbalanced by some disconnected square, so
|
|
||||||
* there will be _some_ error highlight in any invalid grid even
|
|
||||||
* without loop detection. However, in large complicated cases, it's
|
|
||||||
* still nice to highlight the loop itself, so that once the player is
|
|
||||||
* clued in to its existence by a disconnected square elsewhere, they
|
|
||||||
* don't have to spend forever trying to find it.
|
|
||||||
*
|
|
||||||
* The new wrinkle in Net, compared to other loop-disallowing puzzles,
|
|
||||||
* is that it can be played with wrapping walls, or - topologically
|
|
||||||
* speaking - on a torus. And a torus has a property that algebraic
|
|
||||||
* topologists would know of as a 'non-trivial H_1 homology group',
|
|
||||||
* which essentially means that there can exist a loop on a torus
|
|
||||||
* which *doesn't* separate the surface into two regions disconnected
|
|
||||||
* from each other.
|
|
||||||
*
|
|
||||||
* In other words, using this algorithm in Net will do fine at finding
|
|
||||||
* _small_ localised loops, but a large-scale loop that goes (say) off
|
|
||||||
* the top of the grid, back on at the bottom, and meets up in the
|
|
||||||
* middle again will not be detected.
|
|
||||||
*
|
|
||||||
* Footpath dsf
|
|
||||||
* ------------
|
|
||||||
*
|
|
||||||
* To solve this homology problem in Net, I hastily thought up another
|
|
||||||
* dsf-based algorithm.
|
|
||||||
*
|
|
||||||
* This time, let's consider each edge of the graph to be a road, with
|
|
||||||
* a separate pedestrian footpath down each side. We'll form a dsf on
|
|
||||||
* those imaginary segments of footpath.
|
|
||||||
*
|
|
||||||
* At each vertex of the graph, we go round the edges leaving that
|
|
||||||
* vertex, in order around the vertex. For each pair of edges adjacent
|
|
||||||
* in this order, we unify their facing pair of footpaths (e.g. if
|
|
||||||
* edge E appears anticlockwise of F, then we unify the anticlockwise
|
|
||||||
* footpath of F with the clockwise one of E) . In particular, if a
|
|
||||||
* vertex has degree 1, then the two footpaths on either side of its
|
|
||||||
* single edge are unified.
|
|
||||||
*
|
|
||||||
* Then, an edge is part of a loop iff its two footpaths are not
|
|
||||||
* reachable from one another.
|
|
||||||
*
|
|
||||||
* This algorithm is almost as simple to implement as the face dsf,
|
|
||||||
* and it works on a wider class of graphs embedded in plane-like
|
|
||||||
* surfaces; in particular, it fixes the torus bug in the face-dsf
|
|
||||||
* approach. However, it still depends on the graph having _some_ sort
|
|
||||||
* of embedding in a 2-manifold, because it relies on there being a
|
|
||||||
* meaningful notion of 'order of edges around a vertex' in the first
|
|
||||||
* place, so you couldn't use it on a wildly nonplanar graph like the
|
|
||||||
* diamond lattice. Also, more subtly, it depends on the graph being
|
|
||||||
* embedded in an _orientable_ surface - and that's a thing that might
|
|
||||||
* much more plausibly change in future puzzles, because it's not at
|
|
||||||
* all unlikely that at some point I might feel moved to implement a
|
|
||||||
* puzzle that can be played on the surface of a Mobius strip or a
|
|
||||||
* Klein bottle. And then even this algorithm won't work.
|
|
||||||
*
|
|
||||||
* Tarjan's bridge-finding algorithm
|
|
||||||
* ---------------------------------
|
|
||||||
*
|
|
||||||
* And so, finally, we come to the algorithm above. This one is pure
|
|
||||||
* graph theory: it doesn't depend on any concept of 'faces', or 'edge
|
|
||||||
* ordering around a vertex', or any other trapping of a planar or
|
|
||||||
* quasi-planar graph embedding. It should work on any graph
|
|
||||||
* whatsoever, and reliably identify precisely the set of edges that
|
|
||||||
* form part of some loop. So *hopefully* this long string of failures
|
|
||||||
* has finally come to an end...
|
|
||||||
*/
|
|
||||||
|
Reference in New Issue
Block a user