Graphs

18 problems from Rosalind — Algorithmic Heights. Press Run on any block to execute it in your browser.

DEG — Degree Array

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: DEG — Degree Array
# https://rosalind.info/problems/deg/
#
# Given: A simple graph with n vertices in the edge list format.
# Return: An array D[1..n] where D[i] is the degree of vertex i.

let n = 6
let edge_list = [[1, 2], [2, 3], [6, 3], [5, 6], [2, 5], [2, 4], [4, 1]]

# Each edge contributes one to both of its endpoints, so a single pass over the
# edge list is enough — the adjacency structure never has to be built.
let vertex_degree = []
for _ in range(0, n) {
    vertex_degree = push(vertex_degree, 0)
}
for edge in edge_list {
    let u = edge[0] - 1
    let v = edge[1] - 1
    vertex_degree[u] = vertex_degree[u] + 1
    vertex_degree[v] = vertex_degree[v] + 1
}

let result = vertex_degree |> map(|d| str(d)) |> join(" ")

println("Result:   " + result)
println("Expected: 2 4 2 2 2 2")

fn test_deg_degree_array() {
    assert result == "2 4 2 2 2 2", "DEG: got " + result
}

DDEG — Double-Degree Array

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: DDEG — Double-Degree Array
# https://rosalind.info/problems/ddeg/
#
# Given: A simple graph with n vertices in the edge list format.
# Return: An array D[1..n] where D[i] is the sum of the degrees of i's neighbours.

let n = 5
let edge_list = [[1, 2], [2, 3], [4, 3], [2, 4]]

# Two passes rather than one: the neighbour sum for a vertex needs every degree
# to be final first, so the degrees are counted before anything is summed.
let vertex_degree = []
for _ in range(0, n) {
    vertex_degree = push(vertex_degree, 0)
}
for edge in edge_list {
    vertex_degree[edge[0] - 1] = vertex_degree[edge[0] - 1] + 1
    vertex_degree[edge[1] - 1] = vertex_degree[edge[1] - 1] + 1
}

let neighbour_sum = []
for _ in range(0, n) {
    neighbour_sum = push(neighbour_sum, 0)
}
for edge in edge_list {
    let u = edge[0] - 1
    let v = edge[1] - 1
    neighbour_sum[u] = neighbour_sum[u] + vertex_degree[v]
    neighbour_sum[v] = neighbour_sum[v] + vertex_degree[u]
}

let result = neighbour_sum |> map(|d| str(d)) |> join(" ")

println("Result:   " + result)
println("Expected: 3 5 5 5 0")

fn test_ddeg_neighbour_degree_sums() {
    assert result == "3 5 5 5 0", "DDEG: got " + result
}

BFS — Breadth-First Search

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: BFS — Breadth-First Search
# https://rosalind.info/problems/bfs/
#
# Given: A simple directed graph with n vertices in the edge list format.
# Return: An array D[1..n] where D[i] is the length of a shortest path from
# vertex 1 to vertex i, and -1 where i is unreachable.

let n = 6
let edge_list = [[4, 6], [6, 5], [4, 3], [3, 5], [2, 1], [1, 4]]

# Directed: each edge is added once, so 2 -> 1 does not make 2 reachable.
let adjacent = []
for _ in range(0, n) {
    adjacent = push(adjacent, [])
}
for edge in edge_list {
    let u = edge[0] - 1
    adjacent[u] = push(adjacent[u], edge[1] - 1)
}

let distance = []
for _ in range(0, n) {
    distance = push(distance, -1)
}
distance[0] = 0

# The queue is a list plus a read cursor. Breadth-first order is what makes the
# first arrival at a vertex the shortest one, so a vertex is only ever assigned
# a distance once — the -1 test doubles as the visited check.
let queue = [0]
let queue_head = 0
while queue_head < len(queue) {
    let current = queue[queue_head]
    queue_head = queue_head + 1
    for next in adjacent[current] {
        if distance[next] == -1 {
            distance[next] = distance[current] + 1
            queue = push(queue, next)
        }
    }
}

let result = distance |> map(|d| str(d)) |> join(" ")

println("Result:   " + result)
println("Expected: 0 -1 2 1 3 2")

fn test_bfs_shortest_path_lengths() {
    assert result == "0 -1 2 1 3 2", "BFS: got " + result
}

CC — Connected Components

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: CC — Connected Components
# https://rosalind.info/problems/cc/
#
# Given: A simple graph with n vertices in the edge list format.
# Return: The number of connected components in the graph.

let n = 12
let edge_list = [
    [1, 2], [1, 5], [5, 9], [5, 10], [9, 10],
    [3, 4], [3, 7], [3, 8], [4, 8], [7, 11],
    [8, 11], [11, 12], [8, 12],
]

