Skip to content

Second-Best Minimum Spanning Tree — Practice Tasks

All tasks must be solved in Go, Java, and Python. Each task ships with a problem spec, constraints, hints, and starter code in all three languages. Fill in the logic and validate against the evaluation criteria. A final benchmark task compares the naive O(E·V) and the LCA O((V+E) log V) approaches.

Reusable building blocks across tasks: Union-Find / Kruskal (from 10-mst-kruskal-prim), LCA binary lifting (from 13-lca), and the three-branch swap rule: w > max → w−max; w == max → 0; w < max & second-max exists → w−secondMax.


Beginner Tasks (5)

Task 1 — Build the MST and list its non-tree edges

Problem. Read a connected weighted undirected graph. Build the MST with Kruskal. Print the MST weight, then print every non-tree edge (the edges not chosen) one per line as u v w. These non-tree edges are the only swap candidates for the second-best MST.

Input / Output spec. - First line: n m (vertices, edges). - Next m lines: u v w. - Output: MST weight on line 1, then each non-tree edge u v w.

Constraints. - 2 ≤ n ≤ 10^5, n−1 ≤ m ≤ 2·10^5. - Weights fit in 32-bit; the graph is connected.

Hint. Sort edges by weight; union the endpoints if they are in different sets (tree edge) else record it as non-tree.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
    "sort"
)

type Edge struct{ u, v, w int }

func main() {
    in := bufio.NewReader(os.Stdin)
    out := bufio.NewWriter(os.Stdout)
    defer out.Flush()
    var n, m int
    fmt.Fscan(in, &n, &m)
    edges := make([]Edge, m)
    for i := range edges {
        fmt.Fscan(in, &edges[i].u, &edges[i].v, &edges[i].w)
    }
    _ = sort.Slice
    // TODO: Kruskal. Track inMST[]. Sum weight. Print non-tree edges.
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task1 {
    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());
        int m = Integer.parseInt(st.nextToken());
        int[][] edges = new int[m][3];
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            edges[i][0] = Integer.parseInt(st.nextToken());
            edges[i][1] = Integer.parseInt(st.nextToken());
            edges[i][2] = Integer.parseInt(st.nextToken());
        }
        // TODO: sort by weight, Kruskal, print MST weight + non-tree edges
    }
}

Starter — Python.

import sys

def main():
    data = sys.stdin.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    m = int(data[idx]); idx += 1
    edges = []
    for _ in range(m):
        u, v, w = int(data[idx]), int(data[idx+1]), int(data[idx+2])
        idx += 3
        edges.append((u, v, w))
    # TODO: Kruskal, print MST weight then non-tree edges
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - MST weight matches a reference Kruskal. - Exactly m − (n − 1) non-tree edges are printed. - Runs within time for m = 2·10^5.


Task 2 — Path maximum on a fixed tree (single query)

Problem. You are given a tree (not a general graph) with n nodes and weighted edges, plus one query (u, v). Print the maximum-weight edge on the path between u and v. Use a simple BFS/DFS walk for this task (no LCA needed yet).

Input / Output spec. - First line: n. - Next n−1 lines: a b w. - Last line: u v. - Output: the maximum edge weight on the path u..v.

Constraints. - 2 ≤ n ≤ 2000 (so an O(n) walk is fine). - Weights are positive 32-bit integers.

Hint. BFS from u recording the max edge seen to reach each node; read off the max for v.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    var n int
    fmt.Fscan(in, &n)
    adj := make([][][2]int, n)
    for i := 0; i < n-1; i++ {
        var a, b, w int
        fmt.Fscan(in, &a, &b, &w)
        adj[a] = append(adj[a], [2]int{b, w})
        adj[b] = append(adj[b], [2]int{a, w})
    }
    var u, v int
    fmt.Fscan(in, &u, &v)
    // TODO: BFS from u, track max edge to reach each node, print maxTo[v]
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task2 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        List<int[]>[] adj = new List[n];
        for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();
        for (int i = 0; i < n - 1; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            int w = Integer.parseInt(st.nextToken());
            adj[a].add(new int[]{b, w});
            adj[b].add(new int[]{a, w});
        }
        StringTokenizer st = new StringTokenizer(br.readLine());
        int u = Integer.parseInt(st.nextToken());
        int v = Integer.parseInt(st.nextToken());
        // TODO: BFS from u tracking max edge, print result for v
    }
}

