Time for 2 nodes to collide

Viewed 214

We are given a graph of N nodes. (1-N), where each node has exactly 1 directed edge to some node (this node can be the same node).

We need to answer the queries of type : A, B, which asks time required when 2 objects collide if one start at A and other start at B. Both moves 1 hop in 1 sec. If it's not possible for them to collide time would be -1.

Time : from X -> to Y : 1 hop = 1 second.

Constraints :

N, Q <= 10^5 (number of nodes, number of queries).

Example : for given graph

   A -> B -> C -> D -> E
                  ^    |
                  K <- F

Query(A, E) : 3 seconds, as at time t = 3 secs they both will be on node D.
Query(C, D) : -1 seconds, as they will never collide.

What's the optimal way to answer each query?

Brute Force Approach: time - O(Q * N)

Improved solution using binary lifting technique: time - O(Q * log(N))

private static int[] collisionTime(int N, int Q, int[] A, int[][] queries) {

    // ancestor matrix : creation time- O(n * log(n))
    int M = (int) (Math.ceil(Math.log10(N) / Math.log10(2))) + 1;
    int[][] ancestor = new int[N + 1][M];
    for(int i = 1; i <= N; i++) {
        ancestor[i][0] = A[i]; // 2^0-th ancestor. 
    }
    for(int j = 1; j < M; j++) {
        for(int i = 1; i <= N; i++) {
            ancestor[i][j] = ancestor[ancestor[i][j-1]][j-1];
        }
    }

    int[] answer = new int[Q];
    for(int i = 0; i < Q; i++) { 
        int u = queries[i][0];
        int v = queries[i][1];
        answer[i] = timeToCollide(u, v, ancestor);
    }

    return answer;
}

// using binary lifting: time- O(log(n))
private static int timeToCollide(int u, int v, int[][] ancestor) {
    int m = ancestor[0].length;

    // edge cases
    if(u == v)                              // already in collision state
        return 0;              
    if(ancestor[u][m-1] != ancestor[v][m-1]) // their top most ancestor is not the same means they cannot collide at all.
        return -1;

    int t = 0;
    for(int j = m - 1; j >=0; j--) {
        if(ancestor[u][j] != ancestor[v][j]) {
            u = ancestor[u][j];
            v = ancestor[v][j];
            t += Math.pow(2, j);
        }
    }
    return t + 1;
}
3 Answers
  1. Find all the terminal cycles and designate an arbitrary vertex in each terminal cycle as the cycle root (O(N))
  2. For each vertex, record the length of its terminal cycle, its distance to entry into the terminal cycle, and the distance to the terminal cycle root (O(N)).
  3. Sever the outgoing link from each cycle root. This turns the graph into a forest.
  4. For each query, find the closest (lowest) common ancestor of the two query nodes in this forest.
  5. From the information saved about each query node and the lowest common ancestor, you can figure out the time to collision in constant time.

Step (4), the lowest common ancestor query, is a very well-studied problem. The best algorithms require only linear processing time and provide constant query time, leading to O(N + Q) time for this problem all together.

I believe that the following approach technically achieves O(N+Q) time complexity.

Observations

Subgraphs: The graph is not necessarily contiguous. All graphs consist or one or more disjoint contiguous complete subgraphs, meaning:

  • No nodes are shared between subgraphs ("disjoint")
  • All of the nodes in the subgraph are connected ("contiguous")
  • There are no paths connecting different subgraphs ("complete")

I will hereafter refer to these as the subgraphs of the graph or just "subgraphs". These subgraphs have the following additional properties, which are a consequence of their definition (above) and the types of nodes in the graph (they are all "parent-pointer nodes" with exactly one out-edge/pointer):

  • All such subgraphs must have exactly one cycle in them (because a cycle is the only way that they can terminate or be closed)
  • The cycle can be of any length cycle.Len >= 1
  • Additionally, there may be any number (t >= 0) trees attached to the cycle at their root (base)
  • All nodes are either in the cycle or in one of these trees (the roots of the trees are in the cycle, but also counted as part of a tree)

