Solution to Problem 2: Critical Intersections
This problem is just a cloaked version of a well known problem about graphs. Let us first see, how to model this problem via graphs. Let us recall, from the solution to the "The Great Escape Problem" of October 2004, the basic definitions regarding graphs: A graph, say G, consists of two sets, a set of vertices or nodes (V) and a set of edges (E). The set of vertices could be given finite set. Each edge is a set consisting of two vertices. We may interpret the set of edges as describing which pairs of vertices are "neighbours". Example: Here is a graph G = (V,E) with 7 vertices and 7 edges: V = { 1,2,3,4,5,6,7 } E = { {1,2},{1,3},{2,3},{3,4},{5,6},{5,7},{6,7} } It is often convenient to draw graphs with boxes or circles to represent the nodes and lines/curves connecting boxes/circles to denote edges: --- --- --- | 1 |---------| 2 | | 7 | --- /--- /---\ | / / \ | /-----/ / \ | / / \ ---/ --- ---/ \ --- | 3 |--------| 4 | | 5 |------------| 6 | --- --- --- --- Note that the placement of the boxes or the style of drawing the edges are irrelevant. The "real" graph is given by the two sets V and E and the picture is just to help us think. A path in a graph is a sequence of vertices v1,v2,...,vk such that {v1,v2}, {v2,v3} ... {v{k-1}vk} are edges. For example, 1,2,3,4 is a path in the above graph. The length of a path is the number of edges in the path. For example, the length of the path 1,2,3,4 is 3. We say that two vertices u and v are connected if there is a path u=v_1, v_2, ..., v_k = v. In this case we say that v is reachable from u (and u is reachable from v, since v_k v_{k-1} ... v_1 is a path from v to u). We can think of the intersections and roads of Siruseri as a graph. The vertices of this graph are the intersections of Siruseri and the edges are the streets. Thus, an intersection is reachable from another if and only if it is reachable in the graph theoretic sense (described above). Notice that we have been assured that initially every intersection is reachable from every other intersection. A graph in which every vertex is reachable from every other vertex is called a "connected graph". (The example graph defined above is not connected, since there is no path from vertex 1 to vertex 7, for example, and thus it cannot represent the intersections and streets of Siruseri.) What is a critical intersection? Recall, that an intersection Y is critical if there are two other intersections X and Z such that all the routes from X to Z go through Y. (Since streets are bidirectional, it follows that all the routes from Z to X also go through Y). In the language of graphs, a vertex Y is a critical intersection if there are vertices X and Z such that all the paths from X to Z go through Y (i.e. Y appears in any path beginning at X and ending in Y). Such vertices are called "Articulation Points" in the language of graphs. Our aim is to count the number of articulation points in the graph representing the intersections and roads of Siruseri. So how do we find whether a vertex Y in a graph G is an articulation point? Suppose we drop Y (and all the edges incident on it) from G and get the graph G-Y = ( V - {Y}, E - {{X,Y} | X in V} ). If Y is an articulation point then, G-Y is no longer a connected graph. Conversely, if Y is any vertex in a connected graph G such that G-Y is not connected then Y is an articulation point of G. Thus, to determine whether Y is an articulation point, it suffices to check if the graph G-Y is connected. This can be done, for example, by the breadth-first search algorithm described in the solution to the "The Great Escape" problem from October 2004 problem set in time proportional to N*N where N is the number of vertices. We can repeat this for each Y to determine if it is an articulation point. The overall time taken would thus be N*N*N. We now describe a different algorithm called depth-first search algorithm to explore the vertices of a graph and determine whether it is connected. Like breadth-first search it takes time proportional to N*N to determine if the graph is connected (actually time proportional to the number of edges, just like breadth-first search, but we leave that discussion for later.) However, it is easier to implement than the breadth-first search and has other interesting properites. Recall that the breadth-first search algorithm visits the vertices of the graph as follows: visit every neighbour of the current vertex, then visit all the neighbours of the neighbours and so on. It unravels the graph level by level --- first the vertex itself, then the vertices at distance 1, then the vertices at distance 2 and so on. On the other hand, the depth-first search explores the graph by going as far as possible via a picked neighbour (cutting through many levels) before backtracking and exploring other neighbours. The depth-first search algorithm is rather easy to understand and here is its code: ============================================================================ /* Mark all vertices as unvisited */ for each vertex v in G { Visited[v] = 0 } dfs(u) /* start the dfs algorithm at some vertex u */ /* if the graph is connected then the dfs would have visited all the vertices. Otherwise there would be unvisited vertices */ if there is vertex v with Visited[v]=0 then { print "Graph Not Connected" } else { print "Graph is Connected" } /* The dfs algorithm */ dfs(v) { if (Visited[v]=1) then return /* v has been visited in the past*/ Visited[v] = 1 /* visit v */ DFSNo[v] = curr /* v is the curr'th vertex visited */ curr = curr + 1 for each neighbour w of v do { dfs(w) } } -------------------------------------------------------------------------- To illustrate how this algorithm works, here is a graph and a description of a run of the dfs algorithm on this graph: --- --- --- | A |---------| B |------| F | --- /--- --- | / | /-----/ | / ---/ --- --- | C |--------| D |--------| E | --- --- --- Suppose we call dfs at the vertex (C) then, the sequence of calls made is as follows:(Any call to an already visited vertex is placed inside square parantheses) dfs(C) | |-------> dfs(A) | | | |--------> dfs(B) | | | | | |-------> [dfs(C)] | | |-------> [dfs(B)] | | |-------> dfs(F) | | | | | |-------> [dfs(B)] | | | |--------> [dfs(C)] | |-------> [dfs(B)] | | |-------> dfs(D) | |--------> dfs(E) | | | |-------> [dfs(D)] | |---------> [dfs(C)] The order in which the vertices are visited is C, A, B, F, D, E. Starting at the vertex C and having picked the neighbour A, the dfs visited all the vertices reachable via A (namely A, B, and F) before returning to explore the graph via the other neighbour $D$. Notice, that each dfs(v) is called as many times as there are edges incident on v. Thus each vertex is examined as many times as its degree and thus the overall running time is proportional to the number of edges in G. However, that is true only if we can examine each neighbour of a vertex in time proportional to its degree. However, if we maintain the graph as an adjacency matrix then this algorithm will take time proportional to N*N. Thus the overall time taken to find the articulation points is N*N*N. (This will get 100% marks for the range of inputs mentioned in the problem statement.) Exercise: Try and write a iterative version of the dfs algorithm (i.e. try to program it without recursion.) Hint: Recall that the vertices were maintained in a queue in the BFS algorithm. Here you would need a "stack". i.e. an array into with vertices are inserted and removed where the element removed is the last one to be inserted. It turns out that a single DFS of the graph G can determine all the articulation points in the graph! However, we postpone a description of that idea to a different day.