# Undirected, so every edge goes in both directions. Vertex 6 appears in no
# edge and is still a component of its own, which is why the sweep below starts
# from every vertex rather than from the endpoints of the edge list.
let adjacent = []
for _ in range(0, n) {
    adjacent = push(adjacent, [])
}
for edge in edge_list {
    let u = edge[0] - 1
    let v = edge[1] - 1
    adjacent[u] = push(adjacent[u], v)
    adjacent[v] = push(adjacent[v], u)
}

let seen = []
for _ in range(0, n) {
    seen = push(seen, false)
}

let components = 0
for start in range(0, n) {
    if seen[start] == false {
        components = components + 1
        # Flood the whole component from this vertex before moving on.
        let stack = [start]
        seen[start] = true
        while len(stack) > 0 {
            let current = stack[len(stack) - 1]
            stack = slice(stack, 0, len(stack) - 1)
            for next in adjacent[current] {
                if seen[next] == false {
                    seen[next] = true
                    stack = push(stack, next)
                }
            }
        }
    }
}

println("Result:   " + str(components))
println("Expected: 3")

fn test_cc_component_count() {
    assert components == 3, "CC: got " + str(components)
}

BIP — Testing Bipartiteness

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: BIP — Testing Bipartiteness
# https://rosalind.info/problems/bip/
#
# Given: A positive integer k and k simple graphs in the edge list format.
# Return: For each graph, 1 if it is bipartite and -1 otherwise.

let graphs = [
    { vertices: 3, edges: [[1, 2], [3, 2], [3, 1]] },
    { vertices: 4, edges: [[1, 4], [3, 1], [1, 2]] },
]

# Two-colour by breadth-first search. A graph is bipartite exactly when this
# never has to give a vertex the colour its neighbour already has, so the odd
# cycle in the first graph — the triangle — is what forces the -1.
fn is_bipartite(graph) {
    let n = graph.vertices
    let adjacent = []
    for _ in range(0, n) {
        adjacent = push(adjacent, [])
    }
    for edge in graph.edges {
        let u = edge[0] - 1
        let v = edge[1] - 1
        adjacent[u] = push(adjacent[u], v)
        adjacent[v] = push(adjacent[v], u)
    }

    let colour = []
    for _ in range(0, n) {
        colour = push(colour, -1)
    }

    let consistent = true
    # The graph need not be connected, so every uncoloured vertex starts a sweep.
    for start in range(0, n) {
        if colour[start] == -1 {
            colour[start] = 0
            let queue = [start]
            let head = 0
            while head < len(queue) {
                let current = queue[head]
                head = head + 1
                for next in adjacent[current] {
                    if colour[next] == -1 {
                        colour[next] = 1 - colour[current]
                        queue = push(queue, next)
                    } else {
                        if colour[next] == colour[current] {
                            consistent = false
                        }
                    }
                }
            }
        }
    }
    if consistent then 1 else -1
}

let result = graphs |> map(|g| str(is_bipartite(g))) |> join(" ")

println("Result:   " + result)
println("Expected: -1 1")

fn test_bip_detects_odd_cycles() {
    assert result == "-1 1", "BIP: got " + result
}

DAG — Testing Acyclicity

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: DAG — Testing Acyclicity
# https://rosalind.info/problems/dag/
#
# Given: A positive integer k and k simple directed graphs in the edge list format.
# Return: For each graph, 1 if the graph is acyclic and -1 otherwise.

let graphs = [
    { vertices: 2, edges: [[1, 2]] },
    { vertices: 4, edges: [[4, 1], [1, 2], [2, 3], [3, 1]] },
    { vertices: 4, edges: [[4, 3], [3, 2], [2, 1]] },
]

# Kahn's algorithm, used as a test rather than to produce an order: repeatedly
# remove a vertex with no remaining incoming edge. Anything left over lies on a
# cycle, because every vertex in it keeps an incoming edge from the vertex
# before it. The second graph leaves 1, 2 and 3 behind.
fn is_acyclic(graph) {
    let n = graph.vertices
    let adjacent = []
    let indegree = []
    for _ in range(0, n) {
        adjacent = push(adjacent, [])
        indegree = push(indegree, 0)
    }
    for edge in graph.edges {
        let u = edge[0] - 1
        let v = edge[1] - 1
        adjacent[u] = push(adjacent[u], v)
        indegree[v] = indegree[v] + 1
    }

    let queue = []
    for v in range(0, n) {
        if indegree[v] == 0 {
            queue = push(queue, v)
        }
    }

    let removed = 0
    let head = 0
    while head < len(queue) {
        let current = queue[head]
        head = head + 1
        removed = removed + 1
        for next in adjacent[current] {
            indegree[next] = indegree[next] - 1
            if indegree[next] == 0 {
                queue = push(queue, next)
            }
        }
    }

    if removed == n then 1 else -1
}

let result = graphs |> map(|g| str(is_acyclic(g))) |> join(" ")

println("Result:   " + result)
println("Expected: 1 -1 1")

fn test_dag_detects_cycles() {
    assert result == "1 -1 1", "DAG: got " + result
}