Starter — Python.

import sys
from collections import deque

def main():
    data = sys.stdin.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    adj = [[] for _ in range(n)]
    for _ in range(n - 1):
        a, b, w = int(data[idx]), int(data[idx+1]), int(data[idx+2])
        idx += 3
        adj[a].append((b, w))
        adj[b].append((a, w))
    u, v = int(data[idx]), int(data[idx+1])
    # TODO: BFS from u tracking max edge to each node, print max_to[v]
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Correct on hand traces (path tree, star tree, single edge). - O(n) per query; no LCA required at this size.


Task 3 — Second-best MST weight (naive path walk)

Problem. Compute the second-best MST weight using the naive method: build the MST, then for each non-tree edge walk the tree path in O(V) to find the max (and, for ties, second-max) edge, and apply the three-branch rule. Print the second-best weight, or -1 if no second-best exists.

Input / Output spec. Same input format as Task 1; output the single second-best weight (or -1).

Constraints. - 2 ≤ n ≤ 2000, m ≤ 5000 (naive O(E·V) must pass). - Weights are 32-bit; graph connected.

Hint. For each non-tree edge, BFS/DFS the MST from u to v recording the two largest distinct edge weights on the path. Remember the w == max → 0 branch.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
    "sort"
)

type Edge struct{ u, v, w int }

func main() {
    in := bufio.NewReader(os.Stdin)
    var n, m int
    fmt.Fscan(in, &n, &m)
    edges := make([]Edge, m)
    for i := range edges {
        fmt.Fscan(in, &edges[i].u, &edges[i].v, &edges[i].w)
    }
    _ = sort.Slice
    // TODO: Kruskal -> inMST, mstW, adjacency.
    // TODO: for each non-tree edge, walk path for (max, secondMax).
    // TODO: apply three-branch rule, track best delta, print mstW+best or -1.
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task3 {
    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());
        int m = Integer.parseInt(st.nextToken());
        int[][] edges = new int[m][3];
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < 3; j++) edges[i][j] = Integer.parseInt(st.nextToken());
        }
        // TODO: Kruskal, then naive path walk for (max, secondMax) per non-tree edge.
    }
}

Starter — Python.

import sys

def main():
    data = sys.stdin.read().split()
    p = 0
    n = int(data[p]); p += 1
    m = int(data[p]); p += 1
    edges = []
    for _ in range(m):
        edges.append((int(data[p]), int(data[p+1]), int(data[p+2])))
        p += 3
    # TODO: Kruskal -> in_mst, mst_w, adjacency
    # TODO: naive path walk per non-tree edge -> (max, second_max)
    # TODO: three-branch delta, print mst_w + best or -1
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Matches a brute-force enumerator on small graphs, including tie-heavy ones. - Returns -1 for a tree input. - Returns MST weight (delta 0) when multiple MSTs exist.


Task 4 — Detect whether the MST is unique

Problem. Given a connected weighted graph, print UNIQUE if it has exactly one MST, else MULTIPLE. Use the second-best machinery: the MST is non-unique iff some non-tree edge's weight equals its path-maximum (delta = 0).

Input / Output spec. Same input as Task 1; output UNIQUE or MULTIPLE.

Constraints. - 2 ≤ n ≤ 2000, m ≤ 5000 (naive path walk acceptable).

