chiark / gitweb /
Fix homology bug (!) in Net's loop highlighter.
authorSimon Tatham <anakin@pobox.com>
Mon, 29 Dec 2014 10:11:47 +0000 (10:11 +0000)
committerSimon Tatham <anakin@pobox.com>
Mon, 29 Dec 2014 10:11:47 +0000 (10:11 +0000)
I unthinkingly transplanted into Net the same loop-finding algorithm
used in Loopy and Slant, which identifies the connected components
into which the grid lines divide the plane, and marks an edge as part
of a loop iff it separates two different components. This works fine
for a planar graph, but in Net's wrapping mode, it's possible to have
loops which do not have this property - e.g. a loop can go off the top
of the grid and back on the bottom to join up with itself, and then
it _doesn't_ disconnect the surface into two components.

(In principle, this kind of problem can turn up in any topological
space with a non-trivial H_1 homology group, which is why it fails on
the torus to which Net's wrapping mode corresponds, but not on the
plane or sphere. I think it's forgivable that I hadn't expected
homology to be the cause of any bug in practical code ever!)

Fixed by inventing yet another dsf-based loop-finding algorithm, this
one based on tracing round the outside of connected components of the
graph. It's still not _fully_ general, in that this one still depends
on the graph being drawn on an orientable surface (so it'll need
another rewrite if I ever add Mobius strip or Klein bottle modes for
Net...), but it's fairly simple to state and implement, and detects
loops that the previous implementation did not, such as the one in the
starting position of 3x3w:1a39ac6a8 .

net.c