DIJ — Dijkstra's Algorithm

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: DIJ — Dijkstra's Algorithm
# https://rosalind.info/problems/dij/
#
# Given: A simple directed graph with positive edge weights and n vertices in
# the edge list format.
# Return: An array D[1..n] of shortest path lengths from vertex 1, with -1 where
# a vertex is unreachable.

let n = 6
let edge_list = [
    [3, 4, 4], [1, 2, 4], [1, 3, 2], [2, 3, 3], [6, 3, 2],
    [3, 5, 5], [5, 4, 1], [3, 2, 1], [2, 4, 2], [2, 5, 3],
]

let adjacent = []
for _ in range(0, n) {
    adjacent = push(adjacent, [])
}
for edge in edge_list {
    let u = edge[0] - 1
    adjacent[u] = push(adjacent[u], [edge[1] - 1, edge[2]])
}

# Scanning for the nearest unsettled vertex rather than using a priority queue:
# quadratic, and clearer. Positive weights are what make the greedy choice safe
# — once a vertex is settled no later path can improve it.
let infinity = 1000000000
let distance = []
let settled = []
for _ in range(0, n) {
    distance = push(distance, infinity)
    settled = push(settled, false)
}
distance[0] = 0

let step = 0
while step < n {
    let nearest = -1
    for v in range(0, n) {
        if settled[v] == false and distance[v] < infinity {
            if nearest == -1 or distance[v] < distance[nearest] {
                nearest = v
            }
        }
    }
    if nearest == -1 {
        step = n
    } else {
        settled[nearest] = true
        for link in adjacent[nearest] {
            let next = link[0]
            let weight = link[1]
            if distance[nearest] + weight < distance[next] {
                distance[next] = distance[nearest] + weight
            }
        }
        step = step + 1
    }
}

let result = distance |> map(|d| if d == infinity then "-1" else str(d)) |> join(" ")

println("Result:   " + result)
println("Expected: 0 3 2 5 6 -1")

fn test_dij_shortest_paths() {
    assert result == "0 3 2 5 6 -1", "DIJ: got " + result
}

SQ — Square in a Graph

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: SQ — Square in a Graph
# https://rosalind.info/problems/sq/
#
# Given: A positive integer k and k simple undirected graphs in the edge list format.
# Return: For each graph, 1 if it contains a simple cycle of length 4, and -1 otherwise.

let graphs = [
    { vertices: 4, edges: [[3, 4], [4, 2], [3, 2], [3, 1], [1, 2]] },
    { vertices: 4, edges: [[1, 2], [3, 4], [2, 4], [4, 1]] },
]

# A four-cycle is two vertices joined by two different paths of length two, so
# the question is whether any pair of vertices has two neighbours in common.
# Searching for the cycle directly would mean trying orderings; counting shared
# neighbours needs one pass over the pairs.
fn has_square(graph) {
    let n = graph.vertices
    let neighbours = []
    for _ in range(0, n) {
        neighbours = push(neighbours, [])
    }
    for edge in graph.edges {
        let u = edge[0] - 1
        let v = edge[1] - 1
        neighbours[u] = push(neighbours[u], v)
        neighbours[v] = push(neighbours[v], u)
    }

    let found = false
    let u = 0
    while u < n and found == false {
        let v = u + 1
        while v < n and found == false {
            let shared = 0
            for candidate in neighbours[u] {
                if contains(neighbours[v], candidate) {
                    shared = shared + 1
                }
            }
            if shared >= 2 {
                found = true
            }
            v = v + 1
        }
        u = u + 1
    }
    if found then 1 else -1
}

let result = graphs |> map(|g| str(has_square(g))) |> join(" ")

println("Result:   " + result)
println("Expected: 1 -1")

fn test_sq_finds_four_cycles() {
    assert result == "1 -1", "SQ: got " + result
}

BF — Bellman-Ford Algorithm

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: BF — Bellman-Ford Algorithm
# https://rosalind.info/problems/bf/
#
# Given: A simple directed graph with integer edge weights (possibly negative)
# and n vertices in the edge list format.
# Return: An array D[1..n] of shortest path lengths from vertex 1, with "x"
# where a vertex is unreachable.

let n = 9
let edge_list = [
    [1, 2, 10], [3, 2, 1], [3, 4, 1], [4, 5, 3], [5, 6, -1],
    [7, 6, -1], [8, 7, 1], [1, 8, 8], [7, 2, -4], [2, 6, 2],
    [6, 3, -2], [9, 5, -10], [9, 4, 7],
]

# Dijkstra cannot be used here: a negative edge can improve a vertex after it
# has been settled. Bellman-Ford instead relaxes every edge n-1 times, which is
# enough because a shortest path visits at most n vertices.
let infinity = 1000000000
let distance = []
for _ in range(0, n) {
    distance = push(distance, infinity)
}
distance[0] = 0

