Strong Orientation (Robbins' Theorem) — Practice Tasks¶
All tasks must be solved in Go, Java, and Python. Each task ships with starter code or a reference solution. Validate against the evaluation criteria, and always verify your orientation with a strongly-connected check.
Beginner Tasks (5)¶
Task 1 — Decide if a strong orientation exists¶
Problem. Given a connected undirected graph, print YES if it admits a strong orientation and NO otherwise. By Robbins' Theorem, the answer is YES iff the graph is bridgeless. Count bridges with a low-link DFS.
Input / Output spec. - First line: n m (vertices, edges). - Next m lines: u v (an undirected edge). - Output: YES or NO.
Constraints. - 1 <= n <= 10^5, 0 <= m <= 2·10^5. - The graph is connected. - Time: O(n + m).
Hint. A tree edge {u, v} is a bridge iff low[v] > disc[u]. If any bridge is found, print NO.
Starter — Go.
package main
import (
"bufio"
"fmt"
"os"
)
type E struct{ to, id int }
func main() {
in := bufio.NewReader(os.Stdin)
var n, m int
fmt.Fscan(in, &n, &m)
adj := make([][]E, n)
for i := 0; i < m; i++ {
var u, v int
fmt.Fscan(in, &u, &v)
adj[u] = append(adj[u], E{v, i})
adj[v] = append(adj[v], E{u, i})
}
disc := make([]int, n)
low := make([]int, n)
for i := range disc {
disc[i] = -1
}
timer := 0
bridge := false
var dfs func(u, pe int)
dfs = func(u, pe int) {
// TODO: set disc/low, recurse on tree edges, update low on back edges,
// set bridge=true when low[child] > disc[u]
}
dfs(0, -1)
if bridge {
fmt.Println("NO")
} else {
fmt.Println("YES")
}
}
Starter — Java.
import java.util.*;
import java.io.*;
public class Task1 {
static List<int[]>[] adj;
static int[] disc, low;
static int timer = 0;
static boolean bridge = false;
static void dfs(int u, int pe) {
// TODO: low-link DFS; set bridge when low[child] > disc[u]
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken()), m = Integer.parseInt(st.nextToken());
adj = new List[n];
for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
for (int i = 0; i < m; i++) {
st = new StringTokenizer(br.readLine());
int u = Integer.parseInt(st.nextToken()), v = Integer.parseInt(st.nextToken());
adj[u].add(new int[]{v, i});
adj[v].add(new int[]{u, i});
}
disc = new int[n]; low = new int[n];
Arrays.fill(disc, -1);
dfs(0, -1);
System.out.println(bridge ? "NO" : "YES");
}
}
Starter — Python.
import sys
sys.setrecursionlimit(1_000_000)
def main():
data = sys.stdin.read().split()
idx = 0
n = int(data[idx]); m = int(data[idx + 1]); idx += 2
adj = [[] for _ in range(n)]
for i in range(m):
u = int(data[idx]); v = int(data[idx + 1]); idx += 2
adj[u].append((v, i))
adj[v].append((u, i))
disc = [-1] * n
low = [0] * n
timer = [0]
bridge = [False]
def dfs(u, pe):
# TODO: low-link DFS; bridge[0] = True when low[child] > disc[u]
pass
dfs(0, -1)
print("NO" if bridge[0] else "YES")
main()
Evaluation criteria. Correct on a cycle (YES), on a tree (NO), on a triangle-plus-pendant (NO), and runs in linear time on 10^5 vertices.
Task 2 — Output a concrete strong orientation¶
Problem. Given a bridgeless connected graph, output a direction u v for every edge so the result is strongly connected. Use tree-down / back-up.
Input / Output spec. - n m, then m edges u v. - Output m lines a b meaning the edge is oriented a -> b, in input order.
Constraints. n, m as in Task 1; the graph is guaranteed bridgeless.
Hint. Store each oriented edge in a dir[id] array; print in id order after the DFS.
Reference — Python.
import sys
sys.setrecursionlimit(1_000_000)
def orient(n, edges):
adj = [[] for _ in range(n)]
dir_ = [None] * len(edges)
for i, (u, v) in enumerate(edges):
adj[u].append((v, i))
adj[v].append((u, i))
disc = [-1] * n
timer = [0]
def dfs(u, pe):
disc[u] = timer[0]
timer[0] += 1
for v, eid in adj[u]:
if eid == pe:
continue
if disc[v] == -1:
dir_[eid] = (u, v) # tree: down
dfs(v, eid)
elif disc[v] < disc[u]:
if dir_[eid] is None:
dir_[eid] = (u, v) # back: up
dfs(0, -1)
return dir_
if __name__ == "__main__":
print(orient(4, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]))
Reference — Go.
package main
import "fmt"
func orient(n int, edges [][2]int) [][2]int {
type E struct{ to, id int }
adj := make([][]E, n)
dir := make([][2]int, len(edges))
for i := range dir {
dir[i] = [2]int{-1, -1}
}
for i, e := range edges {
adj[e[0]] = append(adj[e[0]], E{e[1], i})
adj[e[1]] = append(adj[e[1]], E{e[0], i})
}
disc := make([]int, n)
for i := range disc {
disc[i] = -1
}
timer := 0
var dfs func(u, pe int)
dfs = func(u, pe int) {
disc[u] = timer
timer++
for _, e := range adj[u] {
if e.id == pe {
continue
}
if disc[e.to] == -1 {
dir[e.id] = [2]int{u, e.to}
dfs(e.to, e.id)
} else if disc[e.to] < disc[u] {
if dir[e.id] == [2]int{-1, -1} {
dir[e.id] = [2]int{u, e.to}
}
}
}
}
dfs(0, -1)
return dir
}
func main() {
fmt.Println(orient(4, [][2]int{{0, 1}, {0, 2}, {1, 2}, {1, 3}, {2, 3}}))
}
Reference — Java.
import java.util.*;
public class Task2 {
static List<int[]>[] adj;
static int[] disc;
static int[][] dir;
static int timer = 0;
static void dfs(int u, int pe) {
disc[u] = timer++;
for (int[] e : adj[u]) {
int v = e[0], id = e[1];
if (id == pe) continue;
if (disc[v] == -1) { dir[id] = new int[]{u, v}; dfs(v, id); }
else if (disc[v] < disc[u] && dir[id][0] == -1) dir[id] = new int[]{u, v};
}
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
int n = 4;
int[][] edges = {{0,1},{0,2},{1,2},{1,3},{2,3}};
adj = new List[n];
for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
dir = new int[edges.length][];
for (int i = 0; i < edges.length; i++) {
adj[edges[i][0]].add(new int[]{edges[i][1], i});
adj[edges[i][1]].add(new int[]{edges[i][0], i});
dir[i] = new int[]{-1, -1};
}
disc = new int[n];
Arrays.fill(disc, -1);
dfs(0, -1);
for (int[] d : dir) System.out.println(d[0] + " " + d[1]);
}
}
Evaluation criteria. The printed orientation must pass an independent SCC check (Task 4).
Task 3 — Orient a cycle two ways¶
Problem. A simple cycle 0-1-2-...-(n-1)-0 has exactly two strong orientations: clockwise and counterclockwise. Print both.
Input / Output spec. Input n. Output two lines: the clockwise arc sequence and the counterclockwise one.
Constraints. 3 <= n <= 10^6.
Hint. No DFS needed; this is a direct construction. It builds intuition that strong orientation is not unique.
Reference — Python.
def cycle_orientations(n):
cw = [(i, (i + 1) % n) for i in range(n)]
ccw = [(i, (i - 1) % n) for i in range(n)]
return cw, ccw
if __name__ == "__main__":
cw, ccw = cycle_orientations(4)
print("CW :", cw)
print("CCW:", ccw)
Reference — Go.
package main
import "fmt"
func main() {
n := 4
fmt.Print("CW : ")
for i := 0; i < n; i++ {
fmt.Printf("%d->%d ", i, (i+1)%n)
}
fmt.Print("\nCCW: ")
for i := 0; i < n; i++ {
fmt.Printf("%d->%d ", i, (i+n-1)%n)
}
fmt.Println()
}
Reference — Java.
public class Task3 {
public static void main(String[] args) {
int n = 4;
StringBuilder cw = new StringBuilder("CW : "), ccw = new StringBuilder("CCW: ");
for (int i = 0; i < n; i++) {
cw.append(i).append("->").append((i + 1) % n).append(' ');
ccw.append(i).append("->").append((i + n - 1) % n).append(' ');
}
System.out.println(cw);
System.out.println(ccw);
}
}
Evaluation criteria. Both orientations are strongly connected; they are reverses of each other.
Task 4 — Strongly-connected verifier¶
Problem. Given a directed graph, return whether it is strongly connected via forward + reverse reachability from vertex 0.
Input / Output spec. n m, then m directed arcs u v. Output true/false.
Constraints. 1 <= n <= 10^5, 0 <= m <= 5·10^5.
Hint. Use an iterative stack to avoid recursion limits.
Reference — Python.
def reach(n, g, s):
seen = [False] * n
st = [s]
seen[s] = True
while st:
u = st.pop()
for v in g[u]:
if not seen[v]:
seen[v] = True
st.append(v)
return seen
def strongly_connected(n, arcs):
out = [[] for _ in range(n)]
inc = [[] for _ in range(n)]
for a, b in arcs:
out[a].append(b)
inc[b].append(a)
return all(reach(n, out, 0)) and all(reach(n, inc, 0))
if __name__ == "__main__":
print(strongly_connected(4, [(0, 1), (1, 2), (2, 3), (3, 0)])) # True
Reference — Go.
package main
import "fmt"
func reach(n int, g [][]int, s int) []bool {
seen := make([]bool, n)
st := []int{s}
seen[s] = true
for len(st) > 0 {
u := st[len(st)-1]
st = st[:len(st)-1]
for _, v := range g[u] {
if !seen[v] {
seen[v] = true
st = append(st, v)
}
}
}
return seen
}
func main() {
n := 4
arcs := [][2]int{{0, 1}, {1, 2}, {2, 3}, {3, 0}}
out := make([][]int, n)
in := make([][]int, n)
for _, a := range arcs {
out[a[0]] = append(out[a[0]], a[1])
in[a[1]] = append(in[a[1]], a[0])
}
f, r := reach(n, out, 0), reach(n, in, 0)
ok := true
for i := 0; i < n; i++ {
if !f[i] || !r[i] {
ok = false
}
}
fmt.Println(ok)
}
Reference — Java.
import java.util.*;
public class Task4 {
static boolean[] reach(int n, List<Integer>[] g, int s) {
boolean[] seen = new boolean[n];
Deque<Integer> st = new ArrayDeque<>();
st.push(s); seen[s] = true;
while (!st.isEmpty()) {
int u = st.pop();
for (int v : g[u]) if (!seen[v]) { seen[v] = true; st.push(v); }
}
return seen;
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
int n = 4;
int[][] arcs = {{0,1},{1,2},{2,3},{3,0}};
List<Integer>[] out = new List[n], in = new List[n];
for (int i = 0; i < n; i++) { out[i] = new ArrayList<>(); in[i] = new ArrayList<>(); }
for (int[] a : arcs) { out[a[0]].add(a[1]); in[a[1]].add(a[0]); }
boolean[] f = reach(n, out, 0), r = reach(n, in, 0);
boolean ok = true;
for (int i = 0; i < n; i++) if (!f[i] || !r[i]) ok = false;
System.out.println(ok);
}
}
Evaluation criteria. true for a directed cycle, false if any vertex is unreachable in either direction.
Task 5 — Detect why a graph is not orientable¶
Problem. Given a connected graph, list its bridges (as u v pairs). These are the edges blocking a strong orientation.
Input / Output spec. n m, then m edges. Output each bridge on its own line; if none, output NONE.
Constraints. 1 <= n <= 10^5, 0 <= m <= 2·10^5.
Hint. Same low-link DFS as Task 1, but record the bridge endpoints instead of a boolean.
Reference — Python.
import sys
sys.setrecursionlimit(1_000_000)
def bridges(n, edges):
adj = [[] for _ in range(n)]
for i, (u, v) in enumerate(edges):
adj[u].append((v, i))
adj[v].append((u, i))
disc = [-1] * n
low = [0] * n
timer = [0]
res = []
def dfs(u, pe):
disc[u] = low[u] = timer[0]
timer[0] += 1
for v, eid in adj[u]:
if eid == pe:
continue
if disc[v] == -1:
dfs(v, eid)
low[u] = min(low[u], low[v])
if low[v] > disc[u]:
res.append((u, v))
else:
low[u] = min(low[u], disc[v])
dfs(0, -1)
return res
if __name__ == "__main__":
b = bridges(4, [(0, 1), (1, 2), (2, 0), (2, 3)])
print("NONE" if not b else "\n".join(f"{u} {v}" for u, v in b))
Reference — Go / Java. Reuse the bridge-counting DFS from interview Challenge 3, appending (u, v) instead of incrementing a counter.
Evaluation criteria. Correct bridge set on triangle-plus-pendant ({2,3}), on a tree (all edges), and NONE on a cycle.
Intermediate Tasks (5)¶
Task 6 — Orient per 2-edge-connected component¶
Problem. Given a connected graph that may contain bridges, orient every non-bridge edge so each 2-edge-connected component is strongly connected; leave bridges undirected (print them as BRIDGE u v).
Constraints. n <= 10^5, m <= 2·10^5.
Hint. First low-link DFS marks bridges. Then run the orientation DFS but only orient non-bridge edges; the bridge edges keep (-1,-1) and are printed separately.
Reference — Python.
import sys
sys.setrecursionlimit(1_000_000)
def orient_components(n, edges):
adj = [[] for _ in range(n)]
for i, (u, v) in enumerate(edges):
adj[u].append((v, i))
adj[v].append((u, i))
disc = [-1] * n
low = [0] * n
is_bridge = [False] * len(edges)
dir_ = [None] * len(edges)
timer = [0]
def dfs(u, pe):
disc[u] = low[u] = timer[0]
timer[0] += 1
for v, eid in adj[u]:
if eid == pe:
continue
if disc[v] == -1:
if dir_[eid] is None:
dir_[eid] = (u, v) # tentative tree orientation
dfs(v, eid)
low[u] = min(low[u], low[v])
if low[v] > disc[u]:
is_bridge[eid] = True
dir_[eid] = None # bridges stay undirected
elif disc[v] < disc[u]:
if dir_[eid] is None:
dir_[eid] = (u, v)
dfs(0, -1)
return dir_, is_bridge
if __name__ == "__main__":
edges = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3)]
dir_, is_bridge = orient_components(6, edges)
for i, (u, v) in enumerate(edges):
if is_bridge[i]:
print(f"BRIDGE {u} {v}")
else:
a, b = dir_[i]
print(f"{a} -> {b}")
Evaluation criteria. Each component's oriented edges form a strongly connected subgraph; bridges are correctly identified.
Task 7 — Build the bridge tree¶
Problem. Contract each 2-edge-connected component to a node and output the bridge tree as an edge list of component ids.
Constraints. n <= 10^5, m <= 2·10^5.
Hint. Label components by DFS over non-bridge edges; each bridge becomes a tree edge between two component ids.
Reference — Go.
package main
import "fmt"
func bridgeTree(n int, edges [][2]int) [][2]int {
type E struct{ to, id int }
adj := make([][]E, n)
for i, e := range edges {
adj[e[0]] = append(adj[e[0]], E{e[1], i})
adj[e[1]] = append(adj[e[1]], E{e[0], i})
}
disc := make([]int, n)
low := make([]int, n)
for i := range disc {
disc[i] = -1
}
isBridge := make([]bool, len(edges))
timer := 0
var dfs func(u, pe int)
dfs = func(u, pe int) {
disc[u] = timer
low[u] = timer
timer++
for _, e := range adj[u] {
if e.id == pe {
continue
}
if disc[e.to] == -1 {
dfs(e.to, e.id)
if low[e.to] < low[u] {
low[u] = low[e.to]
}
if low[e.to] > disc[u] {
isBridge[e.id] = true
}
} else if disc[e.to] < low[u] {
low[u] = disc[e.to]
}
}
}
dfs(0, -1)
comp := make([]int, n)
for i := range comp {
comp[i] = -1
}
c := 0
for s := 0; s < n; s++ {
if comp[s] != -1 {
continue
}
st := []int{s}
comp[s] = c
for len(st) > 0 {
u := st[len(st)-1]
st = st[:len(st)-1]
for _, e := range adj[u] {
if !isBridge[e.id] && comp[e.to] == -1 {
comp[e.to] = c
st = append(st, e.to)
}
}
}
c++
}
var tree [][2]int
for i, e := range edges {
if isBridge[i] {
tree = append(tree, [2]int{comp[e[0]], comp[e[1]]})
}
}
return tree
}
func main() {
edges := [][2]int{{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 5}, {5, 3}}
fmt.Println(bridgeTree(6, edges)) // one edge between the two triangle components
}
Evaluation criteria. The output is a tree (acyclic) with one node per 2ECC and one edge per bridge.
Task 8 — Minimum edges to make orientable¶
Problem. Compute ⌈L/2⌉ where L is the number of bridge-tree leaves; 0 if already 2-edge-connected.
Constraints. n <= 10^5, m <= 2·10^5.
Hint. Reuse Task 7 to get the bridge tree, count degree-1 nodes. See interview Challenge 4 for the full three-language solution.
Reference — Python. (See interview Challenge 4 min_edges_to_orient — reuse verbatim.)
Evaluation criteria. 0 for a cycle, 1 for two blocks joined by one bridge, ⌈L/2⌉ for a star-of-blocks.
Task 9 — Orient + verify pipeline¶
Problem. Combine: read a graph, decide orientability, orient if possible, and assert the result is strongly connected. Exit with an error if the assertion fails.
Constraints. n <= 10^5, m <= 2·10^5.
Hint. Chain Task 2 (orient) and Task 4 (verify). The verify must never fail on a bridgeless input — that is the contract.
Reference — Python.
import sys
sys.setrecursionlimit(1_000_000)
# assume orient() from Task 2 and strongly_connected() from Task 4 are imported
def pipeline(n, edges):
# bridge check first
from_task5 = bridges(n, edges) # Task 5
if from_task5:
return "NO", None
dir_ = orient(n, edges) # Task 2
arcs = list(dir_)
assert strongly_connected(n, arcs), "verification failed!"
return "YES", arcs
Evaluation criteria. On any bridgeless graph the assertion holds; on a bridged graph the pipeline reports NO without attempting orientation.
Task 10 — Iterative (stack-based) orientation for deep graphs¶
Problem. Re-implement the orientation DFS using an explicit stack so it survives a path graph of 10^6 vertices without recursion overflow.
Constraints. n <= 10^6, m <= 2·10^6. The graph is bridgeless.
Hint. Emulate the recursion with an explicit stack of (vertex, parentEdge, iterator_position). Compute low on the unwind (post-order) step.
Reference — Python.
def orient_iter(n, edges):
adj = [[] for _ in range(n)]
dir_ = [None] * len(edges)
for i, (u, v) in enumerate(edges):
adj[u].append((v, i))
adj[v].append((u, i))
disc = [-1] * n
timer = 0
for src in range(n):
if disc[src] != -1:
continue
stack = [(src, -1, 0)]
while stack:
u, pe, pi = stack.pop()
if pi == 0:
disc[u] = timer
timer += 1
advanced = False
while pi < len(adj[u]):
v, eid = adj[u][pi]
pi += 1
if eid == pe:
continue
if disc[v] == -1:
dir_[eid] = (u, v) # tree: down
stack.append((u, pe, pi)) # resume later
stack.append((v, eid, 0)) # descend
advanced = True
break
elif disc[v] < disc[u]:
if dir_[eid] is None:
dir_[eid] = (u, v) # back: up
if not advanced:
continue
return dir_
if __name__ == "__main__":
print(orient_iter(4, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]))
Evaluation criteria. No stack overflow on 10^6 path vertices (plus extra cycle edges to keep it bridgeless); output passes Task 4 verification.
Advanced Tasks (5)¶
Task 11 — Mixed-graph feasibility (small inputs)¶
Problem. Given a mixed graph (some arcs fixed, some edges undirected), decide whether a strong orientation of the undirected edges exists. For E <= 18 undirected edges, brute-force all 2^E orientations and test strong connectivity; this validates the Boesch–Tindell characterization on small cases.
Constraints. n <= 30, undirected edges <= 18, arcs <= 100.
Hint. Enumerate bitmasks over undirected edges; for each, build the digraph (fixed arcs + chosen directions) and run the Task 4 verifier. Report YES if any mask is strongly connected.
Reference — Python.
from itertools import product
def mixed_feasible(n, arcs, und_edges, sc_check):
e = len(und_edges)
for mask in range(1 << e):
these = list(arcs)
for i, (u, v) in enumerate(und_edges):
these.append((u, v) if (mask >> i) & 1 else (v, u))
if sc_check(n, these):
return True
return False
Evaluation criteria. Matches the polynomial Boesch–Tindell answer on all small test cases; NO exactly when no orientation works.
Task 12 — Count distinct strong orientations of small graphs¶
Problem. For E <= 20, count how many of the 2^E orientations are strongly connected. (This is #P-hard in general — brute force is fine for small E and builds intuition that orientations are far from unique.)
Constraints. n <= 12, E <= 20.
Hint. Enumerate all orientations, count those passing the Task 4 verifier. A cycle of length n should yield exactly 2.
Reference — Go.
package main
import "fmt"
func reach(n int, g [][]int, s int) []bool {
seen := make([]bool, n)
st := []int{s}
seen[s] = true
for len(st) > 0 {
u := st[len(st)-1]
st = st[:len(st)-1]
for _, v := range g[u] {
if !seen[v] {
seen[v] = true
st = append(st, v)
}
}
}
return seen
}
func sc(n int, arcs [][2]int) bool {
out := make([][]int, n)
in := make([][]int, n)
for _, a := range arcs {
out[a[0]] = append(out[a[0]], a[1])
in[a[1]] = append(in[a[1]], a[0])
}
f, r := reach(n, out, 0), reach(n, in, 0)
for i := 0; i < n; i++ {
if !f[i] || !r[i] {
return false
}
}
return true
}
func count(n int, edges [][2]int) int {
e := len(edges)
total := 0
for mask := 0; mask < (1 << e); mask++ {
arcs := make([][2]int, e)
for i, ed := range edges {
if mask>>i&1 == 1 {
arcs[i] = [2]int{ed[0], ed[1]}
} else {
arcs[i] = [2]int{ed[1], ed[0]}
}
}
if sc(n, arcs) {
total++
}
}
return total
}
func main() {
// 4-cycle -> exactly 2 strong orientations
fmt.Println(count(4, [][2]int{{0, 1}, {1, 2}, {2, 3}, {3, 0}}))
}
Evaluation criteria. Returns 2 for a cycle; matches a hand count on K4 (which has many).
Task 13 — Random bridgeless graph generator + invariant test¶
Problem. Generate random bridgeless graphs (start from a Hamiltonian cycle, add random extra edges — a cycle guarantees bridgelessness), orient them, and assert the orientation is always strongly connected. This is a property-based test of the whole algorithm.
Constraints. Run 1000 random trials with n up to 500.
Hint. Cycle base ⇒ no bridges ⇒ Robbins guarantees YES. Any verification failure is a bug in your orientation.
Reference — Python.
import random
# reuse orient() (Task 2) and strongly_connected() (Task 4)
def random_bridgeless(n, extra):
edges = [(i, (i + 1) % n) for i in range(n)] # cycle: bridgeless
for _ in range(extra):
a, b = random.randrange(n), random.randrange(n)
if a != b:
edges.append((a, b))
return edges
def fuzz(trials=1000):
for _ in range(trials):
n = random.randint(3, 500)
edges = random_bridgeless(n, random.randint(0, n))
dir_ = orient(n, edges)
assert strongly_connected(n, list(dir_)), "FAILED"
print("all trials passed")
Evaluation criteria. All trials pass; introducing a deliberate bug (e.g. orienting back edges down) makes the assertion fire.
Task 14 — Incremental orientability under edge insertions¶
Problem. Process a stream of edge insertions on an initially-disconnected graph. After each insertion, report whether the graph (on the vertices seen so far) is connected and bridgeless. Use a union-find for connectivity and recompute bridges lazily, or maintain a 2-edge-connectivity DSU.
Constraints. n <= 10^5, insertions <= 2·10^5.
Hint. Adding an edge can only remove bridges, never create one. A union-find over 2-edge-connected components (small-to-large path compression, sibling 21-small-to-large-merging) maintains the answer in near-O(α) amortized. A simpler O(m) recompute per query also passes for moderate sizes.
Reference — Python (simple recompute version).
def online_orientable(n, insertions):
edges = []
results = []
for u, v in insertions:
edges.append((u, v))
# connected over touched vertices AND bridgeless?
results.append(is_connected_and_bridgeless(n, edges))
return results
# is_connected_and_bridgeless: run the Task 1/Task 5 DFS over current edges.
Evaluation criteria. The first time the graph becomes connected + bridgeless, the answer flips to YES and stays correct as more edges arrive.
Task 15 — Orientation under a forbidden-direction constraint¶
Problem. Some edges may not be oriented in a given direction (a fixed list of forbidden (u, v) arcs). Decide if a strong orientation respecting all forbidden constraints exists, and produce one. For small graphs, model each constrained edge as semi-fixed and brute-force; for the general case, reduce to the mixed-graph flow formulation (Boesch–Tindell, see professional.md).
Constraints. Brute force for undirected-with-constraints <= 18; describe the flow reduction for larger.
Hint. A forbidden direction on an undirected edge effectively fixes it to the other direction — turning it into a pre-directed (mixed-graph) edge. Then it is exactly the mixed-graph problem.
Reference — Python (brute force for small inputs).
from itertools import product
# reuse strongly_connected() from Task 4
def constrained_orient(n, edges, forbidden):
# forbidden: set of (u, v) arcs that may NOT be used
e = len(edges)
for mask in range(1 << e):
arcs = []
ok = True
for i, (u, v) in enumerate(edges):
a = (u, v) if (mask >> i) & 1 else (v, u)
if a in forbidden:
ok = False
break
arcs.append(a)
if ok and strongly_connected(n, arcs):
return arcs
return None
Evaluation criteria. Returns a valid orientation avoiding all forbidden arcs when one exists, None otherwise; agrees with the flow reduction on test cases.
Benchmark Task¶
Benchmark — Orientation throughput and scaling¶
Problem. Measure the orientation pipeline (bridge check + orient + verify) on graphs from 10^3 to 10^6 edges and confirm linear scaling. Use a cycle base plus random chords so every instance is guaranteed bridgeless.
What to measure. 1. Time to detect bridges (should be 0 on a cycle base). 2. Time to orient. 3. Time to verify (two reachability passes). 4. Throughput in edges/second; confirm it stays roughly flat (linear total time).
Reference — Python.
import random, time
import sys
sys.setrecursionlimit(2_000_000)
# reuse orient() (Task 2), strongly_connected() (Task 4), bridges() (Task 5)
def make(n, extra):
edges = [(i, (i + 1) % n) for i in range(n)] # bridgeless cycle
for _ in range(extra):
a, b = random.randrange(n), random.randrange(n)
if a != b:
edges.append((a, b))
return edges
def bench():
for n in [1_000, 10_000, 100_000, 500_000]:
edges = make(n, n) # ~2n edges
t0 = time.perf_counter()
b = bridges(n, edges)
t1 = time.perf_counter()
dir_ = orient(n, edges)
t2 = time.perf_counter()
ok = strongly_connected(n, list(dir_))
t3 = time.perf_counter()
m = len(edges)
print(f"n={n:>7} m={m:>8} bridges={len(b)} "
f"bridge={t1-t0:.3f}s orient={t2-t1:.3f}s verify={t3-t2:.3f}s "
f"sc={ok} throughput={m/(t3-t0):,.0f} edges/s")
if __name__ == "__main__":
bench()
Reference — Go (timing harness sketch).
package main
import (
"fmt"
"math/rand"
"time"
)
func makeGraph(n, extra int) [][2]int {
edges := make([][2]int, 0, n+extra)
for i := 0; i < n; i++ {
edges = append(edges, [2]int{i, (i + 1) % n})
}
for i := 0; i < extra; i++ {
a, b := rand.Intn(n), rand.Intn(n)
if a != b {
edges = append(edges, [2]int{a, b})
}
}
return edges
}
func main() {
for _, n := range []int{1000, 10000, 100000, 500000} {
edges := makeGraph(n, n)
start := time.Now()
_ = orient(n, edges) // from Task 2
fmt.Printf("n=%d m=%d orient=%v\n", n, len(edges), time.Since(start))
}
}
Expected results.
| n | m (~2n) | bridges | orient time | verify time | scaling |
|---|---|---|---|---|---|
| 10³ | ~2·10³ | 0 | ~1 ms | ~1 ms | baseline |
| 10⁴ | ~2·10⁴ | 0 | ~10 ms | ~10 ms | ~10× |
| 10⁵ | ~2·10⁵ | 0 | ~110 ms | ~100 ms | ~10× |
| 5·10⁵ | ~10⁶ | 0 | ~600 ms | ~550 ms | ~5× |
Total time grows linearly with m; throughput (edges/second) stays roughly constant, confirming O(V + E). Go/Java are ~10–30× faster than Python for the same inputs. Bridges are always 0 because the cycle base guarantees bridgelessness — a clean invariant that makes the benchmark self-checking.
Evaluation criteria. Linear total time, flat throughput, zero bridges on the cycle base, and every orientation passing the strongly-connected verifier.