Finding a minimum loop path in a dynamically directed graph

I recently encountered this problem (Edit: Problem A) from the Spotify hacker problem earlier this year, which includes the definition of a train switch to transfer the train back to it. The train should arrive in the same direction in which it remained, and the train will never be able to return to the tracks.

As I understand it, the problem can be modeled as an undirected (?) Graph, where we must find the shortest cycle from a certain vertex or find that such a cycle does not exist. However, the interesting part is that for a vertex v, the vertices adjacent to v depend on the path taken in v, therefore, in a sense, the graph can be considered directional, although this direction depends on the path.

My first thought was to simulate each node as 3 separate vertices, A, B and C, where A ↔ B and A ↔ C, and then use the width search to create a search until we find the original vertex, but this complicated by the warning described above, namely, that the adjacencies for a given vertex depend on the previous vertex that we visited. This means that in our BFS tree, nodes can have multiple parents.

Obviously, a simple BFS search is not enough to solve this problem. I know that there are algorithms that exist to detect loops in a graph. One approach may be to detect all cycles, and then determine for each cycle whether the path is valid. (i.e. does not reverse direction)

Does anyone else have any ideas on approaches to solve this problem?

UPDATE: I followed the approach suggested by @Karussell in the comments.

Here is my solution for github.

The trick was to model the situation using a graph graph rather than a traditional vertex graph. The input file provided in the competition is convenient to specify in terms of edges, so this file can be easily used to build a boundary graph.

The program uses two important classes: Road and Solver. The road has two integer fields: j1 and j2. j1 is the source compound, and j2 is the target compound. Each road is one-way, which means that you can only move from j1 to j2. Each road also includes a LinkedList of neighboring roads and a parent road. The Road class also includes static methods for converting between lines used in the input file and integer indices representing points A, B, and C on each node.

For each entry in the input file, we add two roads to the HashMap, one road for each direction between two intersections. Now we have a list of all the roads that pass between crossings. We just need to connect the roads together at the junctions through switches A, B and C. If the road ends at Junction.A, we look at the roads that start at Junction.B and Junction.C and add these roads as an adjunction. The buildGraph () function returns a path whose target connection (j2) is "1A" == index 0.

At this moment, our schedule is built. To find the shortest path, I just used BFS to move the chart. We leave the root unmarked and start with lines of root adjoins. If we find a road whose target connection is β€œ1A” (index 0), then we find the shortest cycle through the starting point. As soon as we reconstruct the path using each parent property of Road, the trivial task must be appropriately defined in this problem.

Thanks to Karussell for suggesting this approach. If you want to put your comment in the response form with a brief explanation, I agree with her. Thanks @Origin, as well. I must admit that I did not fully follow the logic of your answer, but this, of course, does not mean that this is not true. If someone solves this problem using your solution, I would be very interested to see it.

+6
source share
2 answers

As my comment suggested: I think that you can solve this through a boundary graph or through an improvement , which is a more or less "extended" node graph.

More details:

  • Your situation is similar to turn restrictions in road networks. They can be modeled if you create one street node per (directional!) And connect these nodes depending on the allowed turns.
  • So, not only keep the position of your current position, but also the direction and possible further "situations". To make it possible that even the same position with 180 Β° rotation is different from your current state.

Instead of simulating your β€œstate” (which is directed!) Into the graph, you can also assign possible results for each transition - now the algorithm should be more intelligent and should decide for what reason what to do, depending on your previous one ( including direction). I think this is the main idea of ​​the β€œextended” node graph, which should be less memory intensive (not so important in your case).

+1
source

One possible approach: first create some kind of graph to simulate all the connections (column G). Then build another graph in which we find a cycle (graph H). For each node A in G, we add a node to the graph of H. Each A node also has 2 outgoing edges (to nodes B and C in column G). In H, these edges will go to the next A node, which will be met in G. For example, A node in H corresponding to A node of the switch with ID 3 will have an outgoing edge to node 9 and node 6 in H. The weight of each edge is the number switches passing along this route (including the start switch).

This will give a graph in which we can grow the shortest shortest path tree. If we reach the beginning again, the cycle will be completed.

The key is that the switch is only a decision point if it moves in the direction A->. There is no need to simulate the opposite direction, as this will only complicate the search.

edit: a few clarifications

The challenge is to determine the shortest path from A to A (again). The shortest definition is the number of keys transmitted. This will be used in the Dijkstra based search algorithm. Basically we will do Dijkstra on column H, in which the cost of the ribs is equal to the number of switches in this ribs.

In column H we will have a node for each switch. Each node will have 2 outgoing edges corresponding to two paths that can be taken (directions B and C). The edges in H will correspond to the entire route between two nodes A in the original graph. For example, in the description of the problem, we get the following:

A node corresponding to switch 1:

  • 1 outbound link to node 2, weight 2, corresponding to taking C when exiting switch 1. Weight is 2 because we skip switch 1 and switch 3 if we go from A1-> C1-> C3-> A3-> A2
  • 1 outgoing connection with node 3, weight 2, corresponding to the choice of direction B

A node corresponding to switch 2:

  • 1 outgoing connection with node 6, weight 2, corresponding to direction B
  • there is no second link, since the direction C is a dead end.

A node corresponding to switch 3:

  • 1 outbound link to node 6, weight 2, corresponding to taking direction C
  • 1 outgoing communication with node 9, weight 3, corresponding to the adoption of the B-direction and transmit switches 3, 7 and 8

etc. for each switch. This gives a graph with 10 nodes, each of which has no more than 2 directed edges.

Now we can start building our Dijkstra tree. We start with node 1 and have 2 possible directions, B and C. We put them on the priority side. Then the queue contains [node 2, weight 2] and [node 3, weight 2], since we can reach input A of switch 2 after passing 2 switches and input A of switch 3 after passing 2 switches. Then we continue the search, taking the lowest weight record from the queue:

  • [node 2, weight 2]: only direction B is required, so put [node 6, weight 4] in the queue
  • [node 3, weight 2]: 2 directions to add [node 6, weight 4] and [node 9, weight 5] to the queue.
  • [node 6, weight 4]: maybe 2 directions, add [node 4, weight 5] and [node 8, weight 8] to the queue]
  • [node 9, weight 5]: only direction C, add [node 10, weight 6]
  • [node 4, weight 5]: add [node 5, weight 7] for direction C and [node 1, weight 9] for direction B]
  • [node 10, weight 6]: add [node 1, weight 8] for direction C and [node 1, weight 10] for direction B
  • [node 5, weight 7]: add [node 1, weight 11] and [node 8, weight 10]
  • [node 8, weight 8]: add [node 7, weight 9]
  • [node 1, weight 8]: we found our way back so we can stop

(errors are possible, I just do it manually)

Then the algorithm stops with a finite length of 8 for the loop. Determining the next path is simply a matter of supporting parent pointers for the nodes when they are installed and the path unpacked.

We can use Dijkstra because each node in H corresponds to bypassing the original node (in G) in the right direction. Each node in H can then be set to Dijkstra, so the complexity of the algorithm is limited by the complexity of the Dijkstra algorithm (which can handle an upper limit of 100k for the number of switches).

+1
source

All Articles