let sweep = 0
while sweep < n - 1 {
    let changed = false
    for edge in edge_list {
        let u = edge[0] - 1
        let v = edge[1] - 1
        let weight = edge[2]
        if distance[u] < infinity and distance[u] + weight < distance[v] {
            distance[v] = distance[u] + weight
            changed = true
        }
    }
    # Nothing improved, so nothing will: the remaining rounds cannot change it.
    if changed == false then sweep = n
    sweep = sweep + 1
}

let result = distance |> map(|d| if d == infinity then "x" else str(d)) |> join(" ")

println("Result:   " + result)
println("Expected: 0 5 5 6 9 7 9 8 x")

fn test_bf_handles_negative_weights() {
    assert result == "0 5 5 6 9 7 9 8 x", "BF: got " + result
}

CTE — Shortest Cycle Through a Given Edge

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: CTE — Shortest Cycle Through a Given Edge
# https://rosalind.info/problems/cte/
#
# Given: A positive integer k and k simple directed graphs with positive edge
# weights, each with a first specified edge.
# Return: For each graph, the length of a shortest cycle through the first edge,
# or -1 if there is none.

let graphs = [
    { vertices: 4, edges: [[2, 4, 2], [3, 2, 1], [1, 4, 3], [2, 1, 10], [1, 3, 4]] },
    { vertices: 4, edges: [[3, 2, 1], [2, 4, 2], [4, 1, 3], [2, 1, 10], [1, 3, 4]] },
]

# A cycle through the edge u -> v is that edge plus a shortest path from v back
# to u, so the whole problem reduces to one Dijkstra run from v. Weights are
# positive here, which is what lets Dijkstra be used rather than Bellman-Ford.
let infinity = 1000000000

fn shortest_path(graph, source, target) {
    let n = graph.vertices
    let adjacent = []
    for _ in range(0, n) {
        adjacent = push(adjacent, [])
    }
    for edge in graph.edges {
        adjacent[edge[0] - 1] = push(adjacent[edge[0] - 1], [edge[1] - 1, edge[2]])
    }

    let distance = []
    let settled = []
    for _ in range(0, n) {
        distance = push(distance, infinity)
        settled = push(settled, false)
    }
    distance[source] = 0

    let remaining = n
    while remaining > 0 {
        let nearest = -1
        for v in range(0, n) {
            if settled[v] == false and distance[v] < infinity {
                if nearest == -1 or distance[v] < distance[nearest] {
                    nearest = v
                }
            }
        }
        if nearest == -1 {
            remaining = 0
        } else {
            settled[nearest] = true
            for link in adjacent[nearest] {
                if distance[nearest] + link[1] < distance[link[0]] {
                    distance[link[0]] = distance[nearest] + link[1]
                }
            }
            remaining = remaining - 1
        }
    }
    distance[target]
}

fn shortest_cycle(graph) {
    let first = graph.edges[0]
    let back = shortest_path(graph, first[1] - 1, first[0] - 1)
    if back == infinity then -1 else back + first[2]
}

let result = graphs |> map(|g| str(shortest_cycle(g))) |> join(" ")

println("Result:   " + result)
println("Expected: -1 10")

fn test_cte_shortest_cycle_through_an_edge() {
    assert result == "-1 10", "CTE: got " + result
}

TS — Topological Sorting

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Any order with every edge pointing forwards is accepted, so that property is asserted rather than the sample text.

# Rosalind: TS — Topological Sorting
# https://rosalind.info/problems/ts/
#
# Given: A simple directed acyclic graph with n vertices in the edge list format.
# Return: A topological sorting of the graph.

let n = 4
let edge_list = [[1, 2], [3, 1], [3, 2], [4, 3], [4, 2]]

# Kahn's algorithm, the same sweep DAG uses to test for cycles — here the order
# in which vertices come off the queue is the answer rather than a by-product.
let adjacent = []
let indegree = []
for _ in range(0, n) {
    adjacent = push(adjacent, [])
    indegree = push(indegree, 0)
}
for edge in edge_list {
    let u = edge[0] - 1
    let v = edge[1] - 1
    adjacent[u] = push(adjacent[u], v)
    indegree[v] = indegree[v] + 1
}

let order = []
for v in range(0, n) {
    if indegree[v] == 0 {
        order = push(order, v)
    }
}
let queue_head = 0
while queue_head < len(order) {
    let current = order[queue_head]
    queue_head = queue_head + 1
    for next in adjacent[current] {
        indegree[next] = indegree[next] - 1
        if indegree[next] == 0 {
            order = push(order, next)
        }
    }
}

let result = order |> map(|v| str(v + 1)) |> join(" ")

# Any order with every edge pointing forwards is accepted. Taking the lowest
# numbered ready vertex first happens to reproduce the sample here.
let position = []
for _ in range(0, n) {
    position = push(position, 0)
}
for i in range(0, len(order)) {
    position[order[i]] = i
}
let backwards = edge_list |> filter(|e| position[e[0] - 1] >= position[e[1] - 1])

println("Result:   " + result)
println("Expected: 4 3 1 2  (any valid order is accepted)")