diff --git a/net.c b/net.c
index 893e8f7d890866d23d04bdfb3e7fc1edd05b7b2c..afce82b9a4ed6d9f561c90781a616f375a149131 100644 (file)
--- a/net.c
+++ b/net.c
@@ -1913,53 +1913,183 @@ static int *compute_loops_inner(int w, int h, int wrapping,
                                 const unsigned char *tiles,
                                 const unsigned char *barriers)
 {
-    int W = w + 1, H = h + 1;
     int *loops, *dsf;
     int x, y;
 
     /*
-     * Construct a dsf covering _vertices_ of the grid, so we have one
-     * more in each direction than we do squares.
-     */
-    dsf = snew_dsf(W*H);
-
-    /*
-     * For each grid square, unify adjacent vertices of that square
-     * unless there's a connection separating them. (We only need to
-     * check the connection in _this_ square, without bothering to
-     * look for one on the other side of the grid line, because the
-     * loop will do that anyway when it gets to the other square.)
+     * The loop-detecting algorithm I use here is not quite the same
+     * one as I've used in Slant and Loopy. Those two puzzles use a
+     * very similar algorithm which works by finding connected
+     * components, not of the graph _vertices_, but of the pieces of
+     * space in between them. You divide the plane into maximal areas
+     * that can't be intersected by a grid edge (faces in Loopy,
+     * diamond shapes centred on a grid edge in Slant); you form a dsf
+     * over those areas, and unify any pair _not_ separated by a graph
+     * edge; then you've identified the connected components of the
+     * space, and can now immediately tell whether an edge is part of
+     * a loop or not by checking whether the pieces of space on either
+     * side of it are in the same component.
      *
-     * Barriers break loops, so we disallow any connection which
-     * terminates in a barrier.
-     */
+     * In Net, this doesn't work reliably, because of the toroidal
+     * wrapping mode. A torus has non-trivial homology, which is to
+     * say, there can exist a closed loop on its surface which is not
+     * the boundary of any proper subset of the torus's area. For
+     * example, consider the 'loop' consisting of a straight vertical
+     * line going off the top of the grid and coming back on the
+     * bottom to join up with itself. This certainly wants to be
+     * marked as a loop, but it won't be detected as one by the above
+     * algorithm, because all the area of the grid is still connected
+     * via the left- and right-hand edges, so the two sides of the
+     * loop _are_ in the same equivalence class.
+     *
+     * The replacement algorithm I use here is also dsf-based, but the
+     * dsf is now over _sides of edges_. That is to say, on a general
+     * graph, you would have two dsf elements per edge of the graph.
+     * The unification rule is: for each vertex, iterate round the
+     * edges leaving that vertex in cyclic order, and dsf-unify the
+     * _near sides_ of each pair of adjacent edges. The effect of this
+     * is to trace round the outside edge of each connected component
+     * of the graph (this time of the actual graph, not the space
+     * between), so that the outline of each component becomes its own
+     * equivalence class. And now, just as before, an edge is part of
+     * a loop iff its two sides are not in the same component.
+     *
+     * This correctly detects even homologically nontrivial loops on a
+     * torus, because a torus is still _orientable_ - there's no way
+     * that a loop can join back up with itself with the two sides
+     * swapped. It would stop working, however, on a Mobius strip or a
+     * Klein bottle - so if I ever implement either of those modes for
+     * Net, I'll have to revisit this algorithm yet again and probably
+     * replace it with a completely general and much more fiddly
+     * approach such as Tarjan's bridge-finding algorithm (which is
+     * linear-time, but looks to me as if it's going to take more
+     * effort to get it working, especially when the graph is
+     * represented so unlike an ordinary graph).
+     *
+     * In Net, the algorithm as I describe it above has to be fiddled
+     * with just a little, to deal with the fact that there are two
+     * kinds of 'vertex' in the graph - one set at face-centres, and
+     * another set at edge-midpoints where two wires either do or do
+     * not join. Since those two vertex classes have very different
+     * representations in the Net data structure, separate code is
+     * needed for them.
+     */
+
+    /* Four potential edges per grid cell; one dsf node for each side
+     * of each one makes 8 per cell. */
+    dsf = snew_dsf(w*h*8);
+
+    /* Encode the dsf nodes. We imagine going round anticlockwise, so
+     * BEFORE(dir) indicates the clockwise side of an edge, e.g. the
+     * underside of R or the right-hand side of U. AFTER is the other
+     * side. */
+#define BEFORE(dir) ((dir)==R?7:(dir)==U?1:(dir)==L?3:5)
+#define AFTER(dir) ((dir)==R?0:(dir)==U?2:(dir)==L?4:6)
+
+#if 0
+    printf("--- begin\n");
+#endif
     for (y = 0; y < h; y++) {
         for (x = 0; x < w; x++) {
-            int t = tiles[y*w+x];
-            if (barriers)
-                t &= ~barriers[y*w+x];
-            if (!(t & L))
-                dsf_merge(dsf, y*W+x, (y+1)*W+x);
-            if (!(t & R))
-                dsf_merge(dsf, y*W+(x+1), (y+1)*W+(x+1));
-            if (!(t & U))
-                dsf_merge(dsf, y*W+x, y*W+(x+1));
-            if (!(t & D))
-                dsf_merge(dsf, (y+1)*W+x, (y+1)*W+(x+1));
+            int tile = tiles[y*w+x]; 
+            int dir;
+            for (dir = 1; dir < 0x10; dir <<= 1) {
+                /*
+                 * To unify dsf nodes around a face-centre vertex,
+                 * it's easiest to do it _unconditionally_ - e.g. just
+                 * unify the top side of R with the right side of U
+                 * regardless of whether there's an edge in either
+                 * place. Later we'll also unify the top and bottom
+                 * sides of any nonexistent edge, which will e.g.
+                 * complete a connection BEFORE(U) - AFTER(R) -
+                 * BEFORE(R) - AFTER(D) in the absence of an R edge.
+                 *
+                 * This is a safe optimisation because these extra dsf
+                 * nodes unified into our equivalence class can't get
+                 * out of control - they are never unified with
+                 * anything _else_ elsewhere in the algorithm.
+                 */
+#if 0
+                printf("tile centre %d,%d: merge %d,%d\n",
+                       x, y,
+                       (y*w+x)*8+AFTER(C(dir)),
+                       (y*w+x)*8+BEFORE(dir));
+#endif
+                dsf_merge(dsf,
+                          (y*w+x)*8+AFTER(C(dir)),
+                          (y*w+x)*8+BEFORE(dir));
+
+                if (tile & dir) {
+                    int x1, y1;
+
+                    OFFSETWH(x1, y1, x, y, dir, w, h);
+
+                    /*
+                     * If the tile does have an edge going out in this
+                     * direction, we must check whether it joins up
+                     * (without being blocked by a barrier) to an edge
+                     * in the next cell along. If so, we unify around
+                     * the edge-centre vertex by joining each side of
+                     * this edge to the appropriate side of the next
+                     * cell's edge; otherwise, the edge is a stub (the
+                     * only one reaching the edge-centre vertex) and
+                     * so we join its own two sides together.
+                     */
+                    if ((barriers && barriers[y*w+x] & dir) ||
+                        !(tiles[y1*w+x1] & F(dir))) {
+#if 0
+                        printf("tile edge stub %d,%d -> %c: merge %d,%d\n",
+                               x, y, (dir==L?'L':dir==U?'U':dir==R?'R':'D'),
+                               (y*w+x)*8+BEFORE(dir),
+                               (y*w+x)*8+AFTER(dir));
+#endif
+                        dsf_merge(dsf,
+                                  (y*w+x)*8+BEFORE(dir),
+                                  (y*w+x)*8+AFTER(dir));
+                    } else {
+#if 0
+                        printf("tile edge conn %d,%d -> %c: merge %d,%d\n",
+                               x, y, (dir==L?'L':dir==U?'U':dir==R?'R':'D'),
+                               (y*w+x)*8+BEFORE(dir),
+                               (y*w+x)*8+AFTER(F(dir)));
+#endif
+                        dsf_merge(dsf,
+                                  (y*w+x)*8+BEFORE(dir),
+                                  (y1*w+x1)*8+AFTER(F(dir)));
+#if 0
+                        printf("tile edge conn %d,%d -> %c: merge %d,%d\n",
+                               x, y, (dir==L?'L':dir==U?'U':dir==R?'R':'D'),
+                               (y*w+x)*8+AFTER(dir),
+                               (y*w+x)*8+BEFORE(F(dir)));
+#endif
+                        dsf_merge(dsf,
+                                  (y*w+x)*8+AFTER(dir),
+                                  (y1*w+x1)*8+BEFORE(F(dir)));
+                    }
+                } else {
+                    /*
+                     * As discussed above, if this edge doesn't even
+                     * exist, we unify its two sides anyway to
+                     * complete the unification of whatever edges do
+                     * exist in this cell.
+                     */
+#if 0
+                    printf("tile edge missing %d,%d -> %c: merge %d,%d\n",
+                           x, y, (dir==L?'L':dir==U?'U':dir==R?'R':'D'),
+                           (y*w+x)*8+BEFORE(dir),
+                           (y*w+x)*8+AFTER(dir));
+#endif
+                    dsf_merge(dsf,
+                              (y*w+x)*8+BEFORE(dir),
+                              (y*w+x)*8+AFTER(dir));
+                }
+            }
         }
     }
 
