mirror of
git://git.tartarus.org/simon/puzzles.git
synced 2025-04-20 07:31:30 -07:00
Files

Thanks to Amir Livne Bar-on, who implemented this variant from a description posted in the Mastodon thread following up my recent blog post about loop-finding. The revised algorithm has the same asymptotic complexity as the one I already had. But it has better constant factors, and less code: it can run in a single depth-first pass over the graph instead of three separate passes, and it needs to store fewer variables per vertex. This version relies on the insight that if you DFS over an undirected graph and imagine it constructing a rooted spanning forest with each component's tree rooted at whatever vertex you started that component from, then every edge that the DFS visits without making it part of the spanning forest must join a vertex to one of its direct ancestors in that component's tree. (Because the other options are that it joins the DFS's current vertex to one the search hasn't visited at all yet – in which case the DFS _would_ follow it, and make it a forest edge after all. Or else it joins this vertex to a cousin in an earlier finished subtree – but then when the DFS processed that subtree, it would have explored the same edge in the other direction, and added our current vertex to that subtree, which by assumption it didn't.) Hence, instead of assigning every vertex a distinct integer label and calculating the min/max label reachable from each subtree, we can instead assign each vertex its tree depth, and simply calculate the minimum _depth_ of vertex reachable from each subtree: if a subtree starting at depth D can reach a vertex at depth <D, it's because there's one of those non-tree edges to a vertex outside the subtree, so the tree edge entering the subtree isn't a bridge. And since every non-tree edge must point to a vertex we've already seen (and hence assigned a depth to), this can be done in the same pass as calculating the depths in the first place - and we don't even need to _store_ the spanning forest we generate.
207 lines
6.9 KiB
C
207 lines
6.9 KiB
C
/*
|
|
* Routine for finding loops in graphs, reusable across multiple
|
|
* puzzles.
|
|
*
|
|
* The strategy is Tarjan's bridge-finding algorithm, which is
|
|
* designed to list all edges whose removal would disconnect a
|
|
* previously connected component of the graph. We're interested in
|
|
* exactly the reverse - edges that are part of a loop in the graph
|
|
* 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/
|
|
*
|
|
* The specific variant of Tarjan's algorithm we use is the one from
|
|
* https://mathstodon.xyz/@abacabadabacaba@infosec.exchange/113113280480134188
|
|
*/
|
|
|
|
#include "puzzles.h"
|
|
|
|
struct findloopstate {
|
|
int depth, shallowest_reachable, subtree_size;
|
|
int parent, component_root;
|
|
int prev, next;
|
|
};
|
|
|
|
struct findloopstate *findloop_new_state(int nvertices)
|
|
{
|
|
return snewn(nvertices, struct findloopstate);
|
|
}
|
|
|
|
void findloop_free_state(struct findloopstate *state)
|
|
{
|
|
sfree(state);
|
|
}
|
|
|
|
bool findloop_is_loop_edge(struct findloopstate *pv, int u, int v)
|
|
{
|
|
/*
|
|
* In the DFS-built forest, all edges are either are from parent
|
|
* to child or from child to ancestor.
|
|
*
|
|
* Back-edges to ancestors must be parts of loops. In order to
|
|
* detect whether a parent-to-child edge is part of a loop, we
|
|
* check if any ancestor is reachable from that child's subtree.
|
|
*/
|
|
if (pv[u].parent == v && pv[u].shallowest_reachable >= pv[u].depth)
|
|
return false;
|
|
if (pv[v].parent == u && pv[v].shallowest_reachable >= pv[v].depth)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static bool findloop_is_bridge_oneway(
|
|
struct findloopstate *pv, int u, int v, int *u_vertices, int *v_vertices)
|
|
{
|
|
if (pv[u].parent != v)
|
|
return false;
|
|
if (pv[u].shallowest_reachable < pv[u].depth)
|
|
return false;
|
|
|
|
if (u_vertices)
|
|
*u_vertices = pv[u].subtree_size;
|
|
if (v_vertices) {
|
|
int r = pv[u].component_root;
|
|
*v_vertices = pv[r].subtree_size - pv[u].subtree_size;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool findloop_is_bridge(
|
|
struct findloopstate *pv, int u, int v, int *u_vertices, int *v_vertices)
|
|
{
|
|
return (findloop_is_bridge_oneway(pv, u, v, u_vertices, v_vertices) ||
|
|
findloop_is_bridge_oneway(pv, v, u, v_vertices, u_vertices));
|
|
}
|
|
|
|
bool findloop_run(struct findloopstate *pv, int nvertices,
|
|
neighbour_fn_t neighbour, void *ctx)
|
|
{
|
|
int u, v, w;
|
|
bool any_loop = false;
|
|
|
|
/*
|
|
* We run a DFS to split the graph into disjoint spanning trees.
|
|
* This construction guarantees that every edge either descends
|
|
* to a new child never visited before or goes to an ancestor.
|
|
* Tracking which ancestors are linked from which subtrees lets
|
|
* us detect all loops efficiently.
|
|
*
|
|
* Here we don't use recursion, instead holding the entire DSF
|
|
* state in the findloopstate struct. The loop below visits each
|
|
* node exactly twice: before and after visiting its subtree.
|
|
*
|
|
* The first time we visit a node, we take care of marking its
|
|
* children with their position in the tree and when they're
|
|
* scheduled to be visited. The second time we update the parent
|
|
* with statistics about the subtree.
|
|
*
|
|
* The order of nodes is managed using a doubly-linked list.
|
|
* The first time a node is visited, we add its children before it
|
|
* in the list and set the pointer to go through them first. The
|
|
* second time we move to the next node in the list, which is a
|
|
* sibling or a parent, or if we're at the root of the connected
|
|
* component will be a node in the next component. (All the nodes
|
|
* of the graph always appear in the list)
|
|
* A linked list is used to handle the case where the same child
|
|
* appears in two levels, to allow us to efficiently remove it from
|
|
* its previous position.
|
|
*
|
|
* The algorithm tracks whether we're at the first or the second
|
|
* visit at a node using the depth property. It's set to a negative
|
|
* value on initialization, and to the depth in the connected
|
|
* component's tree on the first visit.
|
|
* It detects a new connected component using the parent pointer,
|
|
* it's always set to a real node in the search, and is negative for
|
|
* new trees.
|
|
*
|
|
* In the first visit, we go over the node's children, moving them
|
|
* in the list and setting their parent pointer. Edges going to
|
|
* ancestors are noted in the shallowest_reachable field.
|
|
* In the second visit, we adjust the subtree_size and
|
|
* shallowest_reachable fields of the parent.
|
|
*
|
|
* Variables:
|
|
* u = the current node under examination
|
|
* v = the node to go to in the next iteration
|
|
* w = neighbour iterator
|
|
*/
|
|
|
|
for (u = 0; u < nvertices; u++) {
|
|
pv[u].depth = -1;
|
|
pv[u].shallowest_reachable = nvertices;
|
|
pv[u].subtree_size = 1;
|
|
pv[u].parent = -1;
|
|
pv[u].component_root = u;
|
|
pv[u].prev = u - 1;
|
|
pv[u].next = (u == nvertices - 1) ? -1 : u + 1;
|
|
}
|
|
|
|
debug(("------------- new find_loops, nvertices=%d\n", nvertices));
|
|
|
|
v = 0;
|
|
while (v != -1) {
|
|
u = v;
|
|
if (pv[u].depth < 0) {
|
|
/* Our first visit to the node (on the way down the search) */
|
|
if (pv[u].parent < 0) {
|
|
debug((" new component: processing %d\n", u));
|
|
pv[u].depth = 0;
|
|
pv[u].component_root = u;
|
|
} else {
|
|
debug((" processing %d\n", u));
|
|
pv[u].depth = pv[pv[u].parent].depth + 1;
|
|
pv[u].component_root = pv[pv[u].parent].component_root;
|
|
}
|
|
|
|
/* Schedule visits to the neighbors, and then back here */
|
|
v = u;
|
|
for (w = neighbour(u, ctx); w >= 0; w = neighbour(-1, ctx)) {
|
|
if (w == pv[u].parent)
|
|
continue;
|
|
if (pv[w].depth < 0) {
|
|
debug((" adding edge %d-%d to tree\n", u, w));
|
|
pv[w].parent = u;
|
|
/* Remove the neighbour from the linked list */
|
|
if (pv[w].prev >= 0)
|
|
pv[pv[w].prev].next = pv[w].next;
|
|
if (pv[w].next >= 0)
|
|
pv[pv[w].next].prev = pv[w].prev;
|
|
/* Add it to the start of the list */
|
|
pv[w].prev = pv[v].prev;
|
|
pv[w].next = v;
|
|
if (pv[v].prev >= 0)
|
|
pv[pv[v].prev].next = w;
|
|
pv[v].prev = w;
|
|
/* Mark this as the next node to visit */
|
|
v = w;
|
|
} else {
|
|
debug((" found back-edge %d-%d\n", u, w));
|
|
pv[u].shallowest_reachable =
|
|
min(pv[u].shallowest_reachable, pv[w].depth);
|
|
any_loop = true;
|
|
}
|
|
}
|
|
} else {
|
|
debug((" wrapping up %d. |subtree| = %d, min(reachable) = %d\n",
|
|
u, pv[u].subtree_size, pv[u].shallowest_reachable));
|
|
if (pv[u].parent >= 0) {
|
|
if (pv[u].shallowest_reachable >= pv[u].depth) {
|
|
debug((" bridge: %d-%d\n", u, pv[u].parent));
|
|
}
|
|
pv[pv[u].parent].subtree_size += pv[u].subtree_size;
|
|
pv[pv[u].parent].shallowest_reachable =
|
|
min(pv[pv[u].parent].shallowest_reachable,
|
|
pv[u].shallowest_reachable);
|
|
}
|
|
v = pv[u].next;
|
|
}
|
|
}
|
|
|
|
return any_loop;
|
|
}
|