Skip to main content

Guidelines

Whenever you have a clear relation between different elements given by the problem statement you should think if you can reframe the initial problem to a graph. Below you find cheatsheet for most common graph algorithms.

Generalized topological sort implementation

private List<Integer> topologicalSort(Map<Integer, List<Integer>> graph, int[] degree, int n) {
List<Integer> result = new ArrayList<Integer>();
Queue<Integer> q = new LinkedList();

for (int key : graph.keySet()) {
if(degree[key] == 0) {
q.add(key);
}
}

while(!q.isEmpty()) {
int node = q.poll();
result.add(node);
for (int neighbor : graph.get(node)) {
if(--degree[neighbor] == 0) {
q.add(neighbor);
}
}
}
return result.size() == n ? result : new ArrayList();
}
public class MinimumSpanningTree {

// Prim's Algorithm
public static List<Edge> primMST(Graph graph) {
List<Edge> mst = new ArrayList<>();
PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));
Set<Integer> visited = new HashSet<>();
visited.add(0);
pq.addAll(graph.adjList.get(0));

while (!pq.isEmpty() && visited.size() < graph.vertices) {
Edge minEdge = pq.poll();
if (!visited.contains(minEdge.to)) {
mst.add(minEdge);
visited.add(minEdge.to);
pq.addAll(graph.adjList.get(minEdge.to));
}
}

return mst;
}

// Kruskal's Algorithm
public static List<Edge> kruskalMST(Graph graph) {
List<Edge> mst = new ArrayList<>();
PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(e -> e.weight));
UnionFind uf = new UnionFind(graph.vertices);
pq.addAll(graph.edges);

while (!pq.isEmpty() && mst.size() < graph.vertices - 1) {
Edge minEdge = pq.poll();
if (uf.find(minEdge.from) != uf.find(minEdge.to)) {
mst.add(minEdge);
uf.union(minEdge.from, minEdge.to);
}
}

return mst;
}

static class UnionFind {
int[] parent, rank;

public UnionFind(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}

public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}

public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootX] = rootY;
rank[rootY]++;
}
}
}
}

The minimum spanning tree is the spanning tree with the lowest cost (sum of edge weights).

The time complexity of Prim's Algorithm is O(E log V), where E is the number of edges and V is the number of vertices. The space complexity of Prim's Algorithm is O(V + E), where V is the number of vertices and E is the number of edges. If the graph is fully connected the time complexity can degenerate to O(V^2). The algorithm uses a priority queue to keep track of the minimum edge weights, and the queue is updated each time a new vertex is added to the MST. The size of the priority queue is at most E, and each operation to add or remove an element from the queue takes O(log E) time. (E log E <= E log V as show below).

The time complexity of Kruskal's Algorithm is O(E log V), where E is the number of edges and V is the number of vertices. The space complexity of Kruskal's Algorithm is O(V + E), where V is the number of vertices and E is the number of edges (we keep all edges in priority queue, and also parent array for union for all vertices). Note that you can also make a case that time complexity is O(E log E) but we prefer tight upper bound. We have E <= V^2 (fully connected graph), so ElogE <= ElogV^2 = 2 Elog V = O(E log V).

To choose between Prim's and Kruskal's algorithm, it is important to consider the density of the graph. If the graph is dense, Prim's algorithm is generally preferred, while if the graph is sparse, Kruskal's algorithm is a better choice.

A bipartite graph is possible if the graph coloring is possible using two colors such that vertices in a set are colored with the same color.

boolean isBipartite() {
boolean[] visited = new boolean[V];
int[] color = new int[V];

for (int i = 0; i < V; i++) {
if (!visited[i] && !dfs(i, visited, color, 1)) {
return false;
}
}
return true;
}

boolean dfs(int v, boolean[] visited, int[] color, int c) {
visited[v] = true;
color[v] = c;
for (int i = 0; i < adjList[v].size(); i++) {
int neighbor = adjList[v].get(i);
if (!visited[neighbor]) {
if (!dfs(neighbor, visited, color, 3-c)) {
return false;
}
} else if (color[neighbor] == c) {
return false;
}
}
return true;
}