fn test_ts_orders_every_edge_forwards() {
    assert len(order) == n, "TS: expected " + str(n) + " vertices, got " + str(len(order))
    assert len(backwards) == 0, "TS: these edges point backwards: " + str(backwards)
}

HDAG — Hamiltonian Path in DAG

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Hard in general, easy on a DAG: a Hamiltonian path exists exactly when consecutive vertices of the topological order are joined by an edge.

# Rosalind: HDAG — Hamiltonian Path in DAG
# https://rosalind.info/problems/hdag/
#
# Given: A positive integer k and k simple directed acyclic graphs in the edge
# list format.
# Return: For each graph, "1" followed by a Hamiltonian path if one exists, and
# "-1" otherwise.

let graphs = [
    { vertices: 3, edges: [[1, 2], [2, 3], [1, 3]] },
    { vertices: 4, edges: [[4, 3], [3, 2], [4, 1]] },
]

# Finding a Hamiltonian path is hard in general and easy on a DAG, which is why
# the problem restricts to one. A DAG has a Hamiltonian path exactly when its
# topological order is unique, and that holds exactly when consecutive vertices
# in the order are joined by an edge. So: sort topologically, then check the
# n-1 consecutive pairs.
fn hamiltonian_path(graph) {
    let n = graph.vertices
    let adjacent = []
    let indegree = []
    for _ in range(0, n) {
        adjacent = push(adjacent, [])
        indegree = push(indegree, 0)
    }
    for edge in graph.edges {
        let u = edge[0] - 1
        let v = edge[1] - 1
        adjacent[u] = push(adjacent[u], v)
        indegree[v] = indegree[v] + 1
    }

    let order = []
    for v in range(0, n) {
        if indegree[v] == 0 {
            order = push(order, v)
        }
    }
    let head = 0
    while head < len(order) {
        let current = order[head]
        head = head + 1
        for next in adjacent[current] {
            indegree[next] = indegree[next] - 1
            if indegree[next] == 0 {
                order = push(order, next)
            }
        }
    }

    let joined = true
    for i in range(0, len(order) - 1) {
        if contains(adjacent[order[i]], order[i + 1]) == false {
            joined = false
        }
    }

    if joined {
        "1 " + (order |> map(|v| str(v + 1)) |> join(" "))
    } else {
        "-1"
    }
}

let answers = graphs |> map(|g| hamiltonian_path(g))
let result = answers |> join(" / ")

println("Result:   " + result)
println("Expected: 1 1 2 3 / -1")

fn test_hdag_finds_hamiltonian_paths() {
    assert answers[0] == "1 1 2 3", "HDAG[1]: got " + answers[0]
    assert answers[1] == "-1", "HDAG[2]: got " + answers[1]
}

NWC — Negative Weight Cycle

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Every distance starts at 0, not just vertex 1, so a cycle unreachable from vertex 1 is still found.

# Rosalind: NWC — Negative Weight Cycle
# https://rosalind.info/problems/nwc/
#
# Given: A positive integer k and k simple directed graphs with integer edge
# weights in the edge list format.
# Return: For each graph, 1 if it contains a negative weight cycle, -1 otherwise.

let graphs = [
    { vertices: 4, edges: [[1, 4, 4], [4, 2, 3], [2, 3, 1], [3, 1, 6], [2, 1, -7]] },
    { vertices: 3, edges: [[1, 2, -8], [2, 3, 20], [3, 1, -1], [3, 2, -30]] },
]

# Bellman-Ford's other use. After n-1 rounds of relaxation every shortest path
# is final, so an edge that still improves on round n can only be doing it by
# going round a negative cycle. Every distance starts at 0 rather than only
# vertex 1, which is the same as adding a source joined to every vertex by a
# zero-weight edge: the cycle need not be reachable from vertex 1.
fn has_negative_cycle(graph) {
    let n = graph.vertices
    let distance = []
    for _ in range(0, n) {
        distance = push(distance, 0)
    }

    let sweep = 0
    while sweep < n - 1 {
        for edge in graph.edges {
            let u = edge[0] - 1
            let v = edge[1] - 1
            if distance[u] + edge[2] < distance[v] {
                distance[v] = distance[u] + edge[2]
            }
        }
        sweep = sweep + 1
    }

    let improved = false
    for edge in graph.edges {
        if distance[edge[0] - 1] + edge[2] < distance[edge[1] - 1] {
            improved = true
        }
    }
    if improved then 1 else -1
}

let result = graphs |> map(|g| str(has_negative_cycle(g))) |> join(" ")

println("Result:   " + result)
println("Expected: -1 1")

fn test_nwc_detects_negative_cycles() {
    assert result == "-1 1", "NWC: got " + result
}

SCC — Strongly Connected Components

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Kosaraju, both passes iterative: a recursive depth-first search reads better but its depth is the length of a path, and the track allows 10^3 vertices.

# Rosalind: SCC — Strongly Connected Components
# https://rosalind.info/problems/scc/
#
# Given: A simple directed graph with n vertices in the edge list format.
# Return: The number of strongly connected components in the graph.