Hint. Build the MST; for each non-tree edge compute the path-max; if any non-tree edge has w == pathMax, the MST is not unique.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    var n, m int
    fmt.Fscan(in, &n, &m)
    type E struct{ u, v, w int }
    edges := make([]E, m)
    for i := range edges {
        fmt.Fscan(in, &edges[i].u, &edges[i].v, &edges[i].w)
    }
    // TODO: Kruskal; for each non-tree edge compute pathMax.
    // TODO: if any non-tree w == pathMax -> MULTIPLE, else UNIQUE.
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task4 {
    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());
        int m = Integer.parseInt(st.nextToken());
        int[][] edges = new int[m][3];
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < 3; j++) edges[i][j] = Integer.parseInt(st.nextToken());
        }
        // TODO: Kruskal, pathMax per non-tree edge; print UNIQUE / MULTIPLE
    }
}

Starter — Python.

import sys

def main():
    data = sys.stdin.read().split()
    p = 0
    n = int(data[p]); p += 1
    m = int(data[p]); p += 1
    edges = []
    for _ in range(m):
        edges.append((int(data[p]), int(data[p+1]), int(data[p+2])))
        p += 3
    # TODO: Kruskal; if any non-tree edge w == its path-max -> MULTIPLE else UNIQUE
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Agrees with a brute-force MST counter on small graphs. - All-distinct-weight graphs report UNIQUE. - Graphs with a duplicate-weight cycle report MULTIPLE.


Problem. Given a connected graph and a designated tree edge f = (a, b) (guaranteed to be in the MST), compute the cheapest spanning tree that does not use f. That is the MST minus f plus the lightest non-tree edge whose fundamental cycle covers f. Print that weight, or -1 if removing f disconnects the graph.

Input / Output spec. - Same graph input as Task 1, then a final line a b identifying the lost tree edge. - Output: the cheapest spanning weight avoiding f, or -1.

Constraints. - 2 ≤ n ≤ 2000, m ≤ 5000.

Hint. Removing f splits the MST into two components; the cheapest reconnection is the minimum-weight non-tree edge with one endpoint in each component. Find the components via BFS on the MST without f.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    var n, m int
    fmt.Fscan(in, &n, &m)
    type E struct{ u, v, w int }
    edges := make([]E, m)
    for i := range edges {
        fmt.Fscan(in, &edges[i].u, &edges[i].v, &edges[i].w)
    }
    var a, b int
    fmt.Fscan(in, &a, &b)
    // TODO: Kruskal -> mstW, tree adjacency.
    // TODO: remove edge (a,b) from the tree, BFS to 2-color components.
    // TODO: min non-tree edge crossing the cut; print mstW - w(f) + minCross or -1.
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task5 {
    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());
        int m = Integer.parseInt(st.nextToken());
        int[][] edges = new int[m][3];
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < 3; j++) edges[i][j] = Integer.parseInt(st.nextToken());
        }
        st = new StringTokenizer(br.readLine());
        int a = Integer.parseInt(st.nextToken());
        int b = Integer.parseInt(st.nextToken());
        // TODO: Kruskal; cut MST at (a,b); 2-color; min crossing non-tree edge.
    }
}

Starter — Python.

import sys
from collections import deque

def main():
    data = sys.stdin.read().split()
    p = 0
    n = int(data[p]); p += 1
    m = int(data[p]); p += 1
    edges = []
    for _ in range(m):
        edges.append((int(data[p]), int(data[p+1]), int(data[p+2])))
        p += 3
    a, b = int(data[p]), int(data[p+1])
    # TODO: Kruskal; remove (a,b); BFS-color components; min crossing edge
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Correct on a path graph where f is the only bridge between halves. - Returns -1 when f is a bridge of the whole graph (no reconnection). - Matches "rebuild MST without f" on random graphs.


Intermediate Tasks (5)

Task 6 — Second-best MST with LCA binary lifting

Problem. Re-solve Task 3 but at scale: build the MST, then answer all path-max (and second-max) queries with LCA binary lifting in O(log V). Print the second-best weight or -1.

Input / Output spec. Same as Task 1; output the second-best weight or -1.

Constraints. - 2 ≤ n ≤ 10^5, m ≤ 2·10^5. The naive O(E·V) will TLE; you must use LCA.