Terms:

  • cycle Length: The number of nodes in a cycle.
  • cycle Base: An arbitrarily chosen node in the cycle used to measure and distances between two nodes in the same cycle, and also any two nodes in the same subgraph.
  • tree Base: The base or root node of one of the trees attached to the cycle. As the tree base is also the node that attaches it to the cycle, tree base nodes are counted as being in the cycle (and also part of their tree).
  • Distance: For a node in the cycle, this is the distance (number of hops) from that node to the cycle Base (zero if it is the cycle Base). For a node in a tree (not counting tree Base nodes, which count as in the cycle), this is the distance from that node to the tree Base node.

Collisions Cases

Trivial

There are many ways or "forms" of collisions possible in a graph, but we can identify two trivial cases upfront:

(A, B) Relation Collide? Collision Distance
same node Yes 0
different subgraphs No -1

Obviously, if A and B are the same node, then they trivially collide at distance zero. Likewise, if they are in two different subgraphs, then they can never collide because there are no connections between the subgraphs. For the collision cases that follow I will be assuming that these two cases have already been filtered out so that:

  1. A and B are assumed to be different nodes, and
  2. A and B are assumed to be in the same subgraph

Non-Trivial

The following table covers all of the other, non-trivial, cases of the relation between two nodes.

(A, B) Relation Collide? Collision Distance Notes
same cycle No -1 nodes in cycle always stay the same distance apart
A in a tree & B in the cycle (or vice-versa) if they both arrive at A's treeBase at the same time -1 OR A.treeDist if B.cycleDist = (A.treeDist MOD cycle.Len)
A and B are in different trees if A and B's distance to their cycle.Base is equal MOD cycle.Len MAX(A.treeDist, B.treeDist) They meet when the farther one gets to the cycle (tree root)
A & B are in the same tree, but have different treeDist's If their treeDist's are equal MOD cycle.Len MAX(A.treeDist, B.treeDist) They meet when the farther one gets to the cycle (tree root/base)
A & B are in the same tree, and have the same treeDist's Yes At their lowest common ancestor (LCA) in the tree Have to search up the tree

One important principle applied several times above is that two different nodes in a cycle can never collide. This is because when each node follows its path around the cycle, they will always stay the same distance apart, there is no way for one node's path to "catch-up" to another's in the cycle. They can only "collide" if they start out in the cycle at the same node.