It's graph coloring problem, if we can color the graph with two colors we know it's a bipartite graph. We do a DFS search and each time check if we can color all adjancent vertices with different color (either 1 or 2, we use 3-c).

public static int[] dijkstra(List<List<Edge>> adjList, int start) {
int n = adjList.size();
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;

PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(v -> dist[v]));
pq.add(start);

while (!pq.isEmpty()) {
int u = pq.poll();
for (Edge e : adjList.get(u)) {
int v = e.to;
int weight = e.weight;
if (dist[u] != Integer.MAX_VALUE && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.add(v);
}
}
}

return dist;
}


public static int[] bellmanFord(List<List<Edge>> adjList, int start, int n) {
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;

for (int i = 0; i < n - 1; i++) {
for (List<Edge> edges : adjList) {
for (Edge edge : edges) {
if (dist[edge.from] != Integer.MAX_VALUE &&
dist[edge.from] + edge.weight < dist[edge.to]) {
dist[edge.to] = dist[edge.from] + edge.weight;
}
}
}
}

// Check for negative cycles
for (List<Edge> edges : adjList) {
for (Edge edge : edges) {
if (dist[edge.from] != Integer.MAX_VALUE &&
dist[edge.from] + edge.weight < dist[edge.to]) {
throw new IllegalArgumentException("Graph contains a negative cycle");
}
}
}

return dist;
}

Bellman-Ford algorithm is a single-source (we have a start) shortest path algorithm, which allows for negative edge weight and can detect negative cycles in a graph. Dijkstra algorithm is also another single-source shortest path algorithm. However, the weight of all the edges must be non-negative. In Bellman-Ford if there is a negative cycle in the graph, then the shortest path between some vertices can have an infinite negative weight. This is because we can keep going around the negative cycle and decreasing the weight of the path. The time complexit of Bellman-Ford is O(VE), while for Dijkstra we have the same time complexity as for Prim algo O(E log V) (you can also present int as O(V + E log E), but in dense graph E dominates the V, E <= V^2).

If we have a DAG (direct acyclig graph) we can get the shortest path using topological order of the vertices.

public static int[] dagShortestPath(Graph graph, int source) {
List<Integer> sortedVertices = topologicalSort(graph);
int[] distances = new int[graph.vertices];
Arrays.fill(distances, Integer.MAX_VALUE);
distances[source] = 0;
for (int u : sortedVertices) {
for (Edge edge : graph.adjList.get(u)) {
int v = edge.to;
int weight = edge.weight;
distances[v] = Math.min(distances[v], distances[u] + weight);
}
}
return distances;
}

And if we want all pairs shortest path we can use Floyd-Warshall algorithm

public static int[][] floydWarshall(int[][] graph) {
int V = graph.length;
int[][] dist = new int[V][V];

// Initialize distance array
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
dist[i][j] = graph[i][j];
}
}

// Calculate shortest path between all pairs of vertices
for (int k = 0; k < V; k++) {
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (dist[i][k] != Integer.MAX_VALUE && dist[k][j] != Integer.MAX_VALUE &&
dist[i][j] > dist[i][k] + dist[k][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
return dist;
}

The time complexity is O(V^3). With minor modification we can implement Warshall's algorithm to find the transitive closure of a directed graph. The transitive closure of a graph is a new graph that represents the transitive relationship between the vertices of the original graph. In other words, it tells us if there is a path from one vertex to another in the original graph.

public static int[][] transitiveClosure(int[][] graph) {
int n = graph.length;
int[][] closure = new int[n][n];

// Copy the graph to closure
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
closure[i][j] = graph[i][j];
}
}

// Apply Warshall's algorithm
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
closure[i][j] = closure[i][j] | (closure[i][k] & closure[k][j]);
}
}
}

return closure;
}

Fold-Fulkerson can find a maximum matching in a bipartite graph by reducing the problem of maximum bipartite matching to network flow.