Hint. Store up[k][v], mx1[k][v], mx2[k][v]. Build the tree iteratively. Merge keeps the two largest distinct values.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
    "sort"
)

const NEG = -1 << 60

type Edge struct{ u, v, w int }

func merge(a1, a2, b1, b2 int) (int, int) {
    // TODO: return the two largest distinct values among the four
    return 0, 0
}

func main() {
    in := bufio.NewReader(os.Stdin)
    var n, m int
    fmt.Fscan(in, &n, &m)
    edges := make([]Edge, m)
    for i := range edges {
        fmt.Fscan(in, &edges[i].u, &edges[i].v, &edges[i].w)
    }
    _ = sort.Slice
    _ = NEG
    // TODO: Kruskal -> adjacency, inMST, mstW.
    // TODO: build LCA tables (up, mx1, mx2). Query each non-tree edge.
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task6 {
    static final int NEG = Integer.MIN_VALUE / 4;
    static int[] merge(int a1, int a2, int b1, int b2) {
        // TODO: two largest distinct values
        return new int[]{0, 0};
    }
    public static void main(String[] args) throws IOException {
        // TODO: read graph, Kruskal, build LCA, scan non-tree edges
    }
}

Starter — Python.

import sys

NEG = float("-inf")

def merge(a1, a2, b1, b2):
    # TODO: two largest distinct values among the four
    pass

def main():
    data = sys.stdin.buffer.read().split()
    # TODO: parse, Kruskal, build lifting tables, query non-tree edges
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Output identical to Task 3 on shared inputs. - Runs within time for m = 2·10^5 (the naive version would not). - Passes a tie-heavy fuzz test vs brute force.


Task 7 — Report the actual swap edges

Problem. Extend Task 6: along with the second-best weight, print the entering non-tree edge u v w and the weight of the removed tree edge. If several swaps tie for the minimum delta, print the one whose entering edge has the smallest index.

Input / Output spec. Same input as Task 1; output two lines: the second-best weight, then enteringU enteringV enteringW removedWeight. If no second-best, print -1.

Constraints. 2 ≤ n ≤ 10^5, m ≤ 2·10^5.

Hint. Track the winning edge index and the removed weight alongside the minimum delta; iterate edges in index order so ties resolve to the smallest index.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    var n, m int
    fmt.Fscan(in, &n, &m)
    // TODO: as Task 6, but also remember bestIdx and removedWeight for the min delta.
    _ = n
    _ = m
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task7 {
    public static void main(String[] args) throws IOException {
        // TODO: as Task 6, additionally track entering edge index + removed weight
    }
}

Starter — Python.

import sys

def main():
    data = sys.stdin.buffer.read().split()
    # TODO: as Task 6, track (best_delta, entering_index, removed_weight)
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - The reported swap reconstructs a tree whose weight equals the printed second-best. - Tie-break by smallest entering-edge index is respected. - -1 for tree-only inputs.


Task 8 — Critical and pseudo-critical MST edges

Problem. Given a connected graph with indexed edges, output the indices of all critical edges (in every MST) and all pseudo-critical edges (in some MST but not all). Follow the same spec as LeetCode 1489.

Input / Output spec. - n m, then m lines u v w (edge i is the i-th read). - Output two lines: critical indices (ascending), then pseudo-critical indices (ascending).

Constraints. - 2 ≤ n ≤ 100, m ≤ 200 (so O(m²) force-in/force-out is fine).

Hint. Base MST weight B. Edge is critical if forcing it out makes the MST weight > B (or disconnects). Else pseudo-critical if forcing it in keeps weight = B.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
    "sort"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    var n, m int
    fmt.Fscan(in, &n, &m)
    type E struct{ u, v, w, id int }
    edges := make([]E, m)
    for i := 0; i < m; i++ {
        fmt.Fscan(in, &edges[i].u, &edges[i].v, &edges[i].w)
        edges[i].id = i
    }
    sort.Slice(edges, func(a, b int) bool { return edges[a].w < edges[b].w })
    // TODO: mstWeight(skip, force) helper; classify each original edge id.
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task8 {
    public static void main(String[] args) throws IOException {
        // TODO: read graph; sort; mstWeight(skip, force); classify.
    }
}

Starter — Python.

import sys

def main():
    data = sys.stdin.read().split()
    # TODO: read; sort; mst_weight(skip, force); classify each edge id
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Matches a brute-force "in every / some MST" enumerator on small graphs. - Output indices are sorted ascending. - Handles ties (the whole point of the problem).


Task 9 — Edge-weight tolerance ranges

Problem. For each MST (tree) edge f, compute how much its weight could increase before the MST changes: that is the smallest non-tree-edge weight among edges whose fundamental cycle covers f, minus w(f) (or +∞ if no such edge). For each non-tree edge e, compute how much its weight could decrease before it enters the MST: w(e) − pathMax(e). Print both tables.

Input / Output spec. - Same graph input as Task 1. - Output: for each tree edge, u v w tolerance; for each non-tree edge, u v w slack.

Constraints. - 2 ≤ n ≤ 2000, m ≤ 5000 (naive acceptable; LCA optional for credit).

Hint. A non-tree edge e covering a path "tightens" every tree edge on that path: tree edge f's tolerance is min over covering e of w(e) − w(f). Sweep non-tree edges; for each, update the min covering weight of every tree edge on its path.

Starter — Go.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    var n, m int
    fmt.Fscan(in, &n, &m)
    // TODO: Kruskal; for non-tree edges, sweep their paths to set tree-edge tolerances.
    // TODO: for each non-tree edge slack = w - pathMax.
    _ = n
    _ = m
}

