The Minimum Spanning Tree (MST) problem


Overview

 

The Minimum Spanning Tree (MST) problem is a classic computer science problem.

We will study the development of algorithmic ideas for this problem, culminating with Chazelle's O(m α(m,n))-time algorithm, an algorithm that easily meets the "extreme" criterion.

A preview:

Our presentation will pull together material from various sources - see the references below. But most of it will come from [Eisn1997], [Pett1999], [CS153], [Weis2007].


Background: what is a graph?

 

Informal definition:

 

Formally:

 

Depicting a graph:

 

Graph conventions:

 
Exercise: If we disallow multiple edges and self-loops, what is the maximum number of edges in an undirected graph with n vertices? What is this number in order-notation?
 

Definitions (for undirected graphs):

  • Neighbors: Two vertices are neighbors (or are adjacent) if there's an edge between them.

  • Paths:
    • Path: a sequence of vertices in which successive vertices are adjacent.
    • A simple path does not repeat any vertices (and therefore edges) in the sequence.
    • A cycle is a simple path with the same vertex as the first and last vertex in the sequence.

  • Connectivity: two vertices are connected if there is a path that includes them.
     
    Weighted graph:
    • Sometimes, we include a "weight" (number) with each edge.
    • Weight can signify length (for a geometric application) or "importance".
    • Example:

     
    Euclidean graph:
    • The vertices are points in the plane.
    • Edges are implied (all pairs) with Euclidean distance as weights.
    • Note: one can define a version for d-dimensions.
     
    Trees:
    • A tree is a connected graph with no cycles.

    • A spanning tree of a graph is a connected subgraph that is a tree.

     

    Data structures for graphs:

    • Adjacency matrix.
      • Key idea: use a 2D matrix.
      • Row i has "neighbor" information about vertex i.
      • Here, adjMatrix[i][j] = 1 if and only if there's an edge between vertices i and j.
        adjMatrix[i][j] = 0 otherwise.
      • Example:

        
              0 1 1 0 0 0 0 0
              1 0 1 0 0 0 0 0
              1 1 0 1 0 1 0 0
              0 0 1 0 1 0 1 0
              0 0 0 1 0 0 1 0 
              0 0 1 0 0 0 1 1
              0 0 0 1 1 1 0 0
              0 0 0 0 0 1 0 0
              

        Note: adjMatrix[i][j] == adjMatrix[j][i] (convention for undirected graphs).

    • Adjacency list.
      • Key idea: use an array of vertex-lists.
      • Each vertex list is a list of neighbors.
      • Example:

      • Convention: in each list, keep vertices in order of insertion
        add to rear of list

    • Both representations allow complete construction of the graph.

    • Advantages of matrix:
      • Simple to program.
      • Some matrix operations (multiplication) are useful in some applications (connectivity).
      • Efficient for dense (lots of edges) graphs.

    • Advantages of adjacency list:
      • Less storage for sparse (few edges) graphs.
      • Easy to store additional information in the data structure.
        (e.g., vertex degree, edge weight)

    • Implementations for MST use adjacency lists.

    Defining the MST problem

     

    With this background, it's going to be easy to state the MST problem: find the spanning tree of minimum weight (among spanning trees):

    • Input: a weighted, connected graph.

    • Output: find a sub-graph such that
      • The sub-graph is a connected tree.
      • The tree contains all graph vertices.
        it's a spanning tree.
      • The tree weight is defined as the sum of edge-weights in the tree.
      • The tree weight is the least among such spanning trees.

    Exercise:

    • "Eyeball" the weighted graph below and find the minimum spanning tree, and the shortest path from vertex 0 to vertex 6.

    • Try the example given to you in class.

    Some assumptions and notation for the remainder:

    • Let n = |V| = number of vertices.
    • Let m = |E| = number of edges.
    • We will assume unique edge weights. If this is not true, some arbitrary tie-breaking rule can be applied.
            It simplifies analysis.


    Minimum Spanning Trees: Two Key Observations

    The Cut-Edge Observation:

    • Consider a partition of the vertices into two sets.

    • Suppose we re-draw the graph to highlight edges going from one set to the other: (without changing the graph itself)

    • Cut-edge: an edge between the two sets.

    • Consider the minimum-weight edge between the sets.
      (Assume the edge is unique, for now).

    • The minimum-weight edge must be in the minimum spanning tree.

    Exercise: Why?

    Prim's algorithm (a preview):

    • Start with Set1 = {vertex 0}, Set2 = {all others}.

    • At each step, find an edge (cut-edge) of minimum-weight between the two sets.

    • Add this edge to MST and move endpoint from Set2 into Set1 .

    • Repeat until Set1 = {all vertices}.

    The Cycle Property:

    • First, note: adding an edge whose end-points are in the MST will always cause a cycle.

    • Consider adding an edge that causes a cycle in current MST.

    • Suppose the edge has weight larger than all edges in cycle
      no need to consider adding edge to MST.

    • Therefore, if edges are added in order of (increasing) weight
      no need to consider edges that cause a cycle.

    Kruskal's algorithm (a preview):

    • Sort edges in order of increasing weight.
    • Process edges in sort-order.
    • For each edge, add it to the MST if it does not cause a cycle.


    Kruskal's Algorithm

    Key ideas:

    • Sort edges by (increasing) weight.

    • Initially, each vertex is a solitary sub-graph (MST-component).

    • Start with small MST-component's and grow them into one large MST.

    • Need to track: which vertex is in which component (i.e., set).

    Exercise: Show the steps in Kruskal's algorithm for this example:

    Implementation:

    • High-level pseudocode:
      
      Algorithm: Kruskal-MST (G)
      Input: Graph G=(V,E) with edge-weights.
      
      1.   Initialize MST to be empty;
      2.   Place each vertex in its own set;
      3.   Sort edges of G in increasing-order;
      4.   for each edge e = (u,v) in order
      5.       if u and v are not in the same set
      6.           Add e to MST;
      7.           Compute the union of the two sets;
      8.       endif
      9.   endfor
      10.  return MST
      
      Output: A minimum spanning tree for the graph G.
        

    • Now, let's use a union-find algorithm and explain in more detail:
      
      Algorithm: Kruskal-MST (adjMatrix)
      Input: Adjacency matrix: adjMatrix[i][j] = weight of edge (i,j) (if nonzero)
      
      1.   Initialize MST to be empty;
      
           // Initialize union-find algorithm and 
           // place each vertex in its own set. 
      2.   for i=0 to numVertices-1
      3.       makeSingleElementSet (i)
      4.   endfor
      
      5.   sortedEdgeList = sort edges in increasing-order;
      
      6.   Initialize treeMatrix to store MST;
      
           // Process edges in order. 
      7.   while sortedEdgeList.notEmpty()
      8.       edge (i,j) = extract edge from sortedEdgeList.
               // If edge does not cause a cycle, add it. 
      9.       if find(i) != find(j)
      10.          union (i, j)
      11.          treeMatrix[i][j] = treeMatrix[j][i] = adjMatrix[i][j]
      12.      endif
      13.  endwhile
      
      14.  return treeMatrix
      
      Output: adjacency matrix represention of tree.
        

    Analysis: (adjacency list)

    • To place edges in list: simply scan each vertex list once.
      O(m) time.

    • Sorting: O(m log(m)).

    • O(log(n)) per union or find.

    • Total: O(m)+ O(m log(m)) + O(m log(n))
      O(m log(m))
      O(m log(n))


    Prim's Algorithm

    Key ideas:

    • Start with vertex 0 and set current MST to vertex 0.

    • At each step, find lowest-weight edge from an MST vertex to a non-MST vertex and add it to MST.

    • To record the weight of each edge, we will associate edge weight with the vertex that's outside the MST.

    • We will associate a "priority" with each vertex:
      • Priority(v) is defined only for v not in current MST.
      • Intuitively, priority(v) = cost of getting from current MST to v.
      • For example, consider node A with priority(A) = 3.1

      • priority(A) changes to 1.1 after B is added.

    Example:

    • Initially, place vertex 0 in the MST and set the "priority" of each vertex to infinity.

    • Explore edges from current MST: (0, 1) and (0, 2).

    • Pick lowest-weight edge (0, 1) to add
      same as selecting lowest-priority vertex (vertex 1)

    • Explore edges from newly-added vertex: (1,3), (1,2)


      That's why we associate edge-weights with vertices
      only the edges from the new vertex add new information at each step.

    • Pick vertex with lowest priority (vertex 3) and explore its edges:

    • Continuing, we add vertices 2, 4, 6, 5 and 7:

    Implementation:

    • High-level pseudocode:
      
      Algorithm: Prim-MST (G)
      Input: Graph G=(V,E) with edge-weights.
      
      1.   Initialize MST to vertex 0.
      2.   priority[0] = 0
      3.   For all other vertices, set priority[i] = infinity
      4.   Initialize prioritySet to all vertices;
      5.   while prioritySet.notEmpty()
      6.       v = remove minimal-priority vertex from prioritySet;
      7.       for each neighbor u of v
      9.           w = weight of edge (v, u)
      8.           if w < priority[u]
      9.               priority[u] = w        // Decrease the priority. 
      10.          endif
      11.      endfor
      12.  endwhile
      
      Output: A minimum spanning tree of the graph G.
        

    Exercise: Consider line 6 in the algorithm above. How long does it take to execute?
    Analysis:

    • Each edge is added once to the priority set.

    • There are O(n) removals.

    • Since each edge is processed once, there are at most O(m) priority adjustments.

    • Thus, if a removal takes time r(n) and and a priority-decrease takes time d(n)
            O(n r(n) + m d(n)) time overall.

    Exercise: What is the running time when using a list for the priority set?

    • A sorted list.
    • An unsorted list.


    Enter the data structures, part I: the binary heap

    What is a priority queue?

    • Keys or key-value pairs are stored in a data structure.

    • The operations usually supported are:
      • Insert: insert a new key-value pair.
      • Delete: delete a key-value pair.
      • Search: given a key, find the corresponding value.
      • Find minimum, maximum among the keys.

    A priority queue with the decreaseKey operation:

    • For convenience, we will combine minimum and remove (delete) into a single operation: extractMin
    • We would like to change a key while it's in the data structure
      location in data structure also needs to be changed.

    • Sorted linked list:
      • Potentially O(n) time for decreaseKey
      • O(1) time for extractMin

    • Unsorted array:
      • Potentially O(n) time for extractMin
      • O(1) time for decreaseKey

    • Can we do better?
      • O(log(n)) time for extractMin?
      • O(log(n)) time for decreaseKey?

    Implementing a priority queue with a binary heap:

    • Recall extractMin operation (Module 2):

      • Height of heap is O(log(n)) always.
      • Each extractMin results in changes along one tree path.
        O(log(n)) worst-case.

    • Implementing decreaseKey:

      • Simply decrease the value and swap it up towards root to correct location.
        O(log(n)) worst-case.

    Using a priority queue with Prim's algorithm:

    
    Algorithm: Prim-MST (adjList)
    Input: Adjacency list: adjList[i] has list of edges for vertex i
    
         // Initialize vertex priorities and place in priority queue. 
    1.   priority[i] = infinity
    2.   priorityQueue = all vertices (and their priorities) 
         // Set the priority of vertex 0 to 0. 
    3.   decreaseKey (0, 0)
    
    4.   while priorityQueue.notEmpty()
             // Extract best vertex. 
    5.       v = priorityQueue.extractMin()
             // Explore edges going out from v. 
    6.       for each edge e=(v, u) in adjList[v]
    7.           w = weight of edge e;
                 // If there's an edge and it's not a self-loop. 
    8.           if priority[u] > w
                     // New priority. 
    9.               priorityQueue.decreaseKey (u, w)
    10.              predecessor[u] = v
    11.          endif
    12.      endfor
    13.  endwhile
    
         // ... build tree in adjacency list form ... (not shown) 
    
    Output: Adjacency list representation of MST
      
    Analysis:
    • Recall: running time of Prim's algorithm depends on n extract-min's and m decrease-key's.
            O(n log(n) + m log(n))
            = O(m log(n))

    Boruvka's algorithm

     

    It turns out that neither Prim nor Kruskal were the first to address the MST problem [Grah1985]:

    • First, note: Kruskal's algorithm was published in 1956, Prim's in 1957.

    • A solution to the problem was first published in 1926 by Czech mathematician Otakar Boruvka (1899-1995) [WP-2].

    • The problem is itself thought to have been first addressed in 1938 by G.Choquet.

    • The essence of Prim's algorithm first appeared in a 1930 paper by V.Jarnik written in Czech.

    • Dijkstra re-discovered the algorithm in 1959.
            Some books now call this the Jarnik-Prim-Dijkstra algorithm.

    • Kruskal and Prim both worked at AT&T Bell Labs, though neither knew of each other until later.
     

    Key ideas in Boruvka's algorithm:

    • Start with each vertex as its own tree.

    • At each Boruvka phase:
      • Identify the cheapest edge leaving each tree.
      • Merge the trees joined by these edges.

    • Why does this work?
      • The cheapest edge going out from a tree T is the cheapest edge between T and "the outside world"
              By the cut-edge property, it must be in the MST.
     

    Boruvka's algorithm with edge contractions:

    • What is an edge contraction?
      • Merge the two nodes on either side into one new node.

      • Keep cheapest edge among multiple edges to neighbors.

    • The algorithm with contractions:
      
      Algorithm: Boruvka (G)
      Input: Graph G = (V, E)
      1.   Create graph F = (V, φ)     //No edges in F. 
      2.   while F not connected
      3.      for each component in F
      4.         add cheapest outgoing edge to MST
      5.      endfor
      6.      Contract added edges in F and G
      7.   endwhile
           

    • Example:

     

    Analyis:

    • Let Vi be the vertex set after the i-th phase.

    • Each contraction reduces the vertex set by at least half
            |Vi+1| ≤ ½ |Vi|.
            There are at most O(log n) phases.

    • In each phase, we examine at most O(m) edges
            O(m log(n)) time overall.

    • This is as good as Prim+BinaryHeap.


    Enter the data structures, part II: the fibonacci heap

    First, the precursor to the fib-heap: the binomial heap [Vuil1978].

    • The binomial heap is a radical departure from standard data structures in one way:
            Instead of one tree, it uses a collection of trees.

    • To avoid confusion, we'll call the whole structure a binomial queue.

    • A binomial queue is:
      • A collection of binomial trees.
      • Each binomial tree in the queue is min-heap-ordered, with nodes that can have more than two children, and has a certain structure (see below).

    • Recall that min-heap order means: each parent is "less" than its children.

    • A binomial tree Bk of order k is a heap-ordered tree defined recursively:

      • B0 is a node by itself.
      • Bk is the tree you get by taking two Bk-1 trees and making one a right child of the other's root.

    • A queue can have at most one tree of each order.
            e.g., at most one B3 tree.

    • The tree merge operation:
      • Two Bk trees can be joined as above to make a Bk+1 tree.
      • To maintain min-order, the lesser Bk root becomes the Bk+1 root.

    • The queue merge operation:
      • Two binomial queues can be merged using something like binary addition (with carryover).
      • Example: suppose we have two queues

    • Insert operation:
      • Make a new B0 tree and merge that into the existing queue.

    • A min pointer:
      • The true minimum can be in any one of the trees.
      • Keep an external pointer to the running minimum:

    • Extract-min operation:
      • First, here's another view of a binomial queue (useful for extract-min):

      • Remove min node (a root of some Bk).
      • This will leave child trees B0, B1 ... Bk-1.
      • Merge each of these into the rest of the queue.

    • Decrease-key operation:
      • Decrease value and make heap-adjustments within the tree.
              Same as in binary heap.

    Exercise: Show that the binomial tree Bk has 2k nodes. Then argue that there are at most O(log n) trees.
    Binomial queue analysis:

    • At first it seems we have the following:
      • Insert: O(log n) because of all the "carry" operations that might occur.
      • Extract-min: O(log n) because all the subtrees need to be added (a queue-merge operation).
      • Decrease-key: O(log n) as in binary heap.

    • However, consider this:

      • When a carry ripples, it also leaves behind 0's.
              Reduces the need for carries in the future.
              Over time, the actual number of carries may not be high.

    • We want to account for the average cost of an operation over time
            Called amortized analysis.
      • This is still a worst-case (not average).
      • It's really a "worst possible average cost".

    • The idea of a potential function:
      • In physics, "work done" results in storing potential energy.
      • The analog here: "work done" in carries stores "good" zeroes.

    • Amortized analysis of insert-operation:
      • Let Pi = potential = number of 0's after i-th insert.
      • Let Pi - Pi-1 = change in potential.
      • Let Ci = true cost of i-th insert.
      • Then
              Ci = 1 + Pi - Pi-1
        i.e., cost = cost-of-new-item  +   ripple-carry
      • Hence, over n operations
              Σ >Ci = n + Pn - P0
              ≤ 2n
      • Thus, average cost is
              (1/n) Σ Ci = O(1).

    • Note:
      • Above we equated "potential" with "good" zeroes.
      • We could just as easily equate "potential" with "bad" ones.
              Exactly the same analysis but with change-of-sign for potential.

    • Unfortunately: for extract-min and decrease-key, the amortized time is still O(log n).
     

    The Fibonacci heap [Fred1987]: an overview

    • The fibonacci heap is a modification of the binomial such that
      • Decrease-key takes O(1) amortized.
      • Extract min takes O(log n) amortized.

    • Again, we switch to calling it a fibonacci queue because whole thing is a collection of trees.

    • The fib-queue is based on two ideas:
      1. When a decrease-key occurs, don't repair tree.
              Instead, "cut out" the subtree where the decrease-key occurs.
      2. Dispense with the strict binomial-tree properties:
        • Dispense with: at most one Bk tree.
        • Dispense with: recursive structure of each Bk.
     

    More detail:

    • Define
            rank (node) = number of children of the node
            rank (tree) = rank (tree's root)

    • Maintain a list of trees with min-pointer, as in binomial queue.

    • When we merge trees, we will merge two trees of equal rank and create a one-rank-higher tree.

    • Cascading cut:
      • When doing a decrease-key, if heap-order is violated, cut-out node along with subtree:

      • Then, "mark" the parent.
      • A parent with two marks is itself cut-out (as a single-node tree):

      • The cutting cascades upwards as long as marked parents need to be cut out.

    • Lazy-merge:
      • When a cut tree is added to main list, simply concatenate
              Do not perform a merge.
      • A merge is performed only during extract-min.

    • Decrease-key operation:
      • As described above: cut out if needed and cascade backwards.
      • Note: O(1) time except for cascades.
              Will turn out to be O(1) amortized.

    • Extract-min operation:
      • Remove node as in binomial queue.
      • Perform a full merge:
        • Start by "adding" the single nodes and work towards adding higher-rank trees.
        • Algorithm:
          
                       for r = 0 to M
                           while # rank-r trees > 2
                               merge into a rank r+1 tree
                           endwhile
                       endfor
                       
        • Here, M = the largest rank possible (which is O(log n)).

    Exercise: Look up Fibonacci numbers. How are they defined recursively? Prove the following:

    • F0 + ... Fn-2 = Fn - 1.
    • Fk = O(φk), where φ is the golden ratio.
     

    Analysis:

    • First, observe:
      • If Ci is the i-th youngest child of a node N, then rank(Ci) ≥ 2
      • Proof:
        • When Ci is joined with N (during a merge), N had children C1, C2, ... Ci-1.
        • Nodes are joined only if they have the same rank.

        • After merging, it can lose children, but at most one
                Else, it would itself be cut.
        • Thus, it has at least i-2 children.

    • Next: a node of rank r has O(Fr) descendants (not just children).
      • Let Tr be the smallest tree (fewest nodes) of rank r.
      • Then size of T0 = 1 (single node).


              |T0| = F1 and |T1| = F2, the second two Fibonacci numbers.

      • Now consider a rank r tree and order children by age:

      • To make the smallest possible tree, assume all children are minimal subtrees.
              |Tr| ≥ 1 + |T0| + ... + |Tr-1| = O(Fr).
      • But Fr = O(φr).
      • Thus, r = O(log|Tr|).
      • Thus, with at most n nodes, the rank is at most O(log n).

    • Define the "badness" potential as:
            potential = #trees + #marked-nodes

    • For decrease-key:
      • Let C = length of cascade.
      • Then, actual cost = C.
      • We create O(C) new trees, but reduce marked nodes by O(C).
              Total change in potential is O(1).
      • Amortized cost: O(1).

    • For extract-min:
      • Does not change # of marked nodes.
      • Total # trees is like the total number of 1's in binary addition.
              Same change in potential over a number of operations.
      • Total time is bounded by O(log n), the number of trees.
      • Amortized cost: O(log n).

    • Note: we have glossed over some details in the analysis. See the original paper or the description in [Weis2007].
     

    Back to MST:

    • Recall: with Prim's algorithm
      • n extract-min's.
      • m decrease-key's.

    • Thus, for Prim + Fib-heap
            O(n log(n) + m) bound.

    • Compare with:
      • Kruskal: O(m log(n))
              Dominated by O(m log(m)) sort.
      • Prim + Binary-heap: O(m log(n))
              O(n log(n)) extract-min's + O(m log(n)) decrease-key's.
      • Boruvka: O(m log(n))
              From O(log n) phases, each requiring O(m) edge manipulations.
     

    A simple improvement with Boruvka's algorithm:

    • Consider the following algorithm:
        Perform O(loglog n) phases of Boruvka (with contractions).
              This takes O(m loglog(n)) time.
      1. Apply Prim to the resulting graph.
      2. Put together all MST edges into single MST.

    • Now, we start with n components (the original nodes as one-node trees).
      • After phase 1, there are at most n/2 components.
      • After phase 2, at most n/22 components.
      • ... after phase k, at most n/2k components.

    • Thus, if we do O(loglog n) phases, we'll have at most n/log(n) components.
            After contraction, this gives a graph with n/log(n) vertices.

    • Applying Prim + Fibonacci on the last graph:
      • O(m + n/log(n) log(n)) = O(m + n) time.
      • Thus, overall: O(m loglog(n)) time.
              Fastest so far.


    Fredman and Tarjan's Algorithm

    Fredman and Tarjan didn't stop with inventing the Fibonacci heap [Fred1987].

    • Consider Prim+Fib again:
            O(n log(n)) for n extract-min's and O(m) for m decrease-key's.
      • Even if extract-min can't be improved, we can decrease n
              Put fewer nodes into the heap.
      • Suppose we try to control the size of the heap?
      • Recall Boruvka: separate components with their own MST's join together eventually
              The heaps of these components could be kept small.
      • But we don't know how big these components could get.

    • Key ideas:

      • Fix the heap size.
      • If a component's heap gets too big, abandon growing the component for now.
      • Start another component.

    • Fix the heap limit as k (for now).

    • We will redefine a Boruvka phase as follows:
          1.  Grow trees until all heaps reach k-limit.
          2.  Contract all trees to get new graph G'
          3.  Set G = G' and repeat 
          

    • Another insight:
      • The graphs at successive phases (after contraction) get smaller.
              We can use different k values in each phase.
      • The more nodes we have, the smaller we should make k.
              Because, the more the risk of encountering large heaps.

    • Let ni be the number of nodes after (i-1)-st phase (contraction).

    • Let ki be the limit for phase i.
            Define ki = 22m/ni

    • Then, the running time for a single phase takes
            O(m + ni log(ki))
            = O(m + ni log(22m/ni))
            = O(m) .

    • Next, we need to bound the number of phases.

    • To do this, let's estimate the number of trees in phase i:
      • Since we stop trees when heap-size is ki, each tree "touches" ki edges (which are in the heap).

      • Thus
              # trees x ki2m
              # trees ≤ 2m / ki
              ni+12m / ki (because # trees = # nodes at next level of contraction)
              ki+1 = 22m/ni+1 ≥ 2ki.
              The ki's are growing.

    • The ki's can only grow as large as n.

    • Definition: log(i)(n) is i- repeated logs of n:

    • Definition: log*(n) is the number of times you need to take repeated log's to reach 1.

    • Now, let's work backwards from n.
            ki ≥ n
            if log(i)ki ≥ log(i)n
            which will be true if log(i)2m/ni ≥ log(i)n
            which will be true if 2m/ni ≥ log(i)n
            which will be true if 2m/n ≥ log(i)n
            which will be true if m/n ≥ log(i)n

    • Definition: Define β(m,n) = mini log(i)(n) ≤ m/n.

    • Thus, there are at most β(m,n) phases.

    • Note: β(m,n) ≤ log*(m/n).
    So, is this better?
    • The comparison to make is:
      • Prim+Fib: O(nlog(n) + m).
      • Boruvka+Prim+Fib: O(m loglog(n)).
      • Fredman-Tarjan: O(m β(m,n)).

    • Obviously, Fredman-Tarjan is better than Boruvka+Prim+Fib.

    • What about Prim+Fib?
      • Clearly, when m < O(n log n), Fredman-Tarjan is better.
      • It is possible [Eisn1997] to show that for n>16, m β(m,n) = O(nlog(n) + m).

    • Thus, overall, Fredman-Tarjan is the best so far.


    An improvement to Fredman-Tarjan

     

    Gabow et al (including Tarjan) [Gabow1986] found a way to improve the Fredman-Tarjan algorithm:

    • Recall that Fredman-Tarjan takes O(mβ(m,n)) time.
      • Suppose one could reduce the "work" per phase to O(m/p) for some judiciously chosen p.
      • Then, we would have
              total work = O(m/p) per-phase x p phases
              = O(m) overall.

    • Now each phase does a bunch of decrease-keys.
      • We are counting this as O(m) decrease-keys.
      • Each decrease-key takes O(1) time (Fib-heap)
              Can't improve on this.
      • Can we reduce the number of decrease-key operations?

    • The key idea (in rough):
      • Recall: when do we need decrease-key?

      • Place edges into groups called "packets"

      • Each packet is partially-sorted once by edge-weight so that cheapest packet is known.
              One-time cost of m log(p).
      • Apply decrease-key to only "packet leaders."

    • Total time: O(mlog(p) + m).

    • Choose p=β(m,n).
            Total time: O(m log(β(m,n))).


    Numbers big and small

     

    Let's try and get a sense of the size of β(m,n):

    • The worst case is when m = O(n2).

    • Suppose n = number of atoms in the known universe (estimated).
      • n = 1080 = 2266.
              m = 2534.
              m/n = 2266.
              β(m,n) = 9.
              logβ(m,n) = 4.

    • Thus, logβ(m,n) = 4 for all practical purposes in the running time of O(mlogβ(m,n)).

    Exercise: Write down (legibly) the largest number you can think of on a square-inch of paper.
     

    Ackermann's function [Corm2006][WP-3]:

    • Consider the function
            g(i) = 22    when i=1
            g(i) = 2g(i-1)    when i>1

    • Note:
      • log(i)(n) is the inverse of g(i).
      • Such a use of repeated exponentiation is called a tetration.

    • Clearly, g(i) grows ridiculously fast.

    • What if the tetration amount itself grows ridiculously fast?

    • Now consider the function h(i) defined as:

    • Ackermann's function is similar, even if its definition looks innocuous:
            A(1, j) = 2j    for j≥1.
            A(i, 1) = A(i-1, 2)    for i≥2.
            A(i, j) = A(i-1, A(i,j-1))    otherwise.

    • Ackermann (and co-authors) defined this to create an example of a function that is recursive but not primitive-recursive.

    • The inverse is defined as follows:
            α(m,n) = mini such that A(i, m/n) > log(n).

    • To get a sense of its size, A(4,1) > > 1080, the number of atoms in the universe.

    • Thus, α(m,n) < 4 for all practical m,n.

    • But theoretically, one can show that, for large enough m,n
            α(m,n) < β(m,n).

    • Thus, an MST algorithm that takes time O(mα(m,n)) would be an improvement over Gabow et al's algorithm.
     

    On the topic of big numbers:

    • Consider an integer N and Turing machines with at most n rules.
      • Of these, consider those that halt.
      • Of those that halt, identify the one that takes the longest
              Takes the most steps.

    • Define BB(n) = number of steps taken by the longest halting Turing machine with at most n rules.

    • Note: BB = "Busy Beaver".

    • It is known that:
      • BB(1) = 1.
      • BB(2) = 6.
      • BB(3) = 21.
              Laborious construction and checking by hand.
      • BB(4) = 107.

    • So far, not so impressive.

    • But, consider BB(5)
            Not known, but it's been shown that BB(5) > 8 x 1015.


    The algorithms of Chazelle and Pettie

     

    History:

    • In 1997, Bernard Chazelle [Chaz1997] put together the basic ideas in his new approach to MST's that resulted in an O(m α(m,n)log(α(m,n))) algorithm.

    • In 1999, both Chazelle [Chaz2000b] and Seth Pettie [Pett1999] independently improved this to O(m α(m,n)).

    • In 2000, Pettie and Ramachandran [Pett2000] showed that, under reasonable assumptions, O(m α(m,n)) is optimal.

    Because the algorithms are quite complicated (extreme!), we will only sketch out the key ideas. There are two big innovations:

    • A radically new idea for a heap data structure: the soft heap
      • Achieves extract-min and decrease-key in O(1) amortized time.
      • But insertion takes more time: O(log(1/ε)), where ε is a parameter.
      • The radical departure: the heap allows its contents to become corrupted
              The value actually changes (increases) and wrong results get reported back.
      • At most εn items are corrupted.
      • This is exploited in the algorithm to tradeoff speed against the "repair" needed to account for corruption.

    • A somewhat radically different approach to using Boruvka-phases.
      • In all earlier algorithms: Boruvka phases were used (sometimes with size-parameters) to build the tree bottom-up.
              The contractions result in a hiearchy of components
      • The "contraction tree" was useful in analysis, but not used in the algorithm.
      • Here, instead, a "contraction tree" is first constructed according to pre-specified size constraints.
      • The actual MST construction uses the tree.
              The tree is used to guide the MST-building process.
      • MST construction itself occurs in separate phases.
     

    Key ideas:

    • Part I: building the contraction tree

      
            1.   G = original graph
            2.   while G not a single vertex
            3.      Identify sub-graphs according to size parameters
            4.      for each subgraph
            5.         Identify "bad" edges
            6.         Create node in contraction tree
            7.         Place sub-graph nodes as children
            8.         Contract into single vertex of G'
            9.      endfor
            10.     G = G'
            11.  endwhile
            12   return contraction tree
            
      This results in the contraction tree:

    • Note:
      • Along the way (above), identify so-called "bad" edges.
              These are edges whose corrupted values might affect the MST.
      • Turns out, a bottom-up construction takes too long
              The tree is built in post-order to limit the number of "active" sub-graphs at any time to the tree-height.
      • During construction, some edge weights in the soft-heaps get corrupted.
      • Some corrupted edges don't matter to the MST.
              Those that do are called "bad".

    • Part 2: Recurse down the tree and build an MST using non-bad edges.
      • This is done in using Boruvka-phases, but limited by the already-chosen sub-graphs and contractions.

    • Part 3: Add back the bad edges (with their original costs) and recurse again.
     

    Analysis:

    • The height and sub-graph sizes of the contraction tree are under our control (as in Fredman-Tarjan).

    • By choosing these carefully, the O(mlogα(m,n)) time-bound is achieved.
     

    Optimality:

    • It can be shown that the above algorithm takes O(m) time if m/n > O(log(3)(n))
            i.e., it fails O(m) only for extremely-sparse graphs (almost tree-like)

    • Consider this radical idea:
      • Compute MST's of all possible graphs with r vertices.
      • Store these in a pre-computed look-up table.
      • There are at most 2r2 such graphs.
              (From all possible adjacency matrices)
      • If r can be kept small, one can exploit this for optimality.
              Note: r = O(log(3)(n)).

    • How?
      • Contract graph to reduce #vertices by factor of O(log(3)(n)).
              Desired density is achieved.
      • Pick sub-graphs whose optimality can be immediately determined from lookup table.
              No cost to finding MST of these subgraphs.
      • This increases the edge density to above O(log(3)(n)).
      • Now apply O(m) algorithm to remainder and join MST's.

    • Two problems to address:
      1. How find all possible r-sized subgraphs with weights?
              Not the same as all possible r x r binary matrices.
      2. How to make sure that this takes reasonable time?

    • Problem 1: solve using decision-tree approach

      • Instead of "all possible sub-graphs", compute all possible "decision trees on such sub-graphs".
      • This is a well-known approach (in the theory world).

    • Problem 2: not solved.
      • Decision-tree construction time can be bound.
      • But execution time depends on actual values.
              Execution-time can't be known (or tightly bound) ahead of time.
      • But, it can be shown, that it's optimal (in execution time).
              Resulting algorithm is optimal (even if we can't pin down the running time).


    Summary

     

    Let's step back and look at the history one more time:

    • Otakar Boruvka's algorithm in 1926.
            O(m log(n)) time (although he probably didn't know it).
            From O(log n) phases, each requiring O(m) edge manipulations.

    • Jarnik in 1930: essentially Prim's algorithm
            O(n2) because they didn't know of heaps at that time.

    • Kruskal in 1956: O(m log(n))
            Dominated by O(m log(m)) = O(m log(n)) sort.

    • Prim in 1957: re-discovered Jarnik's algorithm
            Still O(n2).

    • Dijkstra in 1959: re-discovered Jarnik-Prim algorithm.

    • The use of binary heaps: (1960's?)
      • Prim + binary-heap: O(m log(n))
              O(n log(n)) extract-min's + O(m log(n)) decrease-key's.
              No faster than Boruvka's original algorithm (analysed in modern times).

    • Improvement's to Boruvka, independently by Yao, and Cheriton-Tarjan in 1975-76.
            O(m loglog(n))

    • Binomial heaps in 1978.

    • 10 year gap.

    • Fredman-Tarjan's Fibonacci heap in 1987:
            Direct improvement of Prim's algorithm to O(nlog(n) + m).

    • Fredman-Tarjan algorithm in 1987: control heap growth
            O(mβ(m,n)).

    • Gabow et al's improvement soon after:
            O(m logβ(m,n)).

    • 10 year gap.

    • Chazelle's soft-heap approach in 1997
            O(m α(m,n)logα(m,n)).

    • Chazelle's and Pettie's improvements to Chazelle's approach in 1999:
            O(m α(m,n)).

    • Pettie and Ramachandran's decision-tree algorithm and proof of optimality in 2000.
            Exact time unknown but at most O(m α(m,n)).
     

    End of story? Various other interesting variations

    • An O(m) randomized algorithm by Karger et al. in 1995.

    • Simpler versions of the soft heap, e.g., Kaplan et al in 2009.

    • The run-relaxed heap by Driscoll et al (which improves decrease-key to O(1) worst-case).

    • Faster (O(n logn) algorithms for Euclidean MST's.


    References and further reading

     


    [WP-1] Wikipedia entry on Minimum Spanning Trees.
    [WP-2] Wikipedia entry on Otakar Boruvka.
    [WP-3] Wikipedia entry on Ackermann's function.
    [Aaro] S.Aaronson. Who can name the biggest number?
    [Boru1926] O.Boruvka. (paper written in Czech).
    [Chaz1997] B.Chazelle. A faster deterministic algorithm for minimum spanning trees. Symp. Foundations of Computer Science, 1997, pp.22-31.
    [Chaz2000] B.Chazelle. The soft heap: an approximate priority queue with optimal error rate. J. ACM, 47:6, 1012-1027, Nov. 2000.
    [Chaz2000b] B.Chazelle. A Minimum Spanning Tree Algorithm with Inverse-Ackermann Type Complexity. J. ACM, 47, 2000, pp. 1028-1047.
    [Cher1976] D.Cheriton and R.E.Tarjan. Finding minimum spanning trees. SIAM J.Computing, Vol. 5, 1976, pp.724-742.
    [Corm2006] T.H.Cormen, C.E.Leiserson, R.Rivest and C.Stein. Introduction to Algorithms (2nd Edition), McGraw-Hill, 2006.
    [Dixo1992] B.Dixon, M.Rauch and R.E.Tarjan. Verification and sensitivity analysis of minimum spanning trees in linear time. SIAM J.Computing, 21:6, 1992, pp.1184-1192.
    [Dris1988] J.R.Driscoll, H.N.Gabow, R.Shrairman and R.E.Tarjan. Relaxed heaps: an alternative to Fibonacci heaps with applications to parallel computation. Comm.ACM, 31:11, 1988, pp.1343-54.
    [Eisn1997] J.Eisner. State-of-the-art algorithms for minimum spanning trees: A tutorial discussion. Manuscript, University of Pennsylvania, April 1997.
    [Fred1987] M.Fredman and R.Tarjan. Fibonacci heaps and their uses in improved network optimization algorithms. J. ACM, 34, 1987, pp.596-615.
    [Gabo1986] H.N.Gabow, Z.Galil, T.Spencer and R.E.Tarjan. Efficient algorithms for finding minimum spanning trees in undirected and directed graphs. Combinatorica, 6:2, 1986, pp.109-122.
    [Grah1985] R.L.Graham and P.Hell. On the history of the minimum spanning tree problem. Annals of the History of Computing, 7, 1985, pp.43-57.
    [Jarn1930] V.Jarnik. (paper written in Czech).
    [Kapl2009] H.Kaplan and U.Zwick. A simpler implementation and analysis of Chazelle's soft heaps. In Proc. ACM -SIAM Symposium on Discrete Algorithms (SODA), New York, NY, Jan 2009, pp. 477-485.
    [Karg1995] D.R.Karger, P.N.Klein and R.E.Tarjan. A randomized linear-time algorithm to find minimum spanning trees. J. ACM, 42:2, March 1995, pp.321-328.
    [King1995] V.King. A simpler minimum spanning tree verification algorithm. Proc. Int. Workshop on Algorithms and Data Structures, 1995, pp.440-448.
    [Krus1956] J.B.Kruskal. On the shortest spanning subtree of a graph and the traveling salesman problem. Proc. Am. Math. Soc., 7, 1956, pp.48-50.
    [Krus1997] J.B.Kruskal. A reminiscence about shortest spanning trees. Archivum Mathematicum, 1997, pp.13-14.
    [Pett1999] S.Pettie. Finding Minimum Spanning Trees in O(m alpha(m,n)) Time. Technical report TR-99-23, University of Texas, Austin, 1999.
    [Pett2000] S.Pettie and V.Ramachandran. An Optimal Minimum Spanning Tree Algorithm. J. ACM, 49:1, 2000, pp. 16-34.
    [Prim1957] R.C.Prim. Shortest connection networks and some generalizations. Bell Sys. Tech. J., 36, 1957, pp.362-391.
    [CS153] R.Simha. Course notes for CS-153 (Undergraduate algorithms course).
    [Weis2007] M.A.Weiss. Data Structures and Algorithm Analysis in Java, 2nd Ed., Addison-Wesley, 2007.
    [Vuil1978] J.Vuillemin. A data structure for manipulating priority queues. Comm. ACM, 21:4, April 1978, pp. 309-315.
    [Yao1975] A.Yao. An O(E loglog(V)) algorithm for finding minimum spanning trees. Inf.Proc.Lett., Vol. 4, 1975, pp.21-23.