The consequences of this are that:

  1. Two different nodes in the cycle can never collide.
  2. A node in a tree can only collide with a node in a cycle, if their total distances to the cycle base are the same Modulo the cycle length (that is the remainder when divided by the cycle length).
  3. This is also true for two nodes in different trees and two nodes in the same tree but with different distances to their tree base.
  4. In all of these cases (from #2 and #3), they will collide when the node that is farthest from its tree Base gets to the cycle (which is also its tree base). This is because nodes in the cycle cannot "catch-up" to each other, so they must always be the same once they are both in the cycle. Thus they always collide when the farther one finally gets to the cycle.

Another important consequence is that every case in both tables above, except for the last one, can be answered in O(1) time, simply by annotating the nodes with so easily determined information:

  1. their Base node (tree or cycle)
  2. their Distance to that base node
  3. the Subgraph they belong to
  4. the Length of their subgraph's Cycle

These can all be easily determined when initializing the graph in O(1) time per node (or O(N) total time).

Approach

Nodes

After the nodes are initially loaded into the graph (MPDGraph object), then I annotate the nodes with the additional information listed above. This process (called "Mapping" in the code) proceeds as follows:

  1. Pick any node
  2. Follow it's path until it "terminates" by reaching a node already in it's path, or a node that was previously mapped
  3. If #2 crossed it's own path, then we've found a new cycle. Designate the node we crossed as the base node of the cycle, and fill-in the mapping properties (cycle, base node, distance, etc.). Unwind our path one node at a time, filling in each node and marking it as InCycle as we go back up the path until we reach the base node again. Now we are ate the base of the tree that our path followed into the cycle, so when we move back to the pervious node in the path we switch to marking it as a tree node, until we return to the first node in our path.
  4. If instead, #2 reached an already mapped node, then we well attach our path to that node and copy its tree/cycle, base etc. information to our current node. Then we will return back up our path as in #3, setting the mapping properties of each node as we go,
  5. If there are any unmapped nodes, pick one and goto #2.

This all takes O(N) time.

Queries

We have a method (called MPDGraph.FindCollision) that given two nodes will apply the rules in the two Collision Cases tables above and return the result. For very case except the last (nodes in same tree and same distance) the distance can be determined in O(1) time by using the mapping properties.

If the two nodes are in the same tree and are also the same distance from their tree base, then they could meet anywhere between them and their common treeBase node. For this case the FindCollision(A,B) method calls the findTreeDistance(A,B) which:

  1. Returns zero if they are the same node.
  2. Otherwise it checks a cache of previously calculated distances to see if it has already been calculated for these two node. If so, then it returns that value.
  3. Otherwise, it calls findTreeDistance passing in the parents of the current two nodes to get their distance, and adds one to that. Then it adds this to the cache and returns the value.

Without this memoization (i.e., the cache) this would take on average apprx. O(log N) for each query of this type. With the memoization it is hard to calculate but I would guess no worse than O(log log N) but for Q counts much larger than N, this will converge to O(1).

This makes the query processing time complexity somewhere between O(Q log log N) and O(Q), and the total time between O(N + Q(log log N)) and O(N + Q).

Code

public static int[] collisionTime(int N, int Q, int[] A, int[,] queries)
{
    // create the graph and fill-in the mapping attributes for all nodes
    var graph = new MPDGraph(A);
    graph.MapAllNodes();

    int[] answers = new int[queries.GetLength(0)];
    for (int i = 0; i < answers.Length; i++)
    {
        answers[i] = graph.FindCollision(queries[i, 0], queries[i, 1]);
    }

    return answers;
}

This utilizes the following classes,

MPDGraph Class:

// MPDG: Mono-Pointing, Directed Graph 
//  An MPDG is a directed graph where every node has exactly one out-edge.
//  (MPDG is my own term, I don't know the real name for these)
class MPDGraph
{
    public Node[] Nodes;
    Dictionary<(Node, Node), int> cachedDistances = new Dictionary<(Node, Node), int>();

    // constructor
    public MPDGraph(int[] Pointers)
    {
        Nodes = new Node[Pointers.Length];

        // first fill the array with objects
        for (int i = 0; i < Nodes.Length; i++) { Nodes[i] = new Node(i); }

        // apply their pointers
        for(int i = 0; i < Nodes.Length; i++) { Nodes[i].toNode = Nodes[Pointers[i]]; }
    }


    // map all of the nodes by filling their mapping properties
    public void MapAllNodes()
    {
        for(int i=0; i<Nodes.Length; i++)
        {
            if (!Nodes[i].isMapped)
                MapPath(Nodes[i], 1);
        }
    }

    // recursively map a path of unmapped nodes, starting from curr
    //  returns true if curr is in a cycle, false otherwise
    public Boolean MapPath(Node curr, int pathNo)
    {
        Boolean inCycle = false;
        curr.pathNo = pathNo;

        Node next = curr.toNode;

        if (next.IsInPath)
        {
            // we have found a new cycle
            Cycle Cycle = new Cycle(this, next, curr.pathNo - next.pathNo + 1);
            curr.Map(Cycle);
            return true;
        }
        else if (next.isMapped)
        {
            // we are joining an already partially mapped tree
            if (next.IsInCycle)
            {
                // next is a tree-Base, the top of our tree and also in the cycle
                curr.Map(next.Cycle, false, next, 1);
            }
            else
            {
                // next is a normal tree-node
                curr.Map(next.Cycle, false, next.BaseNode, next.Distance + 1);
            }
            return false;
        }
        else
        {
            // continue forward on the path, recurse to the next node
            inCycle = MapPath(next, pathNo+1);

            if (inCycle)
            {
                if (next.Cycle.Base == next || next.Distance == 0)
                {
                    //we have returned from the cycleBase, which is also a treeBase
                    // so, switch from Cycle to Tree
                    curr.Map(next.Cycle, false, next, 1);
                    return false;
                }
                else
                {
                    // still in the cycle
                    curr.Map(next.Cycle);
                }
            }
            else
            {
                //returned in tree
                curr.Map(next.Cycle, false, next.BaseNode, next.Distance + 1);
            }

            return inCycle;
        }

    }


    // Given two starting nodes, determine how many steps it takes until their
    //  paths collide. Returns -1 if they will never collide.
    public int FindCollision(int index1, int index2)
    {
        Node node1 = Nodes[index1];
        Node node2 = Nodes[index2];

        // eliminate trivial cases
        if (node1.Cycle != node2.Cycle)
            return -1;      // cant collide if they're in different subgraphs
        else if (node1 == node2)
            return 0;       // if they're the same node, then distance = 0
        else if (node1.IsInCycle && node2.IsInCycle)
            return -1;      // different nodes in a cycle never collide
        
        else
        {  // they're both in the same subgraph, use math to tell if they collide

            // get their distances to the cycle base
            int dist1 = node1.Distance + (node1.IsInCycle ? 0 : node1.BaseNode.Distance);
            int dist2 = node2.Distance + (node2.IsInCycle ? 0 : node2.BaseNode.Distance);
            int cycleLen = node1.Cycle.Length;

            // use math:  modulo(cycle length)
            if ((dist1 % cycleLen) != (dist2 % cycleLen))
            {
                return -1;      // incompatible distances: cannot possibly collide
            }
            else
            {
                // they must collide somewhere, figure out how far that is
                if (node1.IsInCycle || node2.IsInCycle)
                {
                    // if one is in the cycle, they will collide when
                    // the other one reaches the cycle (it's treeBase)
                    return (!node1.IsInCycle ? node1.Distance : node2.Distance);
                }
                else if (node1.BaseNode != node2.BaseNode)
                {
                    // They are in different trees:  they will collide at
                    //  the treeBase of the node that is farther
                    return Math.Max(node1.Distance, node2.Distance);
                }
                else
                {
                    // They are in the same tree:
                    if (node1.Distance != node2.Distance)
                    {
                        //if they are in the same tree, but have different distances
                        //  to the treeBase, then they will collide at the treeBase
                        //  when the farther one arrives at the treeBase
                        return Math.Max(node1.Distance, node2.Distance);
                    }
                    else
                    {
                        //  the hard case, have to walk down their paths
                        //  to find their LCA (Lowest Common Ancestor)
                        return findTreeDistance(node1, node2);
                    }
                }
            }
        }
    }


    int findTreeDistance(Node node1, Node node2)
    {
        if (node1 == node2) return 0;

        // normalize their order
        if (node1.index > node2.index)
        {
            var tmp = node1;
            node1 = node2;
            node2 = tmp;
        }

        // check the cache
        int dist;
        if (cachedDistances.ContainsKey((node1,node2)))
        {
            dist = cachedDistances[(node1, node2)];
        }
        else
        {
            // keep recursing to find where they meet
            dist = findTreeDistance(node1.toNode, node2.toNode) + 1;
            // cache this new distance
            cachedDistances.Add((node1, node2), dist);
        }
        return dist;
    }
}

Node Class:

// Represents a node in the MPDG (Mono-Pointing Directed Graph) with the constraint
//  that all nodes have exactly one out-edge ("toNode").
class Node
{
    // Primary properties (from input)
    public int index { get; set; }      // this node's unique index (to the original array)
    public Node toNode { get; set; }    // what our single out-edge is pointing to


    public Node(int Index_) { this.index = Index_; }


    // Mapping properties
    // (these must be filled-in to finish mapping the node)

    //  The unique cycle of this node's subgraph (all MPDG-subgraphs have exactly one)
    public Cycle Cycle;

    //  Every node is either in their subgraphs cycle or in one of the inverted
    // trees whose apex (base) is attached to it.  Only valid when BaseNode is set.
    // (tree base nodes are counted as being in the cycle)
    public Boolean IsInCycle = false;

    // The base node of the tree or cycle that this node is in.
    //  If (IsInCycle) then it points to this cycle's base node (cycleBase)
    //  Else it points to base node of this node's tree (treeBase)
    public Node BaseNode;

    //  The distance (hops) from this node to the BaseNode
    public int Distance = -1;    // -1 if not yet mapped

    // Total distance from this node to the cycleBase
    public int TotalDistance { get { return Distance + (IsInCycle ? 0 : BaseNode.Distance); } }


    // housekeeping for mapping nodes
    public int pathNo = -1;          // order in our working path

    // Derived (implicit) properties
    public Boolean isMapped { get { return Cycle != null; } }
    public Boolean IsInPath { get { return (pathNo >= 0); } }


    public void Map(Cycle Cycle, bool InCycle = true, Node BaseNode = null, int distance_ = -1)
    {
        this.Cycle = Cycle; 
        IsInCycle = InCycle;

        if (InCycle)
        {
            this.BaseNode = Cycle.Base;
            Distance = (Cycle.Length + (Cycle.Base.pathNo - pathNo)) % Cycle.Length;
        }
        else
        {
            this.BaseNode = BaseNode;
            Distance = distance_;
        }

        pathNo = -1;    // clean-up the path once we're done
    }
}

Cycle Class:

// represents the cycle of a unique MPDG-subgraph
//  (should have one of these for each subgraph)
class Cycle
{
    public MPDGraph Graph; // the MPDG that contains this cycle
    public Node Base;      // the base node of a subgraph's cycle
    public int Length;     // the length of this cycle

    public Cycle(MPDGraph graph_, Node base_, int length_)
    {
        Graph = graph_;
        Base = base_;
        Length = length_;
    }
}

Performance Measurements:

Node Count Build & Map Graph
mean microsecs
Question Count All Questions
mean microsecs
Question mean
microseconds
Total mean
microseconds
50 0.9 1225 26 0.0212 26.9
500 10.1 124750 2267 0.0182 2277.1
1000 23.4 499500 8720 0.0175 8743.4
5000 159.6 12497500 229000 0.0183 229159.6
10000 345.3 49995000 793212 0.0159 793557.3

It is only possible for a collision to occur on a node that has more than 1 link leading to it. Node D in your example.

Let's call these nodes "crash sites"

So you can prune your graph down to just the crash site nodes. The nodes that lead to the crash site nodes become attributes of the crash site nodes.

Like this for your example:

D : { A,B,C }, { E,F,K }

A collision can ONLY occur if the starting nodes are on two different attribute lists of the same crash site node.

Once you are sure a crash can occurr, then you can check that both starting nodes are the same distance from the crash site.

Algorithm:

Prune graph to crash site nodes
LOOP over questions
Get 2 start nodes
LOOP over crash sites
   IF start nodes on two different attributes of crash site
        IF start nodes are equi-distant from crash site
             report crash time
             BREAK from crash site loop

Here is a randomly generated graph with 50 nodes where every node has one out edge connected to another node chosen randomly

enter image description here

The collision sites are

5 7 8 9 10 11 18 19 23 25 31 33 36 37 39

So the algorithm need only loop over 15 nodes, at most, instead of 50.

The answer to the question 'do two particular nodes collide?' is almost always 'NO'. It is kind of boring that way. So let's ask a slightly different question: 'for a particular graph, which pairs of nodes result in a collision?' This requires the same algorithm ( applied to every pair of nodes ) but alway produces an interesting answer.

for this graph

enter image description here

I get this answer

0 and 29 collide at 41
1 and 5 collide at 40
2 and 23 collide at 13
8 and 16 collide at 34
8 and 22 collide at 34
8 and 39 collide at 34
9 and 30 collide at 37
10 and 31 collide at 25
11 and 47 collide at 23
12 and 28 collide at 25
12 and 35 collide at 25
12 and 49 collide at 25
13 and 38 collide at 27
14 and 44 collide at 1
15 and 17 collide at 0
15 and 18 collide at 0
15 and 37 collide at 0
16 and 22 collide at 34
16 and 39 collide at 34
17 and 18 collide at 0
17 and 37 collide at 0
18 and 37 collide at 0
20 and 26 collide at 9
20 and 42 collide at 9
20 and 43 collide at 9
21 and 45 collide at 24
22 and 39 collide at 34
25 and 34 collide at 3
26 and 42 collide at 9
26 and 43 collide at 9
28 and 35 collide at 25
28 and 49 collide at 25
32 and 48 collide at 34
33 and 36 collide at 7
35 and 49 collide at 25
42 and 43 collide at 9

Some timing results

Node Count Crash Sites
millisecs
Question Count Question mean
microsecs
50 0.4 1225 0.02
500 50 124750 0.02
5000 5500 ~12M 0.02
10000 30000 ~50M 0.03
30000 181000 ~450M 0.6

Notes:

  • The mean time for a question is the average of checking every possible pair of nodes for a possible collision.
  • Answering a single question is extremely fast, about 20 nanoseconds for moderately sized graphs ( < 10,000 nodes ) [ A previous timing report included outputting the results when a collision was found, which takes much longer than detecting the collision. These results were taken with all output to the console commented out. ]
  • Setting up the crash sites and their tributaries gets slow with moderately sized graphs ( > 5,000 nodes ). It is only worth doing if a lot of questions are going to be asked.

The code for this is available at https://github.com/JamesBremner/PathFinder

Related