private static int fordFulkerson(int source, int sink) {
int maxFlow = 0;
while (bfs(source, sink)) {
int pathFlow = Integer.MAX_VALUE;
int u = sink;
while (u != source) {
int v = parent[u];
pathFlow = Math.min(pathFlow, graph[v][u]);
u = v;
}
u = sink;
while (u != source) {
int v = parent[u];
graph[v][u] -= pathFlow;
graph[u][v] += pathFlow;
u = v;
}
maxFlow += pathFlow;
}
return maxFlow;
}
private static boolean bfs(int source, int sink) {
Arrays.fill(visited, false);
Queue<Integer> queue = new LinkedList<>();
queue.add(source);
visited[source] = true;
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v = 0; v < graph.length; v++) {
if (!visited[v] && graph[u][v] > 0) {
visited[v] = true;
parent[v] = u;
queue.add(v);
}
}
}
return visited[sink];
}

We use BFS (the number of edges on the path) to check for path between source and sink. The time complexity is O(V * E^2) (there are at most EV augmenting paths and to find each of those we spend O(E) time, actually O(E+V), but V get dominated by E since we're generally talking about connected graphs). The maximum matching problem asks to find a maximum-size set of node pairs in an undirected graph such that each pair is connected with an edge and each node belongs to at most one pair. We can reduce the bipartite maximum matching problem to the maximum flow problem by adding two new nodes to the graph: a source and a sink. We also add edges from the source to each left node and from each right node to the sink. After this, the size of a maximum flow in the graph equals the size of a maximum matching in the original graph. Note that all edges now will have a weight of 1.

Finding a maximum flow: What is the maximum amount of flow we can send from a node to another node? Finding a minimum cut: What is a minimum-weight set of edges that separates two nodes of the graph? It turns out that a maximum flow and a minimum cut are always equally large, so the concepts are two sides of the same coin.

Kosaraju’s algorithm is an efficient method for finding the strongly connected components of a directed graph. The algorithm performs two depth-first searches: the first search constructs a list of nodes according to the structure of the graph, and the second search forms the strongly connected components. The time complexity is that of DFS. This works because even when reversing the graph edges the strongly connectect component will remain connected, only the links between different component will be affected.

public static List<List<Integer>> stronglyConnectedComponents(List<List<Integer>> graph) {
int n = graph.size();

// step 1: compute the reverse graph
List<List<Integer>> reverseGraph = new ArrayList<>();
for (int i = 0; i < n; i++) {
reverseGraph.add(new ArrayList<>());
}
for (int u = 0; u < n; u++) {
for (int v : graph.get(u)) {
reverseGraph.get(v).add(u);
}
}

// step 2: run depth-first search on the reverse graph
Stack<Integer> stack = new Stack<>();
boolean[] visited = new boolean[n];
for (int u = 0; u < n; u++) {
if (!visited[u]) {
dfs(reverseGraph, u, visited, stack);
}
}

// step 3: run depth-first search on the original graph in reverse order of finishing times
List<List<Integer>> components = new ArrayList<>();
visited = new boolean[n];
while (!stack.isEmpty()) {
int u = stack.pop();
if (!visited[u]) {
List<Integer> component = new ArrayList<>();
dfs(graph, u, visited, component);
components.add(component);
}
}

return components;
}

// helper method for depth-first search
public static void dfs(List<List<Integer>> graph, int u, boolean[] visited, List<Integer> path) {
visited[u] = true;
for (int v : graph.get(u)) {
if (!visited[v]) {
dfs(graph, v, visited, path);
}
}
path.add(u);
}

An Eulerian path is a path that goes exactly once through each edge of the graph. An Eulerian circuit is an Eulerian path that starts and ends at the same node. Hierholzer’s algorithm is an efficient method for constructing an Eulerian circuit.

public static List<Integer> eulerianCycle(List<List<Integer>> graph) {
int n = graph.size();

// initialize the degree array and the stack
int[] degree = new int[n];
for (int u = 0; u < n; u++) {
for (int v : graph.get(u)) {
degree[u]++;
}
}
Stack<Integer> stack = new Stack<>();
stack.push(0);

// find the cycle using depth-first search
List<Integer> cycle = new ArrayList<>();
while (!stack.isEmpty()) {
int u = stack.peek();
if (degree[u] == 0) {
cycle.add(u);
stack.pop();
} else {
int v = graph.get(u).get(degree[u] - 1);
degree[u]--;
stack.push(v);
graph.get(v).remove(Integer.valueOf(u));
}
}

// reverse the cycle to get the correct order
Collections.reverse(cycle);
return cycle;
}