let n = 6
let edge_list = [[4, 1], [1, 2], [2, 4], [5, 6], [3, 2], [5, 3], [3, 5]]

# Kosaraju's algorithm. Two passes: the first records the order in which
# vertices finish, the second walks the reversed graph taking vertices in the
# reverse of that order. Every tree of the second pass is exactly one component,
# because finishing last means nothing outside the component can reach back in.
#
# Both passes are iterative. A recursive depth-first search reads better but its
# depth is the length of a path, and this pack's problems allow 10^3 vertices.
let adjacent = []
let reversed = []
for _ in range(0, n) {
    adjacent = push(adjacent, [])
    reversed = push(reversed, [])
}
for edge in edge_list {
    let u = edge[0] - 1
    let v = edge[1] - 1
    adjacent[u] = push(adjacent[u], v)
    reversed[v] = push(reversed[v], u)
}

# Pass one: push each vertex after every edge out of it has been followed.
let visited = []
for _ in range(0, n) {
    visited = push(visited, false)
}
let finish_order = []
for start in range(0, n) {
    if visited[start] == false {
        visited[start] = true
        let stack = [[start, 0]]
        while len(stack) > 0 {
            let top = stack[len(stack) - 1]
            let vertex = top[0]
            let next_child = top[1]
            if next_child < len(adjacent[vertex]) {
                stack[len(stack) - 1] = [vertex, next_child + 1]
                let child = adjacent[vertex][next_child]
                if visited[child] == false {
                    visited[child] = true
                    stack = push(stack, [child, 0])
                }
            } else {
                finish_order = push(finish_order, vertex)
                stack = slice(stack, 0, len(stack) - 1)
            }
        }
    }
}

# Pass two: walk the reversed graph, latest finisher first.
let component = []
for _ in range(0, n) {
    component = push(component, -1)
}
let components = 0
let position = len(finish_order) - 1
while position >= 0 {
    let start = finish_order[position]
    if component[start] == -1 {
        component[start] = components
        let stack = [start]
        while len(stack) > 0 {
            let current = stack[len(stack) - 1]
            stack = slice(stack, 0, len(stack) - 1)
            for previous in reversed[current] {
                if component[previous] == -1 {
                    component[previous] = components
                    stack = push(stack, previous)
                }
            }
        }
        components = components + 1
    }
    position = position - 1
}

println("Result:   " + str(components))
println("Expected: 3")
println("Membership (vertex -> component): " + str(component))

fn test_scc_component_count() {
    assert components == 3, "SCC: got " + str(components)
    # 1, 2 and 4 lie on a cycle, as do 3 and 5; 6 is on its own.
    assert component[0] == component[1], "SCC: 1 and 2 should share a component"
    assert component[0] == component[3], "SCC: 1 and 4 should share a component"
    assert component[2] == component[4], "SCC: 3 and 5 should share a component"
    assert component[5] != component[4], "SCC: 6 should be on its own"
}

2SAT — 2-Satisfiability

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

The hardest problem in the track and the one that pays off the rest: the implication graph is solved with the same Kosaraju pass SCC uses. Answers are checked by substituting them back into the formula, since any satisfying assignment is accepted.

# Rosalind: 2SAT — 2-Satisfiability
# https://rosalind.info/problems/2sat/
#
# Given: A positive integer k and k 2SAT formulas, each a list of two-literal
# clauses over n variables, where -3 means "not x3".
# Return: For each formula, 0 if it cannot be satisfied, or 1 followed by a
# satisfying assignment.

let formulas = [
    { variables: 2, clauses: [[1, 2], [-1, 2], [1, -2], [-1, -2]] },
    { variables: 3, clauses: [[1, 2], [2, 3], [-1, -2], [-2, -3]] },
]

# Two literals per clause is what makes this tractable where 3SAT is not. A
# clause (a or b) is the pair of implications "not a => b" and "not b => a", so
# a formula is a directed graph on the 2n literals. It is unsatisfiable exactly
# when some variable and its negation are strongly connected — that would say
# each implies the other.
#
# The assignment falls out of the same components. Kosaraju numbers them in
# topological order, so making each variable agree with whichever of its two
# literals is numbered higher never points an implication from a true literal to
# a false one, which is what satisfies every clause at once.

# Literal l becomes a node: x_i is 2(i-1) and its negation 2(i-1)+1.
fn literal_node(literal) {
    if literal > 0 {
        2 * (literal - 1)
    } else {
        2 * (0 - literal - 1) + 1
    }
}

fn negate_node(node) {
    if node % 2 == 0 then node + 1 else node - 1
}

