15-1 Longest simple path in a directed acyclic graph

Suppose that we are given a directed acyclic graph $G = (V, E)$ with real-valued edge weights and two distinguished vertices $s$ and $t$ . Describe a dynamic-programming approach for finding a longest weighted simple path from $s$ to $t$ . What does the subproblem graph look like? What is the efficiency of your algorithm?

We will make use of the optimal substructure property of longest paths in acyclic graphs. Let $u$ be some vertex of the graph. If $u = t$, then the longest path from $u$ to $t$ has zero weight. If $u \ne t$, let $p$ be a longest path from $u$ to $t$. Path $p$ has at least two vertices. Let $v$ be the second vertex on the path. Let $p'$ be the subpath of $p$ from $v$ to $t$ ($p'$ might be a zero-length path). That is, the path $p$ looks like

$$u \to v \overset{p'}{\leadsto} t.$$

We claim that $p'$ is a longest path from $v$ to $t$.

To prove the claim, we use a cut-and-paste argument. If $p'$ were not a longest path, then there exists a longer path $p''$ from $v$ to $t$. We could cut out $p'$ and paste in $p''$ to produce a path $u \to v \overset{p''}{\leadsto} t$ which is longer than $p$, thus contradicting the assumption that $p$ is a longest path from $u$ to $t$.

It is important to note that the graph is acyclic. Because the graph is acyclic, path $p''$ cannot include the vertex u, for otherwise there would be a cycle of the form $u \to v \leadsto u$ in the graph. Thus, we can indeed use $p''$ to construct a longer path. The acyclicity requirement ensures that by pasting in path $p''$ , the overall path is still a simple path (there is no cycle in the path). This difference between the cyclic and the acyclic case allows us to use dynamic programming to solve the acyclic case.

Let $dist[u]$ denote the weight of a longest path from $u$ to $t$. The optimal substructure property allows us to write a recurrence for $dist[u]$ as

$$ dist[u] = \begin{cases} 0 & \text{if $u = t$}, \\ \max\limits_{(u, v)\in E}{w(u, v) + dist[v]} & \text{otherwise}. \end{cases} $$

This recurrence allows us to construct the following procedure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
LONGEST-PATH-AUS(G, u, t, dist, next)
    if u == t
        dist[u] = 0
        return (dist, next)
    else if next[u]  0
        return (dist, next)
    else next[u] = 0
        for each vertex v  G.Adj[u]
            (dist, next) = LONGEST-PATH-AUX(G, v, t, dist, next)
            if w(u, v) + dist[v] > dist[u]
                dist[u] = w(u, v) + dist[v]
                next[u] = v
    return (dist, next)

(See Section 22.1 for an explanation of the notation $G.Adj[u]$.)

$\text{LONGEST-PATH-AUX}$ is a memoized, recursive procedure, which returns the tuple $(dist, next)$. The array $dist$ is the memoized array that holds the solution to subproblems. That is, after the procedure returns, $dist[u]$ will hold the weight of a longest path from $u$ to $t$. The array $next$ serves two purposes:

  • It holds information necessary for printing out an actual path. Specifically, if $u$ is a vertex on the longest path that the procedure found, then $next[u]$ is the next vertex on the path.
  • The value in $next[u]$ is used to check whether the current subproblem has been solved earlier. A value of at least zero indicates that this subproblem has been solved earlier.

The first if condition checks for the base case $u = t$. The second if condition checks whether the current subproblem has already been solved. The for loop iterates over each adjacent edge($u, v)$ and updates the longest distance in $dist[u]$.

What is the running time of $\text{LONGEST-PATH-AUX}$? Each subproblem represented by a vertex $u$ is solved at most once due to the memoization. For each vertex, we examine its adjacent edges. Thus, each edge is examined at most once, and the overall running time is $O(E)$. (Section 22.1 discusses how we achieve $O(E)$ time by representing the graph with adjacency lists.)

The $\text{PRINT-PATH}$ procedure prints out the path using information stored in the next array:

1
2
3
4
5
6
PRINT-PATH(s, t, next)
    u = s
    print u
    while u != t
        print "→" next[u]
        u = next[u]

The $\text{LONGEST-PATH-MAIN}$ procedure is the main driver. It creates and initializes the $dist$ and the $next$ arrays. It then calls $\text{LONGEST-PATH-AUX}$ to find a path and $\text{PRINT-PATH}$ to print out the actual path.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
LONGEST-PATH-MAIN(G, s, t)
    n = |G.V|
    let dist[1..n] and next[1..n] be new arrays
    for i = 1 to n
        dist[i] = -
        next[i] = -1
    (dist, next) = LONGEST-PATH-AUX(G, s, t, dist, next)
    if dist[s] == -
        print "No path exists"
    else print "The weight of the longest path is" dist[s]
        PRINT-PATH(s, t, next)

Initializating the dist and next arrays takes $O(V)$ time. Thus the overall running time of $\text{LONGEST-PATH-MAIN}$ is $O(V + E)$.

Alternative solution

We can also solve the problem using a bottom-up aproach. To do so, we need to ensure that we solve "smaller" subproblems before we solve "larger" ones. In our case, we can use a topological sort (see Section 22.4) to obtain a bottom-up procedure, imposing the required ordering on the vertices in $\Theta(V + E)$ time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
LONGEST-PATH2(G, s, t)
    let dist[1..n] and next[1..n] be new arrays
    topologically sort the vertices of G
    for i = 1 to |G.V|
        dist[i] = -
    dist[s] = 0
    for each u in topological order, starting from s
        for each edge (u, v)  G.Adj[u]
            if dist[u] + w(u, v) > dist[v]
                dist[v] = dist[u] + w(u, v)
                next[u] = v
    print "The longest distance is" dist[t]
    PRINT-PATH(s, t, next)

The running time of $\text{LONGEST-PATH2}$ is $\Theta(V + E)$.