Advanced Graph Problems
TL;DR
Tarjan's algorithm finds strongly connected components in O(V + E) single DFS pass, using a recursion stack and low-link values. Bipartite checking uses 2-coloring: attempt to color graph with 2 colors using BFS/DFS; impossible means not bipartite. Euler paths traverse each edge exactly once: existence requires (0 or 2) odd-degree vertices. Network flow concepts: max-flow equals min-cut; Ford-Fulkerson finds max flow by repeatedly finding augmenting paths. These advanced techniques solve problems in connectivity, matching, and resource optimization.
Core Concepts
Strongly Connected Components (SCC)
SCC are maximal vertex subsets where each vertex is reachable from every other vertex.
Key properties:
- Unique decomposition: Every directed graph has unique SCC decomposition
- DAG of SCCs: Condensing SCCs creates a DAG
- Applications: Social networks (finding communities), web crawlers (finding cycles)
Why important: Identifies "super-nodes" for condensed graph analysis.
Tarjan's Algorithm
Tarjan finds all SCCs in single DFS pass using recursion stack and low-link values.
Key concepts:
- Discovery index: When vertex is first visited
- Low-link value: Minimum discovery index reachable from vertex
- Root of SCC: Vertex where low[v] == disc[v]
Algorithm:
- DFS from unvisited vertices
- Track discovery time and low-link value
- Use stack to maintain current path
- When low[v] == disc[v], pop stack to find complete SCC
Time complexity: O(V + E) - single DFS pass.
Bipartite Graphs
Bipartite graph has vertices partitioned into two sets with edges only between sets, never within set.
Properties:
- 2-colorable: Can be colored with 2 colors such that adjacent vertices have different colors
- No odd cycles: All cycles have even length
- Applications: Job scheduling (workers and jobs), matching problems
Bipartite checking:
- Attempt 2-coloring using BFS/DFS
- For each uncolored vertex, assign color 0
- For each neighbor, assign opposite color
- If conflict (neighbor same color), not bipartite
Euler Paths and Circuits
Euler path traverses each edge exactly once. Euler circuit is Euler path forming cycle.
Existence conditions:
- Euler circuit exists: Graph connected, all vertices have even degree
- Euler path exists: Graph connected, exactly 0 or 2 vertices have odd degree
- Cannot exist: More than 2 odd-degree vertices
Finding Euler path (Hierholzer's algorithm):
- If using Euler circuit conditions, start at any vertex
- If using path conditions with 2 odd-degree vertices, start at one
- Follow edges greedily; when stuck, backtrack and find another starting point
- Result is Euler path (may need reversal)
Network Flow Concepts
Flow network has source, sink, and edges with capacities. Flow assigns value to each edge respecting capacity constraints and flow conservation.
Key concepts:
- Capacity: Maximum flow on edge
- Flow conservation: Inflow equals outflow (except source and sink)
- Residual capacity: Remaining capacity after current flow
- Augmenting path: Path from source to sink with available capacity
- Max-flow min-cut theorem: Maximum flow equals minimum cut capacity
Ford-Fulkerson approach:
- Start with zero flow
- While augmenting path from source to sink exists:
- Find path with positive residual capacity
- Add minimum residual capacity along path to flow
- Update residual capacities (forward and backward)
Key Algorithms
Tarjan's SCC Algorithm
Single-pass DFS finding all strongly connected components using discovery times and low-link values.
Bipartite Graph Checking
Two-coloring approach: attempt to color graph with 2 colors; success means bipartite, failure means not.
Euler Path Finding (Hierholzer's Algorithm)
Greedy path following with backtracking to ensure all edges traversed exactly once.
Ford-Fulkerson Maximum Flow
Iterative path augmentation in residual graph until no augmenting paths remain.
Practical Example
- Python
- Java
- TypeScript
from collections import defaultdict, deque
class AdvancedGraph:
def __init__(self):
self.graph = defaultdict(list)
def add_directed_edge(self, u, v):
self.graph[u].append(v)
# Tarjan's Algorithm for SCC
def tarjan_scc(self):
index_counter = [0]
stack = []
lowlinks = {}
index = {}
on_stack = defaultdict(bool)
sccs = []
def strongconnect(v):
index[v] = index_counter[0]
lowlinks[v] = index_counter[0]
index_counter[0] += 1
stack.append(v)
on_stack[v] = True
for w in self.graph[v]:
if w not in index:
strongconnect(w)
lowlinks[v] = min(lowlinks[v], lowlinks[w])
elif on_stack[w]:
lowlinks[v] = min(lowlinks[v], index[w])
if lowlinks[v] == index[v]:
scc = []
while True:
w = stack.pop()
on_stack[w] = False
scc.append(w)
if w == v:
break
sccs.append(scc)
for v in self.graph:
if v not in index:
strongconnect(v)
return sccs
# Bipartite check using 2-coloring
def is_bipartite(self):
color = {}
for start in self.graph:
if start in color:
continue
queue = deque([start])
color[start] = 0
while queue:
u = queue.popleft()
for v in self.graph[u]:
if v not in color:
color[v] = 1 - color[u]
queue.append(v)
elif color[v] == color[u]:
return False
return True
# Euler path detection
def has_euler_path(self):
in_degree = defaultdict(int)
out_degree = defaultdict(int)
for u in self.graph:
for v in self.graph[u]:
out_degree[u] += 1
in_degree[v] += 1
odd_diff = 0
for v in set(list(self.graph.keys()) + list(in_degree.keys())):
diff = out_degree[v] - in_degree[v]
if diff != 0:
if abs(diff) != 1:
return False
odd_diff += 1
return odd_diff == 0 or odd_diff == 2
# Hierholzer's algorithm for Euler path
def euler_path(self):
in_degree = defaultdict(int)
out_degree = defaultdict(int)
graph = defaultdict(list)
for u in self.graph:
for v in self.graph[u]:
graph[u].append(v)
out_degree[u] += 1
in_degree[v] += 1
start = None
for v in graph:
if out_degree[v] - in_degree[v] == 1:
start = v
break
if start is None:
start = next(iter(graph))
stack = [start]
path = []
while stack:
v = stack[-1]
if graph[v]:
u = graph[v].pop()
stack.append(u)
else:
path.append(stack.pop())
return path[::-1]
# Test
g = AdvancedGraph()
edges = [(0, 1), (1, 2), (2, 0), (1, 3), (3, 4), (4, 5), (5, 3)]
for u, v in edges:
g.add_directed_edge(u, v)
print("SCCs:", g.tarjan_scc())
print("Is bipartite:", g.is_bipartite())
print("Has Euler path:", g.has_euler_path())
import java.util.*;
public class AdvancedGraph {
private Map<Integer, List<Integer>> graph;
public AdvancedGraph() {
this.graph = new HashMap<>();
}
public void addDirectedEdge(int u, int v) {
graph.putIfAbsent(u, new ArrayList<>());
graph.putIfAbsent(v, new ArrayList<>());
graph.get(u).add(v);
}
// Tarjan's Algorithm for SCC
public List<List<Integer>> tarjanSCC() {
int[] index = {0};
Stack<Integer> stack = new Stack<>();
Map<Integer, Integer> lowlinks = new HashMap<>();
Map<Integer, Integer> indexMap = new HashMap<>();
Set<Integer> onStack = new HashSet<>();
List<List<Integer>> sccs = new ArrayList<>();
for (int v : graph.keySet()) {
if (!indexMap.containsKey(v)) {
strongconnect(v, index, stack, indexMap, lowlinks, onStack, sccs, graph);
}
}
return sccs;
}
private void strongconnect(int v, int[] index, Stack<Integer> stack,
Map<Integer, Integer> indexMap,
Map<Integer, Integer> lowlinks,
Set<Integer> onStack,
List<List<Integer>> sccs,
Map<Integer, List<Integer>> g) {
indexMap.put(v, index[0]);
lowlinks.put(v, index[0]);
index[0]++;
stack.push(v);
onStack.add(v);
for (int w : g.getOrDefault(v, new ArrayList<>())) {
if (!indexMap.containsKey(w)) {
strongconnect(w, index, stack, indexMap, lowlinks, onStack, sccs, g);
lowlinks.put(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
} else if (onStack.contains(w)) {
lowlinks.put(v, Math.min(lowlinks.get(v), indexMap.get(w)));
}
}
if (lowlinks.get(v).equals(indexMap.get(v))) {
List<Integer> scc = new ArrayList<>();
while (true) {
int w = stack.pop();
onStack.remove(w);
scc.add(w);
if (w == v) break;
}
sccs.add(scc);
}
}
// Bipartite check
public boolean isBipartite() {
Map<Integer, Integer> color = new HashMap<>();
for (int start : graph.keySet()) {
if (color.containsKey(start)) continue;
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
color.put(start, 0);
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : graph.getOrDefault(u, new ArrayList<>())) {
if (!color.containsKey(v)) {
color.put(v, 1 - color.get(u));
queue.offer(v);
} else if (color.get(v).equals(color.get(u))) {
return false;
}
}
}
}
return true;
}
// Euler path detection
public boolean hasEulerPath() {
Map<Integer, Integer> inDegree = new HashMap<>();
Map<Integer, Integer> outDegree = new HashMap<>();
for (int u : graph.keySet()) {
for (int v : graph.get(u)) {
outDegree.put(u, outDegree.getOrDefault(u, 0) + 1);
inDegree.put(v, inDegree.getOrDefault(v, 0) + 1);
}
}
int oddDiff = 0;
Set<Integer> allVertices = new HashSet<>(graph.keySet());
allVertices.addAll(inDegree.keySet());
for (int v : allVertices) {
int diff = outDegree.getOrDefault(v, 0) - inDegree.getOrDefault(v, 0);
if (diff != 0) {
if (Math.abs(diff) != 1) return false;
oddDiff++;
}
}
return oddDiff == 0 || oddDiff == 2;
}
}
class AdvancedGraph {
private graph: Map<number, number[]> = new Map();
addDirectedEdge(u: number, v: number): void {
if (!this.graph.has(u)) this.graph.set(u, []);
if (!this.graph.has(v)) this.graph.set(v, []);
this.graph.get(u)!.push(v);
}
// Tarjan's Algorithm for SCC
tarjanSCC(): number[][] {
let index = 0;
const stack: number[] = [];
const lowlinks = new Map<number, number>();
const indexMap = new Map<number, number>();
const onStack = new Set<number>();
const sccs: number[][] = [];
const strongconnect = (v: number) => {
indexMap.set(v, index);
lowlinks.set(v, index);
index++;
stack.push(v);
onStack.add(v);
for (const w of this.graph.get(v) || []) {
if (!indexMap.has(w)) {
strongconnect(w);
lowlinks.set(v, Math.min(lowlinks.get(v)!, lowlinks.get(w)!));
} else if (onStack.has(w)) {
lowlinks.set(v, Math.min(lowlinks.get(v)!, indexMap.get(w)!));
}
}
if (lowlinks.get(v)! === indexMap.get(v)!) {
const scc: number[] = [];
while (true) {
const w = stack.pop()!;
onStack.delete(w);
scc.push(w);
if (w === v) break;
}
sccs.push(scc);
}
};
for (const v of this.graph.keys()) {
if (!indexMap.has(v)) {
strongconnect(v);
}
}
return sccs;
}
// Bipartite check
isBipartite(): boolean {
const color = new Map<number, number>();
for (const start of this.graph.keys()) {
if (color.has(start)) continue;
const queue: number[] = [start];
color.set(start, 0);
while (queue.length > 0) {
const u = queue.shift()!;
for (const v of this.graph.get(u) || []) {
if (!color.has(v)) {
color.set(v, 1 - color.get(u)!);
queue.push(v);
} else if (color.get(v)! === color.get(u)!) {
return false;
}
}
}
}
return true;
}
// Euler path detection
hasEulerPath(): boolean {
const inDegree = new Map<number, number>();
const outDegree = new Map<number, number>();
for (const u of this.graph.keys()) {
for (const v of this.graph.get(u) || []) {
outDegree.set(u, (outDegree.get(u) || 0) + 1);
inDegree.set(v, (inDegree.get(v) || 0) + 1);
}
}
let oddDiff = 0;
const allVertices = new Set([...this.graph.keys(), ...inDegree.keys()]);
for (const v of allVertices) {
const diff = (outDegree.get(v) || 0) - (inDegree.get(v) || 0);
if (diff !== 0) {
if (Math.abs(diff) !== 1) return false;
oddDiff++;
}
}
return oddDiff === 0 || oddDiff === 2;
}
}
Common Pitfalls
Mixing SCC detection with regular connectivity: SCCs require directed graphs; undirected graphs don't have meaningful SCCs. Use connected components for undirected graphs.
Wrong degree counting for Euler paths: Count actual edges (not vertices). Multi-edges count multiple times. Self-loops count for both in and out degree.
Starting Euler path from wrong vertex: For path with 2 odd-degree vertices, must start from one of them, not arbitrary vertex. Wrong start makes path impossible.
Not building residual graph for flow: Flow algorithms need residual capacities and backward edges. Forgetting backward edges prevents finding optimal flow.
Confusing bipartite with 2-colorability: Not all 2-colorable graphs are valid problem instances; graph structure matters. Verify bipartite property separately.
Topological sorting not on DAG: Tarjan SCC assumes DAG structure; cyclic graphs need different handling. Verify acyclicity first for correct results.
Self-Check
- How does Tarjan's algorithm identify SCC root vertices? What is the significance of low-link values?
- Why must a bipartite graph have no odd-length cycles? Prove this property.
- For network flow, why is max-flow = min-cut true? What does this mean practically?
One Takeaway
Advanced graph algorithms solve complex structural problems. Tarjan's SCC algorithm efficiently decomposes directed graphs into strongly connected components in one DFS pass. Bipartite checking via 2-coloring identifies graph structure quickly. Euler paths traverse edges exactly once, useful for routing and circuit problems. Network flow concepts model resource allocation where max-flow equals minimum cut capacity, enabling optimal distribution solutions. Understanding these algorithms unlocks solutions to real-world problems in social networks, transportation, and telecommunications.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- Tarjan, R. E. (1972). "Depth-first search and linear graph algorithms." SIAM Journal on Computing, 1(2), 146-160.
- Ford, L. R., & Fulkerson, D. R. (1956). "Maximal flow through a network." Canadian Journal of Mathematics, 8(3), 399-404.