-    /*
-     * If the game is in wrapping mode, unify each edge vertex with
-     * its opposite.
-     */
-    if (wrapping) {
-        for (y = 0; y < H; y++)
-            dsf_merge(dsf, y*W+0, y*W+w);
-        for (x = 0; x < W; x++)
-            dsf_merge(dsf, 0*W+x, h*W+x);
-    }
-
+#if 0
+    printf("--- end\n");
+#endif
     loops = snewn(w*h, int);
 
     /*
@@ -1969,20 +2099,16 @@ static int *compute_loops_inner(int w, int h, int wrapping,
      */
     for (y = 0; y < h; y++) {
         for (x = 0; x < w; x++) {
-            int t = tiles[y*w+x];
+            int dir;
+            int tile = tiles[y*w+x];
             int flags = 0;
-            if ((t & L) && (dsf_canonify(dsf, y*W+x) !=
-                            dsf_canonify(dsf, (y+1)*W+x)))
-                flags |= LLOOP;
-            if ((t & R) && (dsf_canonify(dsf, y*W+(x+1)) !=
-                            dsf_canonify(dsf, (y+1)*W+(x+1))))
-                flags |= RLOOP;
-            if ((t & U) && (dsf_canonify(dsf, y*W+x) !=
-                            dsf_canonify(dsf, y*W+(x+1))))
-                flags |= ULOOP;
-            if ((t & D) && (dsf_canonify(dsf, (y+1)*W+x) !=
-                            dsf_canonify(dsf, (y+1)*W+(x+1))))
-                flags |= DLOOP;
+            for (dir = 1; dir < 0x10; dir <<= 1) {
+                if ((tile & dir) &&
+                    (dsf_canonify(dsf, (y*w+x)*8+BEFORE(dir)) !=
+                     dsf_canonify(dsf, (y*w+x)*8+AFTER(dir)))) {
+                    flags |= LOOP(dir);
+                }
+            }
             loops[y*w+x] = flags;
         }
     }