fn strong_components(size, links) {
    let adjacent = []
    let reversed = []
    for _ in range(0, size) {
        adjacent = push(adjacent, [])
        reversed = push(reversed, [])
    }
    for link in links {
        adjacent[link[0]] = push(adjacent[link[0]], link[1])
        reversed[link[1]] = push(reversed[link[1]], link[0])
    }

    let visited = []
    for _ in range(0, size) {
        visited = push(visited, false)
    }
    let finish_order = []
    for start in range(0, size) {
        if visited[start] == false {
            visited[start] = true
            let stack = [[start, 0]]
            while len(stack) > 0 {
                let top = stack[len(stack) - 1]
                let vertex = top[0]
                let next_child = top[1]
                if next_child < len(adjacent[vertex]) {
                    stack[len(stack) - 1] = [vertex, next_child + 1]
                    let child = adjacent[vertex][next_child]
                    if visited[child] == false {
                        visited[child] = true
                        stack = push(stack, [child, 0])
                    }
                } else {
                    finish_order = push(finish_order, vertex)
                    stack = slice(stack, 0, len(stack) - 1)
                }
            }
        }
    }

    let component = []
    for _ in range(0, size) {
        component = push(component, -1)
    }
    let count = 0
    let position = size - 1
    while position >= 0 {
        let start = finish_order[position]
        if component[start] == -1 {
            component[start] = count
            let stack = [start]
            while len(stack) > 0 {
                let current = stack[len(stack) - 1]
                stack = slice(stack, 0, len(stack) - 1)
                for previous in reversed[current] {
                    if component[previous] == -1 {
                        component[previous] = count
                        stack = push(stack, previous)
                    }
                }
            }
            count = count + 1
        }
        position = position - 1
    }
    component
}

fn solve(formula) {
    let n = formula.variables
    let size = 2 * n

    let links = []
    for clause in formula.clauses {
        let a = literal_node(clause[0])
        let b = literal_node(clause[1])
        links = push(links, [negate_node(a), b])
        links = push(links, [negate_node(b), a])
    }

    let component = strong_components(size, links)

    let satisfiable = true
    for i in range(0, n) {
        if component[2 * i] == component[2 * i + 1] {
            satisfiable = false
        }
    }

    if satisfiable == false {
        "0"
    } else {
        let assignment = []
        for i in range(0, n) {
            # The literal in the later component is the one to make true.
            if component[2 * i] > component[2 * i + 1] {
                assignment = push(assignment, i + 1)
            } else {
                assignment = push(assignment, 0 - (i + 1))
            }
        }
        "1 " + (assignment |> map(|v| str(v)) |> join(" "))
    }
}

# Any satisfying assignment is accepted, so the answers are checked by putting
# them back into the formula rather than against the sample text.
fn satisfies(formula, answer) {
    if answer == "0" {
        true
    } else {
        let parts = answer |> split(" ")
        let values = slice(parts, 1, len(parts)) |> map(|t| int(t))
        let unmet = formula.clauses |> filter(|clause| {
            let first = contains(values, clause[0])
            let second = contains(values, clause[1])
            first == false and second == false
        })
        len(unmet) == 0
    }
}

let answers = formulas |> map(|f| solve(f))
let verdicts = range(0, len(formulas)) |> map(|i| satisfies(formulas[i], answers[i]))

for i in range(0, len(formulas)) {
    println("Formula " + str(i + 1) + ": " + answers[i] + "   satisfied: " + str(verdicts[i]))
}
println("Sample output: 0 / 1 1 -2 3")

fn test_2sat_solves_and_refutes() {
    assert answers[0] == "0", "2SAT[1]: this formula is unsatisfiable, got " + answers[0]
    assert verdicts == [true, true], "2SAT: an answer does not satisfy its formula: " + str(verdicts)
    assert substr(answers[1], 0, 1) == "1", "2SAT[2]: expected a satisfying assignment, got " + answers[1]
}

GS — General Sink

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: GS — General Sink
# https://rosalind.info/problems/gs/
#
# Given: A positive integer k and k simple directed graphs in the edge list format.
# Return: For each graph, a vertex from which all other vertices are reachable,
# or -1 if there is none.

let graphs = [
    { vertices: 3, edges: [[3, 2], [2, 1]] },
    { vertices: 3, edges: [[3, 2], [1, 2]] },
]

# Reachability from each vertex in turn. At this size that is the clear way to
# say it; the linear answer condenses the strongly connected components and
# checks whether the single source of the condensation reaches everything, which
# is the same test done once instead of n times.
fn reachable_from(adjacent, start, n) {
    let seen = []
    for _ in range(0, n) {
        seen = push(seen, false)
    }
    seen[start] = true
    let stack = [start]
    let reached = 1
    while len(stack) > 0 {
        let current = stack[len(stack) - 1]
        stack = slice(stack, 0, len(stack) - 1)
        for next in adjacent[current] {
            if seen[next] == false {
                seen[next] = true
                reached = reached + 1
                stack = push(stack, next)
            }
        }
    }
    reached
}

fn general_sink(graph) {
    let n = graph.vertices
    let adjacent = []
    for _ in range(0, n) {
        adjacent = push(adjacent, [])
    }
    for edge in graph.edges {
        adjacent[edge[0] - 1] = push(adjacent[edge[0] - 1], edge[1] - 1)
    }

    let answer = -1
    for v in range(0, n) {
        if answer == -1 and reachable_from(adjacent, v, n) == n {
            answer = v + 1
        }
    }
    answer
}

