UVA - 12363 - Hedge Mazes![]()
This article will explain my thought process to solve a problem called Hedge Mazes (PDF) from UVA Online Judge. Read the original problem statement before reading this article.
Keywords: Graph Theory, Bridge, Connected Components
Problem Summary![]()
Given:
List of rooms
List of corridors between 2 rooms
List of queries containing the start and end room
We want to:
Determine whether exactly 1 path exists from the start to the end room for each query
Glossary![]()
CC — Connected Components
DFS — Depth-first Search
Observation![]()
First, we need to be extra careful with the time limit of this problem. It has 1 second time limit, so for Java user, this means we must use fast IO and primitive data structure as much as possible.
Next, we can model the relationship between rooms and corridors as an undirected graph, where the rooms is the vertices and the corridors is the edges.
After that, we need to figure out the way to ensure that only 1 path exists between any 2 pair of vertices. This is similar to the concept of bridge. The standard definition of a bridge is an edge whose removal increases the number of connected components of the graph. In our case, bridge ensure that there is only 1 path between the edge vertices. This means that 1 path exists between any 2 pair of vertices if and only if the path consist of bridges.
After we’ve known the way to identify pair of vertices with 1 path, we need to figure out how to efficiently answer the queries. Since we know that the path consist of bridges, we could rebuild the graph using the bridges only. Since we want to know whether the start could reach the end room or not, this is similar to the concept of CC. If the 2 vertices are part of the same CC, then a path exists between them.
Algorithm![]()
We could use Tarjan algorithm to identify bridges in an undirected graph. This algorithm is able to find a bridge by traversing the graph using DFS and keeping track of the discovery times and low-link values.
After we’ve identified the bridges, we need to rebuild the graph and identify the CC. This can be done with Disjoint Set by simply combining the bridge vertices together.
Finally, to determine whether exactly 1 path exists between 2 vertices, use the Disjoint Set to check whether 2 vertices belong to the same CC or not.
The time complexity for this solution is O({total queries} + {total vertices} + {total edges}) or O(10^5).
Implementation![]()
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
/**
* 12363 - Hedge Mazes
* Time limit: 1.000 seconds
* https://onlinejudge.org/index.php?option=onlinejudge&page=show_problem&problem=3785
*/
public class Main {
public static void main(final String... args) throws IOException {
final FastReader in = new FastReader(System.in, 1 << 8);
final BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
final Process process = new Process();
while (true) {
final Input input = new Input();
input.totalRooms = in.nextInt();
input.totalCorridors = in.nextInt();
input.totalQueries = in.nextInt();
if (input.isEOF()) break;
input.corridors = new int[input.totalCorridors][];
for (int i = 0; i < input.totalCorridors; i++) {
final int[] corridor = new int[2];
corridor[0] = in.nextInt();
corridor[1] = in.nextInt();
input.corridors[i] = corridor;
}
input.queries = new int[input.totalQueries][];
for (int i = 0; i < input.totalQueries; i++) {
final int[] query = new int[2];
query[0] = in.nextInt();
query[1] = in.nextInt();
input.queries[i] = query;
}
final Output output = process.process(input);
for (int i = 0; i < output.totalAnswers; i++) {
out.write(output.answers[i]);
out.write('\n');
}
out.write('-');
out.write('\n');
}
in.close();
out.flush();
out.close();
}
}
final class FastReader {
private final InputStream stream;
private final byte[] buffer;
private int length = 0;
private int offset = 0;
private final byte[] readBuffer;
public FastReader(InputStream stream, int bufferSize) {
this.stream = stream;
this.buffer = new byte[bufferSize];
this.readBuffer = new byte[bufferSize];
}
public boolean hasNextInt() throws IOException {
skipUntil(b -> !Character.isWhitespace(b));
return !end() && (buffer[offset] == '-' || Character.isDigit(buffer[offset]));
}
public int nextInt() throws IOException {
if (!hasNextInt()) return 0;
boolean negative = buffer[offset] == '-';
if (negative) offset++;
int length = readWhile(Character::isDigit, readBuffer);
int value = 0;
for (int i = 0; i < length; i++) value = value * 10 + readBuffer[i] - '0';
return negative ? -value : value;
}
public boolean hasNext() throws IOException {
skipUntil(b -> !Character.isWhitespace(b));
return !end();
}
public String next() throws IOException {
if (!hasNext()) return "";
int length = readWhile(b -> !Character.isWhitespace(b), readBuffer);
return new String(readBuffer, 0, length);
}
private void skipUntil(Function<Byte, Boolean> criteria) throws IOException {
while (true) {
if (exhausted()) read();
if (end()) return;
if (criteria.apply(buffer[offset])) return;
++offset;
}
}
private int readWhile(Function<Byte, Boolean> criteria, byte[] output) throws IOException {
for (int i = 0; ; i++) {
if (exhausted()) read();
if (end()) return i;
if (!criteria.apply(buffer[offset])) return i;
output[i] = buffer[offset];
offset++;
}
}
private boolean end() {
return length == -1;
}
private boolean exhausted() {
return offset >= length;
}
private void read() throws IOException {
length = stream.read(buffer);
offset = 0;
}
public void close() throws IOException {
stream.close();
}
}
class Input {
public int totalRooms;
public int totalCorridors;
public int totalQueries;
public int[][] corridors;
public int[][] queries;
public boolean isEOF() {
return totalRooms == 0 && totalCorridors == 0 && totalQueries == 0;
}
}
class Output {
public int totalAnswers;
public char[] answers;
}
class Process {
public Output process(final Input input) {
final Output output = new Output();
output.totalAnswers = input.totalQueries;
output.answers = new char[input.totalQueries];
final DisjointSet connectedComponents = new DisjointSet(input.totalRooms);
for (int[] corridor : input.corridors) {
connectedComponents.union(corridor[0], corridor[1]);
}
final UndirectedGraph graph = createGraph(input);
final TarjanBridges tarjanBridges = new TarjanBridges(graph);
final DisjointSet bridgeConnectedComponents = new DisjointSet(input.totalRooms);
final int[][] bridges = tarjanBridges.getBridges();
for (final int[] bridge : bridges) {
bridgeConnectedComponents.union(bridge[0], bridge[1]);
}
for (int i = 0; i < input.totalQueries; i++) {
final int[] query = input.queries[i];
final int cc1 = connectedComponents.find(query[0]);
final int cc2 = connectedComponents.find(query[1]);
if (cc1 != cc2) {
output.answers[i] = 'N';
continue;
}
final int bcc1 = bridgeConnectedComponents.find(query[0]);
final int bcc2 = bridgeConnectedComponents.find(query[1]);
if (bcc1 != bcc2) {
output.answers[i] = 'N';
continue;
}
output.answers[i] = 'Y';
}
return output;
}
private UndirectedGraph createGraph(final Input input) {
final UndirectedGraph.Builder builder = new UndirectedGraph.Builder(input.totalRooms);
for (final int[] corridor : input.corridors) {
builder.add(corridor[0], corridor[1]);
}
return builder.build();
}
}
final class DisjointSet {
private final int[] parents;
public DisjointSet(final int maxVertex) {
parents = new int[maxVertex + 1];
Arrays.fill(parents, -1);
}
public int find(final int child) {
final int parent = parents[child] == -1 ? child : parents[child];
if (parent == child) {
return parent;
} else {
final int grandparent = find(parent);
parents[child] = grandparent;
return grandparent;
}
}
public void union(final int child1, final int child2) {
final int parent1 = find(child1);
final int parent2 = find(child2);
parents[parent2] = parent1;
}
}
final class UndirectedGraph {
private final int[][] edges;
private UndirectedGraph(final Builder builder) {
edges = new int[builder.edges.length][];
for (int vertex = 0; vertex < builder.edges.length; vertex++) {
final List<Integer> destinations = builder.edges[vertex] == null ? Collections.emptyList() : builder.edges[vertex];
edges[vertex] = destinations.stream().mapToInt(Integer::intValue).distinct().toArray();
}
}
public int[][] get() {
return edges;
}
public int[] get(final int from) {
return edges[from];
}
static class Builder {
private final LinkedList<Integer>[] edges;
public Builder(final int maxVertex) {
this.edges = new LinkedList[maxVertex + 1];
}
public void add(final int first, final int second) {
addUni(first, second);
addUni(second, first);
}
private void addUni(final int from, final int into) {
if (edges[from] == null) edges[from] = new LinkedList<>();
edges[from].add(into);
}
public UndirectedGraph build() {
return new UndirectedGraph(this);
}
}
}
final class TarjanBridges {
private static final int NULL = -1;
private final UndirectedGraph graph;
private int[] discovery;
private int[] low;
private int[] parent;
private LinkedList<int[]> bridgeList;
private int[][] bridges;
private int time;
public TarjanBridges(final UndirectedGraph graph) {
this.graph = graph;
initArrays();
initBridges();
}
private void initArrays() {
this.discovery = new int[graph.get().length];
this.low = new int[graph.get().length];
this.parent = new int[graph.get().length];
Arrays.fill(discovery, NULL);
Arrays.fill(low, NULL);
Arrays.fill(parent, NULL);
}
private void initBridges() {
this.bridgeList = new LinkedList<>();
for (int vertex = 0; vertex < graph.get().length; vertex++) {
if (discovery[vertex] == NULL) {
depthFirstSearch(vertex);
}
}
bridges = bridgeList.toArray(new int[0][0]);
}
private void depthFirstSearch(final int vertex) {
discovery[vertex] = low[vertex] = ++time;
for (int nextVertex : graph.get(vertex)) {
if (discovery[nextVertex] == NULL) {
parent[nextVertex] = vertex;
depthFirstSearch(nextVertex);
low[vertex] = Math.min(low[vertex], low[nextVertex]);
if (low[nextVertex] > discovery[vertex]) {
foundBridge(vertex, nextVertex);
foundBridge(nextVertex, vertex);
}
} else if (nextVertex != parent[vertex]) {
low[vertex] = Math.min(low[vertex], discovery[nextVertex]);
}
}
}
private void foundBridge(final int fromVertex, final int intoVertex) {
bridgeList.addLast(new int[]{fromVertex, intoVertex});
}
public int[][] getBridges() {
return bridges;
}
}