Starter — Java.

import java.util.*;
import java.io.*;

public class Task9 {
    public static void main(String[] args) throws IOException {
        // TODO: Kruskal; compute tree-edge tolerances and non-tree slacks
    }
}

Starter — Python.

import sys

def main():
    data = sys.stdin.read().split()
    # TODO: Kruskal; tolerances via covering sweep; slacks via path-max
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - A tree edge with tolerance t: increasing its weight by < t keeps the same MST; by > t changes it (verify by re-running Kruskal with the perturbed weight). - Non-tree slack s: decreasing w(e) by > s makes e enter the MST. - Edges with no covering non-tree edge report +∞ tolerance.


Task 10 — Stress test against brute force

Problem. Write a fuzzer: generate random small connected graphs (n ≤ 7) with integer weights in a tiny range (to force ties), compute the second-best weight with both a brute-force enumerator (all C(m, n−1) subsets) and your fast LCA solution, and assert they agree over thousands of trials. Report the number of trials and any mismatches.

Input / Output spec. - No stdin; the program self-generates inputs from a fixed seed. - Output: tested <count> fails <count> and the first few failing graphs if any.

Constraints. - n ∈ [3, 7], weights in [1, 6], at least 5000 trials.

Hint. The brute-force second-best is: enumerate spanning-tree weights; if the minimum weight occurs ≥ 2 times the answer equals the MST weight, else it is the next distinct weight. This is the exact oracle that catches the equal-weight bug.

Starter — Go.

package main

import (
    "fmt"
    "math/rand"
)

func bruteSecondBest(n int, edges [][3]int) int {
    // TODO: enumerate all (n-1)-subsets, keep spanning trees, derive second-best
    return 0
}

func fastSecondBest(n int, edges [][3]int) int {
    // TODO: your Task 6 solution adapted to this signature
    return 0
}

func main() {
    r := rand.New(rand.NewSource(1))
    tested, fails := 0, 0
    for t := 0; t < 5000; t++ {
        _ = r
        // TODO: build a random connected graph, compare brute vs fast
    }
    fmt.Printf("tested %d fails %d\n", tested, fails)
}

Starter — Java.

import java.util.*;