let result = graphs |> map(|g| str(general_sink(g))) |> join(" ")

println("Result:   " + result)
println("Expected: 3 -1")

fn test_gs_finds_the_source_vertex() {
    assert result == "3 -1", "GS: got " + result
}

SC — Semi-Connected Graph

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

# Rosalind: SC — Semi-Connected Graph
# https://rosalind.info/problems/sc/
#
# Given: A positive integer k and k simple directed graphs in the edge list format.
# Return: For each graph, 1 if the graph is semi-connected and -1 otherwise.

let graphs = [
    { vertices: 3, edges: [[3, 2], [2, 1]] },
    { vertices: 3, edges: [[3, 2], [1, 2]] },
]

# Semi-connected means every pair of vertices is comparable: one of the two
# reaches the other. The second graph fails on the pair 1 and 3 — both reach 2
# and neither reaches the other. Reachability is computed from every vertex and
# the pairs are then checked directly, which says the definition out loud.
fn reachable_set(adjacent, start, n) {
    let seen = []
    for _ in range(0, n) {
        seen = push(seen, false)
    }
    seen[start] = true
    let stack = [start]
    while len(stack) > 0 {
        let current = stack[len(stack) - 1]
        stack = slice(stack, 0, len(stack) - 1)
        for next in adjacent[current] {
            if seen[next] == false {
                seen[next] = true
                stack = push(stack, next)
            }
        }
    }
    seen
}

fn is_semi_connected(graph) {
    let n = graph.vertices
    let adjacent = []
    for _ in range(0, n) {
        adjacent = push(adjacent, [])
    }
    for edge in graph.edges {
        adjacent[edge[0] - 1] = push(adjacent[edge[0] - 1], edge[1] - 1)
    }

    let reaches = []
    for v in range(0, n) {
        reaches = push(reaches, reachable_set(adjacent, v, n))
    }

    let comparable = true
    for u in range(0, n) {
        for v in range(u + 1, n) {
            if reaches[u][v] == false and reaches[v][u] == false {
                comparable = false
            }
        }
    }
    if comparable then 1 else -1
}

let result = graphs |> map(|g| str(is_semi_connected(g))) |> join(" ")

println("Result:   " + result)
println("Expected: 1 -1")

fn test_sc_checks_every_pair() {
    assert result == "1 -1", "SC: got " + result
}

SDAG — Shortest Paths in DAG

solvedbrowser + CLI Problem statement Open in the workbench Download .bl

Relaxing once in topological order is linear, where Bellman-Ford needs n-1 rounds; acyclicity is what buys that.

# Rosalind: SDAG — Shortest Paths in DAG
# https://rosalind.info/problems/sdag/
#
# Given: A weighted DAG with integer edge weights and n vertices in the edge
# list format.
# Return: An array D[1..n] of shortest path lengths from vertex 1, with "x"
# where a vertex is unreachable.

let n = 5
let edge_list = [[2, 3, 4], [4, 3, -2], [1, 4, 1], [1, 5, -3], [2, 4, -2], [5, 4, 1]]

# Weights can be negative, so Dijkstra is out; but the graph is acyclic, so
# Bellman-Ford's n-1 rounds are unnecessary. Relaxing the edges once in
# topological order is enough, because a vertex is only ever reached from
# vertices that come before it. That makes this linear where BF is quadratic.
let adjacent = []
let indegree = []
for _ in range(0, n) {
    adjacent = push(adjacent, [])
    indegree = push(indegree, 0)
}
for edge in edge_list {
    adjacent[edge[0] - 1] = push(adjacent[edge[0] - 1], [edge[1] - 1, edge[2]])
    indegree[edge[1] - 1] = indegree[edge[1] - 1] + 1
}

let order = []
for v in range(0, n) {
    if indegree[v] == 0 {
        order = push(order, v)
    }
}
let queue_head = 0
while queue_head < len(order) {
    let current = order[queue_head]
    queue_head = queue_head + 1
    for link in adjacent[current] {
        indegree[link[0]] = indegree[link[0]] - 1
        if indegree[link[0]] == 0 {
            order = push(order, link[0])
        }
    }
}

let infinity = 1000000000
let distance = []
for _ in range(0, n) {
    distance = push(distance, infinity)
}
distance[0] = 0

for v in order {
    if distance[v] < infinity {
        for link in adjacent[v] {
            if distance[v] + link[1] < distance[link[0]] {
                distance[link[0]] = distance[v] + link[1]
            }
        }
    }
}

let result = distance |> map(|d| if d == infinity then "x" else str(d)) |> join(" ")

println("Result:   " + result)
println("Expected: 0 x -4 -2 -3")

fn test_sdag_shortest_paths() {
    assert result == "0 x -4 -2 -3", "SDAG: got " + result
}