From cd97968b03708bd16868cdac1511c583cc82974b Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Tue, 10 Sep 2024 12:17:22 +0100 Subject: [PATCH] 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. --- findloop.c | 176 ++--------------------------------------------------- 1 file changed, 4 insertions(+), 172 deletions(-) diff --git a/findloop.c b/findloop.c index 4ebdea1..1fc27b9 100644 --- a/findloop.c +++ b/findloop.c @@ -9,6 +9,10 @@ * are precisely those which _wouldn't_ disconnect anything if removed * (individually) - but of course flipping the sense of the output is * 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" @@ -356,175 +360,3 @@ bool findloop_run(struct findloopstate *pv, int nvertices, */ 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... - */