22. January 2013

Inspecting Algorithms with Graphs

Graphs and algorithms are tightly connected. One surprisingly simple way to inspect algorithms graphically is to let the algorithms plot graphs during their execution using GML (Graph Modeling Language). The produced graphs can be formatted with graph editing software.

The computational complexity of recursive algorithms is often analyzed by counting the number of nodes in the recursion tree (see [1]). But sometimes I like to just see the recursion tree and get an intuitive feeling about what the tree looks like.

One example is the plain recursive implementation of the rod-cutting algorithm. Note that there is a much more efficient solution to the rod-cutting problem using dynamic programming (see [1]). In order to visualize the exponential complexity of the plain recursive algorithm (and hence why it is bad), we can decorate the algorithm with some plotting statements. A quite concise and simple plotting format is GML (Graph Modelling Language). I prefer to just encode the graph in GML, and let a graph editing software such as yED do the layout.

 1 #include <algorithm>
 2 #include <iostream>
 3 
 4 int cut_rod(int *p, int n, int &id) {
 5     static int nid = 0;
 6     std::cout << "node" << std::endl
 7               << "[" << std::endl
 8               << "id " << nid << std::endl
 9               << "label " << n << std::endl
10               << "]" << std::endl;
11     id = nid;
12     nid++;
13     if (n == 0) {
14         return 0;
15     } else {
16         int q = -1;
17         for (int i = 1; i <= n; i++) {
18             int cid;
19             q = std::max(q, p[i-1] + cut_rod(p, n - i, cid));
20             std::cout << "edge" << std::endl
21                       << "[" << std::endl
22                       << "source " << id << std::endl
23                       << "target " << cid << std::endl
24                       << "]" << std::endl;
25         }
26         return q;
27     }
28 }
29 
30 int main() {
31     const int n = 4;
32     int p[n];
33     std::fill(p, p + n, 0);
34     int id;
35     std::cout << "graph" << std::endl
36               << "[" << std::endl
37               << "hierarchic 1" << std::endl
38               << "directed 1" << std::endl;
39     cut_rod(p, n, id);
40     std::cout << "]" << std::endl;
41     return 0;
42 }

The algorithm follows the pseudo-code given in [1] (named CUT-ROD), the example prices for the different rod lengths are all initialized to zero, but here the focus is only on the recursion tree. The produced graph, layouted with yED, looks like this (for n = 5):

Another example is a parallelized algorithm for computing the line-of-sight using a parallel scan from the Intel Threading Building Blocks (Intel TBB). Here, we are facing an additional difficulty: it is up to Intel TBB to distribute parts of the problem onto different threads. Also the decision about the order of splitting input data and joining partial results is up to Intel TBB, Hence, it is not so easy to reason about what execution paths the parallelized algorithm might take. Here is an automatically generated plot for the execution of a parallel scan (German speakers can read more about the example in context):

Altogether, this is quite a comfortable way to inspect algorithms graphically.

References

[1] T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to Algorithms, 3rd ed. MIT Press, 2009.