public class Task10 {
    public static void main(String[] args) {
        Random r = new Random(1);
        int tested = 0, fails = 0;
        for (int t = 0; t < 5000; t++) {
            // TODO: random connected graph; compare brute vs fast second-best
        }
        System.out.println("tested " + tested + " fails " + fails);
    }
}

Starter — Python.

import itertools, random

def brute_second_best(n, edges):
    # TODO: enumerate spanning trees; if min weight repeats -> equals MST else next distinct
    pass

def fast_second_best(n, edges):
    # TODO: Task 6 solution
    pass

def main():
    random.seed(1)
    tested = fails = 0
    for _ in range(5000):
        # TODO: random connected graph; compare
        pass
    print(f"tested {tested} fails {fails}")

if __name__ == "__main__":
    main()

Evaluation criteria. - fails == 0 across all trials. - The fuzzer actually exercises ties (verify it generates graphs where second-best equals MST). - Removing the equal-weight branch from the fast solution makes the fuzzer report failures (proves the test has teeth).


Advanced Tasks (5)

Task 11 — Second-best MST with heavy-light decomposition

Problem. Solve the second-best MST, but compute path-maxima with heavy-light decomposition over a segment tree instead of binary lifting. This prepares you for the dynamic variant in Task 13.

Constraints. 2 ≤ n ≤ 10^5, m ≤ 2·10^5. Per-query O(log² V) is acceptable.

Hint. Put edge weights on the child endpoint. HLD decomposes the path into O(log V) chains; query the max over each chain's segment-tree range. See sibling 14-heavy-light-decomposition.

Starter — Go.

package main

// TODO: HLD with segment tree storing edge weights on child nodes.
// TODO: pathMax(u, v) climbing chains; then the standard non-tree-edge scan.
func main() {}

Starter — Java.

public class Task11 {
    // TODO: HLD + segment tree path-max; second-best scan.
    public static void main(String[] args) {}
}

Starter — Python.

# TODO: HLD + iterative segment tree for path-max; second-best scan.
def main():
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Output matches the binary-lifting solution (Task 6) on shared inputs. - Path decomposition uses O(log V) chains per query. - For first-max only this is enough; extend the segment tree node to carry (max, second-max) for full correctness on ties.


Problem. For every tree edge f, compute the cheapest spanning tree that avoids f (the backup cost from Task 5, but for all tree edges at once and efficiently). Print, for each tree edge, u v w backupWeight (or -1 if f is a bridge of the graph).

Constraints. 2 ≤ n ≤ 10^5, m ≤ 2·10^5. Aim for near-linear via a covering sweep, not O(V) per edge.

Hint. Sweep non-tree edges in increasing weight. Each covers a tree path; assign it as the backup of every still-unassigned tree edge on that path, using a union-find "jump to the next unassigned tree edge toward the LCA" trick so each tree edge is assigned exactly once.

Starter — Go.

package main

// TODO: Kruskal; LCA for path endpoints; DSU "skip assigned" pointer-jumping
// to assign each tree edge its lightest covering non-tree edge in near-linear time.
func main() {}

Starter — Java.

public class Task12 {
    // TODO: covering sweep with DSU skip-pointers for per-link backups.
    public static void main(String[] args) {}
}

Starter — Python.

# TODO: covering sweep + DSU skip pointers; per-link backup table.
def main():
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Each tree edge's backup matches a direct "remove f, find min crossing edge" check. - Bridges report -1. - Total time is near-linear (each tree edge assigned once), verified by timing on m = 2·10^5.


Task 13 — Online weight updates with HLD

Problem. Support a mix of operations on a fixed-topology graph whose MST you maintain conceptually via path-max queries: (1) update the weight of an existing edge, (2) query the current second-best MST weight. Use HLD + segment tree so each update is O(log² V); recompute the second-best contribution lazily for affected non-tree edges.

Constraints. 2 ≤ n ≤ 5·10^4, total operations ≤ 10^5. Topology does not change; only weights do.

