We will now consider three variations of the single-source shortest-path problem:
Negative weights:
Directed graphs:
DAG's:
Algorithm: DAG-SPT (G, s) Input: Graph G=(V,E) with edge weights and designated source vertex s. // Initialize priorities and create empty SPT. 1. Set priority[i] = infinity for each vertex ; // Sort vertices in topological order and place in list. 2. vertexList = topological sort of vertices in G; // Place source in shortest path tree. 3. priority[s] = 0 4. Add s to SPT; // Process remaining vertices. 5. while vertexList.notEmpty() // Extract next vertex in topological order. 6. v = extract next vertex in vertexList; // Explore edges from v. 7. for each neighbor u of v 8. w = weight of edge (v, u); // If there's a better way to get to u (via v), then update. 9. if priority[u] > priority[v] + w 10. priority[u] = priority[v] + w 11. predecessor[u] = v 12. endif 13. endfor 14. endwhile 15. Build SPT; 16. return SPT Output: Shortest Path Tree (SPT) rooted at s.
Algorithm: maxWeightPath (G, s) Input: Graph G=(V,E) with edge weights and designated source vertex s. // ... initialization same as DAG-SPT ... 5. while vertexList.notEmpty() // ... same as DAG-SPT ... // Notice the reversal from ">" to "<": 9. if priority[u] < priority[v] + w 10. priority[u] = priority[v] + w // ... 12. endif 14. endwhile // ... same as DAG-SPT ... Output: Longest Path from source s.
The shortest-path between every pair of vertices:
Algorithm: Dijkstra-AllPairsShortestPaths (G) Input: Graph G with edge weights. 1. for each vertex i in G // Find the shortest path tree with i as source. 2. Dijkstra-SPT (i) 3. endfor 4. Construct paths; 5. return paths Output: shortest-path between each pair of vertices.
Key ideas in the Floyd-Warshall algorithm:
Dijk | = |
wij
min ( Dijk-1, Dikk-1 + Dkjk-1 ) |
if k = -1
if k >= 0 |
Note:
In-Class Exercise 9.1:
Write pseudocode to recursively compute Dijn-1.
Start with FloydWarshallRec.java and:
Implementation:
Algorithm: floydWarshall (adjMatrix) Input: Adjacency matrix representation: adjMatrix[i][j] = weight of edge (i,j), if an edge exists; adjMatrix[i][j]=0 otherwise. // Initialize the "base case" corresponding to k == -1. // Note: we set the value to "infinity" when no edge exists. // If we didn't, we would have to include a test in the main loop below. 1. for each i, j 2. if adjMatrix[i][j] > 0 3. D-1[i][j] = adjMatrix[i][j] 4. else 5. D-1[i][j] = infinity 6. endif 7. endfor // Start iterating over k. At each step, use the previously computed matrix. 8. for k=0 to numVertices-1 // Compute Dk[i][j] for each i,j. 9. for i=0 to numVertices-1 10. for j=0 to numVertices-1 11. if i != j // Use the relation between Dk and Dk-1 12. if Dk-1[i][j] < Dk-1[i][k] + Dk-1[k][j] // CASE 2 13. Dk[i][j] = Dk-1[i][j] 14. else 15. Dk[i][j] = Dk-1[i][k] + Dk-1[k][j] // CASE 3 16. endif 17. endif 18. endfor 19. endfor // Matrix copy: current Dk becomes next iteration's Dk-1 20. Dk-1 = Dk 21. endfor // The Dk matrix only provides optimal costs. The // paths still have to be built using Dk. 22. Build paths; 23. return paths Output: paths[i][j] = the shortest path from i to j.
public void allPairsShortestPaths (double[][] adjMatrix) { // Dk_minus_one = weights when k = -1 for (int i=0; i < numVertices; i++) { for (int j=0; j < numVertices; j++) { if (adjMatrix[i][j] > 0) Dk_minus_one[i][j] = adjMatrix[i][j]; else Dk_minus_one[i][j] = Double.MAX_VALUE; // NOTE: we have set the value to infinity and will exploit // this to avoid a comparison. } } // Now iterate over k. for (int k=0; k < numVertices; k++) { // Compute Dk[i][j], for each i,j for (int i=0; i < numVertices; i++) { for (int j=0; j < numVertices; j++) { if (i != j) { // D_k[i][j] = min ( D_k-1[i][j], D_k-1[i][k] + D_k-1[k][j]. if (Dk_minus_one[i][j] < Dk_minus_one[i][k] + Dk_minus_one[k][j]) Dk[i][j] = Dk_minus_one[i][j]; else Dk[i][j] = Dk_minus_one[i][k] + Dk_minus_one[k][j]; } } } // Now store current Dk into D_k-1 for (int i=0; i < numVertices; i++) { for (int j=0; j < numVertices; j++) { Dk_minus_one[i][j] = Dk[i][j]; } } } // end-outermost-for // Next, build the paths by doing this once for each source. // ... (not shown) ... }
Analysis:
In-Class Exercise 9.2: In FloydWarshall.java (the sample code above), count the number of times the innermost if-statement is executed. Explain the difference between this count and the one from the previous exercise: why is the earlier one so much higher?
An optimization:
12. if Dk-1[i][j] < Dk-1[i][k] + Dk-1[k][j] 13. Dk[i][j] = Dk-1[i][j] 14. else 15. Dk[i][j] = Dk-1[i][k] + Dk-1[k][j] 16. endifwith
// The first Dk[i][j] is really Dk-1[i][j] // because we haven't written into it yet. 12. if Dk[i][j] < Dk[i][k] + Dk[k][j] // This is superfluous: 13. Dk[i][j] = Dk[i][j] 14. else // This is all we need: 15. Dk[i][j] = Dk[i][k] + Dk[k][j] 16. endif
Algorithm: floydWarshallOpt (adjMatrix) Input: Adjacency matrix representation: adjMatrix[i][j] = weight of edge (i,j), if an edge exists; adjMatrix[i][j]=0 otherwise. // ... initialization similar to that in floydWarshall ... 1. for k=0 to numVertices-1 2. for i=0 to numVertices-1 3. for j=0 to numVertices-1 4. if i != j // Use the same matrix. 5. if D[i][k] + D[k][j] < D[i][j] 6. D[i][j] = D[i][k] + D[k][j] 7. endif 8. endif 9. endfor 10. endfor 11. endfor // ... path construction ...
Consider an alternative iterative version of Floyd-Warshall:
for k=0 to numVertices-1 for each i,j Compute Dkij endfor endfor
for each i,j for k=0 to numVertices-1 Compute Dkij endfor endforWould this work?
while changeOccurred changeOccurred = false for each i,j for k=0 to numVertices-1 // Compute Dkij if D[i][k] + D[k][j] < D[i][j] // Improved shortest-cost. D[i][j] = D[i][k] + D[k][j] // Since this may propagate, we have to continue iteration. changeOccurred = true endif endfor endfor endwhile
Algorithm: floydWarshallIterative (adjMatrix) Input: Adjacency matrix representation: adjMatrix[i][j] = weight of edge (i,j), if an edge exists; adjMatrix[i][j]=0 otherwise. // ... initialization similar to that in floydWarshallOpt ... 1. changeOccurred = true 2. while changeOccurred changeOccurred = false 3. for i=0 to numVertices-1 4. for j=0 to numVertices-1 5. if i != j // "k" is now in the innermost loop. 6. for k=0 to numVertices 7. if D[i][k] + D[k][j] < D[i][j] // Improved shortest-cost. 8. D[i][j] = D[i][k] + D[k][j] // Since this may propagate, we have to continue iteration. 9. changeOccurred = true 10. endif 11. endfor 10. endif 11. endfor 12. endfor 13. endwhile // ... path construction ...
What do we mean by "distributed routing"?
In-Class Exercise 9.3: Why aren't routes computed just once and for all whenever a network is initialized?
Distributed Floyd-Warshall: a purely distibuted algorithm
while changeOccurred changeOccurred = false for i=0 to numVertices-1 for j=0 to numVertices-1 // Node i says "let me try to get to destination j via k". for k=0 to numVertices // If it's cheaper for me to go via k, let me record that. if D[i][k] + D[k][j] < D[i][j] // Improved shortest-cost: my cost to neighbor k, plus k's cost to j D[i][j] = D[i][k] + D[k][j] changeOccurred = true endif endfor endfor endwhile
An alternative: running Dijkstra at each node
The process of "routing":
Destination | Current cost | Outgoing link |
... | ... | ... |
... | ... | ... |
5 | 4 | (0,2) |
... | ... | ... |
Source | Destination | Current cost | Outgoing link |
... | ... | ... | ... |
... | ... | ... | ... |
1 | 5 | x | (0,2) |
... | ... | ... | ... |
0 | 5 | y | (0,3) |
... | ... | ... | ... |
Internet routing:
What is dynamic programming?
Consider the following routing problem:
Let's now apply dynamic programming:
Algorithm: computeOptimal 1. computeD(0,0) Method: computeD(i,j) 1. if (i,j) is target // if bottom-right 2. return 0 3. endif 4. rightcost = xi,j + computeD(i,j+1) 5. belowcost = yi,j + computeD(i+1,j) 6. diagonalcost = zi,j + computeD(i+1,j+1) 7. return min(rightcost, belowcost, diagonalcost)
Algorithm: computeOptimal 1. initialize 2D array D[i,j] = -1 2. computeD(0,0) Method: computeD(i,j) 1. if D[i,j] ≥ 0 // Already computed 2. return D[i,j] 3. else if (i,j) is target // if bottom-right 4. D[i,j] = 0 5. return 0 6. endif 7. rightcost = xi,j + computeD(i,j+1) 8. belowcost = yi,j + computeD(i+1,j) 9. diagonalcost = zi,j + computeD(i+1,j+1) 10. return min(rightcost, belowcost, diagonalcost)
Algorithm: computeOptimal 1. D[lastrow][lastcolumn] = 0 2. for i = lastrow downto 1 3. for j = lastcolumn downto 1 4. rightcost = xi,j + D[i][j+1] 5. belowcost = yi,j + D[i+1][j] 6. diagonalcost = zi,j + D[i+1][j+1] 7. D[i][j] = min(rightcost, belowcost, diagonalcost) 8. endfor 9. endfor 10. return D[0][0]Note:
In-Class Exercise 9.4: Calculate the remaining D[i][j] values.
Finally, what about the route itself?
In-Class Exercise 9.5: Modify the pseudocode (iterative version) to record the outgoing edge information.
Let's review Floyd-Warshall in terms of dynamic programming:
Dijk | = |
wij
min ( Dijk-1, Dikk-1 + Dkjk-1 ) |
if k = -1
if k >= 0 |
Consider the following problem: (Contiguous Load Balancing)
In-Class Exercise 9.6: Write pseudocode to take as input (1) the task times, and (2) the number of processors, and produce a (contiguous) partition of tasks among the processors.
Example: dynamic programming applied to the load balancing problem
(where j ranges over { 0, ..., i }).
Optimal solution to problem of size (k, i) |
= | Combination of (maximum of) |
optimal solution to problem of size (k-1, j) (for some j) |
and |
some computation (sum across last partition) |
In terms of the equation:
Dik | = | max | (Dj*k-1, | sj*+1 + ... + si) |
Implementation:
Algorithm: dynamicProgrammingLoadBalancing (numTasks, taskTimes, numProcessors) Input: the number of tasks (numTasks), the number of processors (numProcessors), taskTimes[i] = time required for task i. // Initialization. First, the base cases: 1. D[1][i] = sum of taskTimes[0], ... ,taskTimes[i]; // We will set the other values to infinity and exploit this fact in the code. 2. D[k][i] = infinity, for all i and k > 1 // Now iterate over the number of processors. 3. for k=2 to numProcessors // Optimally allocate i tasks to k processors. 4. for i=0 to numTasks-1 // Find the optimal value of D[k][i] using prior computed values. 5. min = max = infinity // Try each value of j in the recurrence relation. 6. for j=0 to i // Compute sj+1 + ... + si 7. sum = 0 8. for m=j+1 to i 9. sum = sum + taskTimes[m] 10. endfor // Use the recurrence relation. 11. max = maximum (D[k-1][j], sum) // Record the best (over j). 12. if max < min 13. D[k][i] = max 14. min = max 15. endif 16. endfor // for j=0 ... // Optimal D[k][i] found. 17. endfor // for i=0 ... 18. endfor // outermost: for k=2 ... 18. Find the actual partition; 19. return partition Output: the optimal partition
Analysis:
An optimization:
Algorithm: dynamicProgrammingLoadBalancing (numTasks, taskTimes, numProcessors) Input: the number of tasks (numTasks), the number of processors (numProcessors), taskTimes[i] = time required for task i. // Precompute partial sums for i=0 to numTasks partialSum[i] = 0 for j=0 to i partialSum = partialSum + taskTimes[i] endfor endfor // ... Remaining initialization as before ... for k=2 to numProcessors for i=0 to numTasks-1 for j=0 to i // Note: sj+1 + ... + si = partialSum[i]-partialSum[j] // Use the recurrence relation. max = maximum (D[k-1][j], partialSum[i] - partialSum[j]) // ... remaining code is identical ...
Consider the following problem:
x0 , x1 , ..., xn-1find the contiguous subsequence
xi , ..., xjwhose sum is the largest.
In-Class Exercise 9.7: Implement the naive and most straightforward approach: try all possible contiguous subsequences. Write down pseudocode and then analyse the running time.
Using dynamic programming:
Largest suffix (of a prefix):
x0 , x1 , ..., xn-1find, for each k, the largest-sum suffix of the numbers
x0 , x1 , ..., xkwhere the sum is taken to be zero, if negative.
Dynamic programming algorithm for suffix problem:
x0 , x1 , ..., xk
Dk | = |
Dk-1 + xk
0 |
if Dk-1 + xk > 0
otherwise |
for k=1 to n-1 // Apply the dynamic programming equation if Dk-1 + xk > 0 Dk = Dk-1 + xk else Dk = 0 endif endfor
D0 | = |
x0
0 |
if x0 > 0
otherwise |
Dynamic programming algorithm for subsequence problem:
x0 , x1 , ..., xk
Sk | = |
Dk-1 + xk
Sk-1 |
if Dk-1 + xk > Sk-1
otherwise |
Algorithm: maxSubsequenceSum (X) Input: an array of numbers, at least one of which is positive // Initial value of D 1. if X[0] > 0 2. D = X[0] 3. else 4. D = 0 5. endif // Initial value of S, the current best max 6. S = X[0] // Single scan 7. for k = 1 to n-1 // Update S 8. if D + X[k] > S 9. S = D + X[k] 10. endif // Update D 11. if D + X[k] > 0 12. D = D + X[k] 13. else 14 . D = 0 15. endif 16. endfor 17. return S
Consider the following problem:
Key | Access probability |
A | 0.4 |
B | 0.1 |
C | 0.2 |
D | 0.1 |
E | 0.2 |
Past solutions we have seen:
In-Class Exercise 9.8:
Consider the following two optimal-list algorithms:
Analyze the running time of each algorithm in terms of the
number of list elements n.
Algorithm: optimalList (E, p)
Input: set of elements E, with probabilities p
// Base case: single element list
1. if |E| = 1
2. return list containing E
3. endif
4. m = argmaxi p(i) // Which i gives the max
5. front = new node with m
6. list = optimalList (E - {m}, p - pm)
7. return list.addToFront (front)
Algorithm: optimalList2 (E, p)
Input: set of elements E, with probabilities p
// Base case: single element list
1. if |E| = 1
2. return list containing E
3. endif
4. for i=0 to E.length
5. front = new node with i
6. list = optimalList2 (E - {i}, p)
7. tempList = list.addToFront (front)
8. cost = evaluate (tempList)
9. if cost < bestCost
10. best = i
11. bestList = tempList
12. endif
13. endfor
14. return bestList
Optimal binary search tree:
In-Class Exercise 9.9: Which of the two list algorithms could work for the optimal binary tree problem? Write down pseudocode.
Dynamic programming solution:
Implementation:
// Initialize C and apply base cases. for i=0 to numKeys-2 for j=i+1 to numKeys-1 min = infinity sum = pi + ... + pj; for k=i to j if C(i, k-1) + C(k+1, j) + sum < min min = C(i, k-1) + C(k+1, j) + sum ...Suppose, above, i=0, j=10 and k=1 in the innermost loop
Algorithm: optimalBinarySearchTree (keys, probs) Input: keys[i] = i-th key, probs[i] = access probability for i=th key. // Initialize array C, assuming real costs are positive (or zero). // We will exploit this entry to check whether a cost has been computed. 1. for each i,j set C[i][j] = -1; // Base cases: 2. for each i, C[i][i] = probs[i]; // Search across various i, j ranges. 3. for i=0 to numKeys-2 4. for j=i+1 to numKeys-1 // Recursive method computeC actually implements the recurrence. 5. C[i][j] = computeC (i, j, probs) 6. endfor 7. endfor // At this point, the optimal solution is C(0, numKeys-1) 8. Build tree; 9. return tree Output: optimal binary search tree
Algorithm: computeC (i, j, probs) Input: range limits i and j, access probabilities // Check whether sub-problem has been solved before. // If so, return the optimal cost. This is an O(1) computation. 1. if (C[i][j] >= 0) 2. return C[i][j] 3. endif // The sum of access probabilities used in the recurrence relation. 4. sum = probs[i] + ... + probs[j]; // Now search possible roots of the tree. 5. min = infinity 6. for k=i to j // Optimal cost of the left subtree (for this value of k). 7. Cleft = computeC (i, k-1) // Optimal cost of the right subtree. 8. Cright = computeC (k+1, j) // Record optimal solution. 9. if Cleft + Cright < min 10. min = Cleft + Cright 11. endif 12. endfor 13. return min Output: the optimal cost of a binary tree for the sub-range keys[i], ..., keys[j].
Analysis:
So, what to make of dynamic programming and its "strangeness"?
Key points to remember:
In-Class Exercise 9.10:
For the moment, let's set aside efficiency and merely get some
practice with seeing if dynamic programming can be set up properly.
Consider an array A with n elements and the following
three problems:
For each of the above problems, can
\(D_{ij}\) be written
in terms of \(D_{ik}\) and \(D_{kj}\) where \(i\leq k\leq j\),
with possibly a few additional terms like \(A[k]\)?