Hint. Maintain the MST and the HLD path-max structure. On a weight change, only swaps through paths touching the changed edge can flip; recompute the global second-best by re-scanning affected non-tree edges (or, for full credit, maintain a multiset of per-edge deltas keyed by edge and update incrementally).

Starter — Go.

package main

// TODO: HLD path-max with point updates; maintain min swap delta under updates.
func main() {}

Starter — Java.

public class Task13 {
    // TODO: HLD + segment tree updates; incremental second-best.
    public static void main(String[] args) {}
}

Starter — Python.

# TODO: HLD updates; recompute/maintain second-best across operations.
def main():
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Each query matches a from-scratch recomputation at that moment. - Updates are O(log² V); queries within budget. - Correct under interleaved updates and queries (stress vs naive recompute).


Task 14 — k smallest spanning tree weights

Problem. Output the weights of the k smallest spanning trees in nondecreasing order (with multiplicity). For k = 2 this is MST then second-best; for larger k use Eppstein/Gabow branching: a priority queue of constrained subproblems, each expanded by its best constrained swap.

Constraints. 2 ≤ n ≤ 50, m ≤ 200, k ≤ 100. Correctness over speed at this size.

Hint. A subproblem fixes some edges "in" and some "out." Its best tree is the constrained MST; its best swap is the minimum-delta swap respecting the constraints (path-max over removable edges only). Pop the cheapest, emit it, and branch on the swap edge.

Starter — Go.

package main

// TODO: constrained MST + best swap; priority queue of subproblems; pop k times.
func main() {}

Starter — Java.

public class Task14 {
    // TODO: Eppstein/Gabow branching for k-best spanning trees.
    public static void main(String[] args) {}
}

Starter — Python.

# TODO: branching over (forced-in, forced-out) subproblems; k smallest weights.
def main():
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - For k = 1, 2 matches MST and second-best exactly. - The emitted sequence is nondecreasing. - Matches a brute-force "sort all spanning-tree weights, take first k" on tiny graphs.


Task 15 — Count distinct second-best spanning trees

Problem. Given a connected graph, count how many distinct spanning trees achieve the second-best weight. (When the MST is non-unique, "second-best weight" equals the MST weight, and you count distinct minimum trees minus the one designated MST — define and document your convention.)

Constraints. 2 ≤ n ≤ 12, m ≤ 30. Brute force or careful combinatorics.

Hint. For small n, enumerate spanning trees (or use Kirchhoff's theorem to count, see sibling 24-kirchhoff-theorem), bucket by weight, and report the count at the second-best weight. State clearly whether two trees that differ only by an equal-weight edge identity count as distinct.

Starter — Go.

package main

// TODO: enumerate spanning trees; bucket by weight; count at second-best weight.
func main() {}

Starter — Java.

public class Task15 {
    // TODO: enumerate / count spanning trees by weight; report second-best count.
    public static void main(String[] args) {}
}

Starter — Python.

# TODO: enumerate spanning trees by weight; count those at the second-best weight.
def main():
    pass

if __name__ == "__main__":
    main()

Evaluation criteria. - Agrees with a hand count on small graphs. - The distinctness convention is documented and consistent. - Handles the non-unique-MST case explicitly.


Benchmark Task — Naive vs LCA path-max

Problem. Benchmark two second-best-MST implementations on random connected graphs of increasing size: - (a) Naive: path walk per non-tree edge, O(E·V). - (b) LCA: binary lifting, O((V+E) log V).

Generate a connected graph (a random spanning path plus random extra edges), build it once, and time only the second-best computation (exclude graph generation and MST build if you wish, but be consistent). Report mean time over at least 5 runs per size. Use the same seed across languages so inputs match.

Sizes to test. (V, E) ∈ {(1000, 5000), (5000, 20000), (20000, 80000), (50000, 200000)}. The naive method may be skipped for the largest size if it would exceed a few minutes — note that in your report.

Starter — Go.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func genGraph(n, m int, seed int64) [][3]int {
    r := rand.New(rand.NewSource(seed))
    edges := make([][3]int, 0, m)
    for i := 0; i < n-1; i++ {
        edges = append(edges, [3]int{i, i + 1, 1 + r.Intn(1000)})
    }
    for len(edges) < m {
        u, v := r.Intn(n), r.Intn(n)
        if u != v {
            edges = append(edges, [3]int{u, v, 1 + r.Intn(1000)})
        }
    }
    return edges
}

func naiveSecondBest(n int, edges [][3]int) int  { /* TODO */ return 0 }
func lcaSecondBest(n int, edges [][3]int) int    { /* TODO */ return 0 }

func timeIt(f func() int) float64 {
    start := time.Now()
    f()
    return float64(time.Since(start).Microseconds()) / 1000.0
}

func main() {
    type sz struct{ v, e int }
    sizes := []sz{{1000, 5000}, {5000, 20000}, {20000, 80000}, {50000, 200000}}
    fmt.Println("V        E         naive_ms      lca_ms")
    for _, s := range sizes {
        edges := genGraph(s.v, s.e, 42)
        // TODO: average over >=5 runs; skip naive for the largest if too slow
        _ = edges
    }
}

Starter — Java.

import java.util.*;

public class Benchmark {
    static int[][] genGraph(int n, int m, long seed) {
        Random r = new Random(seed);
        List<int[]> es = new ArrayList<>();
        for (int i = 0; i < n - 1; i++) es.add(new int[]{i, i + 1, 1 + r.nextInt(1000)});
        while (es.size() < m) {
            int u = r.nextInt(n), v = r.nextInt(n);
            if (u != v) es.add(new int[]{u, v, 1 + r.nextInt(1000)});
        }
        return es.toArray(new int[0][]);
    }

    static int naiveSecondBest(int n, int[][] edges) { /* TODO */ return 0; }
    static int lcaSecondBest(int n, int[][] edges)   { /* TODO */ return 0; }

    public static void main(String[] args) {
        int[][] sizes = {{1000, 5000}, {5000, 20000}, {20000, 80000}, {50000, 200000}};
        System.out.println("V        E         naive_ms      lca_ms");
        for (int[] s : sizes) {
            int[][] edges = genGraph(s[0], s[1], 42);
            // TODO: average over >=5 runs; print row; skip naive for largest if slow
        }
    }
}

Starter — Python.

import random, time
from typing import List, Tuple

def gen_graph(n: int, m: int, seed: int) -> List[Tuple[int, int, int]]:
    r = random.Random(seed)
    edges = [(i, i + 1, 1 + r.randrange(1000)) for i in range(n - 1)]
    while len(edges) < m:
        u, v = r.randrange(n), r.randrange(n)
        if u != v:
            edges.append((u, v, 1 + r.randrange(1000)))
    return edges

def naive_second_best(n, edges) -> int:
    # TODO: O(E*V) path walk
    return 0

def lca_second_best(n, edges) -> int:
    # TODO: O((V+E) log V) binary lifting
    return 0

def time_it(fn) -> float:
    t = time.perf_counter()
    fn()
    return (time.perf_counter() - t) * 1000.0

def main():
    sizes = [(1000, 5000), (5000, 20000), (20000, 80000), (50000, 200000)]
    print("V        E         naive_ms      lca_ms")
    for v, e in sizes:
        edges = gen_graph(v, e, 42)
        # TODO: average >=5 runs; skip naive for the largest if too slow
        _ = edges

if __name__ == "__main__":
    main()

Evaluation criteria. - Both methods produce the same second-best weight on every size (correctness gate before timing). - The LCA method scales smoothly; the naive method grows roughly with E·V and becomes impractical by (20000, 80000). - Same seed yields the same graph across languages (verify by printing the MST weight). - Report mean over ≥ 5 runs, excluding graph generation from the timed region. - Writeup: at which size does naive cross 1 second, and which language showed the widest naive/LCA gap, and why (interpreter overhead vs constant factors).