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:
D_{ij}^{k} | = |
w_{ij}
min ( D_{ij}^{k-1}, D_{ik}^{k-1} + D_{kj}^{k-1} ) |
if k = -1
if k >= 0 |
Note:
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^{k-1}[i][j] = adjMatrix[i][j] 4. else 5. D^{k-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 D^{k}[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 D^{k} and D^{k-1} 12. if D^{k-1}[i][j] < D^{k-1}[i][k] + D^{k-1}[k][j] // CASE 2 13. D^{k}[i][j] = D^{k-1}[i][j] 14. else 15. D^{k}[i][j] = D^{k-1}[i][k] + D^{k-1}[k][j] // CASE 3 16. endif 17. endif 18. endfor 19. endfor // Matrix copy: current D^{k} becomes next iteration's D^{k-1} 20. D^{k-1} = D^{k} 21. endfor // The D^{k} matrix only provides optimal costs. The // paths still have to be built using D^{k}. 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.1: Start with the following template and:
An optimization:
12. if D^{k-1}[i][j] < D^{k-1}[i][k] + D^{k-1}[k][j] 13. D^{k}[i][j] = D^{k-1}[i][j] 14. else 15. D^{k}[i][j] = D^{k-1}[i][k] + D^{k-1}[k][j] 16. endifwith
// The first D^{k}[i][j] is really D^{k-1}[i][j] // because we haven't written into it yet. 12. if D^{k}[i][j] < D^{k}[i][k] + D^{k}[k][j] // This is superfluous: 13. D^{k}[i][j] = D^{k}[i][j] 14. else // This is all we need: 15. D^{k}[i][j] = D^{k}[i][k] + D^{k}[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 ...
First, consider an iterative version of Floyd-Warshall:
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 ...
for k=0 to numVertices-1 for i=0 to numVertices-1 for j=0 to numVertices-1 // ... compute with D[i][j], D[i][k] and D[k][j] ... endfor endfor endfor
for i=0 to numVertices-1 for j=0 to numVertices-1 for k=0 to numVertices-1 // ... compute with D[i][j], D[i][k] and D[k][j] ... endfor endfor endforIn this case:
What do we mean by "distributed routing"?
In-Class Exercise 9.2: Why aren't routes computed just once and for all whenever a network is initialized?
Distributed Floyd-Warshall: a purely distibuted algorithm
while changeOccurred 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
A semi-distributed algorithm: 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:
Consider the following problem: (Contiguous Load Balancing)
In-Class Exercise 9.3: Write an algorithm to take as input (1) the task times, and (2) the number of processors, and produce a (contiguous) partition of tasks among the processors. Start by downloading this template.
What is dynamic programming?
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:
D_{i}^{k} | = | max | (D_{j*}^{k-1}, | s_{j*+1} + ... + s_{i}) |
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 s_{j+1} + ... + s_{i} 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
static int[] dynamicProgramming (double[] taskTimes, int numProcessors) { int numTasks = taskTimes.length; // If we have enough processors, one processor per task is optimal. if (numProcessors >= numTasks) { int[] partition = new int [numTasks]; for (int i=0; i < numTasks; i++) partition[i] = i; return partition; } // Create the space for the array D. double[][] D = new double [numProcessors+1][]; for (int p=0; p<=numProcessors; p++) D[p] = new double [numTasks]; // Base cases: for (int i=0; i < numTasks; i++) { // Set D[1][i] = s_0 + ... + s_i double sum = 0; for (int j=0; j<=i; j++) sum += taskTimes[j]; D[1][i] = sum; for (int k=i+2; k<=numProcessors; k++) D[k][i] = Double.MAX_VALUE; // Note: we are using MAX_VALUE in lieu of INFINITY. } // Dynamic programming: compute D[k][i] for all k // Now iterate over the number of processors. for (int k=2; k<=numProcessors; k++) { // In computing D[k][i], we iterate over i second. for (int i=0; i < numTasks; i++) { // Find the optimal value of D[k][i] using // prior computed values. double min = Double.MAX_VALUE; double max = Double.MAX_VALUE; // Try each value of j in the recurrence relation. for (int j=0; j<=i; j++) { // Compute s_{j+1} + ... + s_{i} double sum = 0; for (int m=j+1; m<=i; m++) sum += taskTimes[m]; // Use the recurrence relation. max = D[k-1][j]; if (sum > max) { max = sum; } // Record the best (over j). if (max < min) { min = max; D[k][i] = min; } } // end-innermost-for } // end-second-for // Optimal D[k][i] found. } //end-outermost-for // ... compute the partition itself (not shown) ... }
Example:
Task | 0 | 1 | 2 | 3 | 4 |
Time | 50 | 23 | 62 | 72 | 41 |
k = 2, i = 0
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 0.0 | 0.0 | 0.0 | 0.0 |
INF | INF | 0.0 | 0.0 | 0.0 |
k = 2, i = 1
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 0.0 | 0.0 | 0.0 |
INF | INF | 0.0 | 0.0 | 0.0 |
k = 2, i = 2
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 0.0 | 0.0 |
INF | INF | 0.0 | 0.0 | 0.0 |
k = 2, i = 3
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 134.0 | 0.0 |
INF | INF | 0.0 | 0.0 | 0.0 |
k = 2, i = 4
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 134.0 | 135.0 |
INF | INF | 0.0 | 0.0 | 0.0 |
k = 3, i = 0
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 134.0 | 135.0 |
50.0 | INF | 0.0 | 0.0 | 0.0 |
k = 3, i = 1
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 134.0 | 135.0 |
50.0 | 50.0 | 0.0 | 0.0 | 0.0 |
k = 3, i = 2
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 134.0 | 135.0 |
50.0 | 50.0 | 62.0 | 0.0 | 0.0 |
k = 3, i = 3
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 134.0 | 135.0 |
50.0 | 50.0 | 62.0 | 73.0 | 0.0 |
k = 3, i = 4
0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
50.0 | 73.0 | 135.0 | 207.0 | 248.0 |
50.0 | 50.0 | 73.0 | 134.0 | 135.0 |
50.0 | 50.0 | 62.0 | 73.0 | 113.0 |
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: s_{j+1} + ... + s_{i} = partialSum[i]-partialSum[j] // Use the recurrence relation. max = maximum (D[k-1][j], partialSum[i] - partialSum[j]) // ... remaining code is identical ...
The Floyd-Warshall Algorithm used earlier is actually dynamic programming:
D_{ij}^{k} | = |
w_{ij}
min ( D_{ij}^{k-1}, D_{ik}^{k-1} + D_{kj}^{k-1} ) |
if k = -1
if k >= 0 |
Consider the following problem:
x_{0} , x_{1} , ..., x_{n-1}find the contiguous subsequence
x_{i} , ..., x_{j}whose sum is the largest.
In-Class Exercise 9.4: Implement the naive and most straightforward approach: try all possible contiguous subsequences. Start by downloading this template. For now, ignore the template for the faster algorithm.
Using dynamic programming:
Largest suffix (of a prefix):
x_{0} , x_{1} , ..., x_{n-1}find, for each k, the largest-sum suffix of the numbers
x_{0} , x_{1} , ..., x_{k}where the sum is taken to be zero, if negative.
Dynamic programming algorithm for suffix problem:
x_{0} , x_{1} , ..., x_{k}
D_{k} | = |
D_{k-1} + x_{k}
0 |
if D_{k-1} + x_{k} > 0
otherwise |
for k=1 to n-1 // Apply the dynamic programming equation if D_{k-1} + x_{k} > 0 D_{k} = D_{k-1} + x_{k} else D_{k} = 0 endif endfor
D_{0} | = |
x_{0}
0 |
if x_{0} > 0
otherwise |
Dynamic programming algorithm for subsequence problem:
x_{0} , x_{1} , ..., x_{k}
S_{k} | = |
D_{k-1} + x_{k}
S_{k-1} |
if D_{k-1} + x_{k} > S_{k-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
In-Class Exercise 9.5: Implement the faster algorithm and compare with the naive algorithm.
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:
Optimal binary search tree:
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 = p_{i} + ... + p_{j}; 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. C_{left} = computeC (i, k-1) // Optimal cost of the right subtree. 8. C_{right} = computeC (k+1, j) // Record optimal solution. 9. if C_{left} + C_{right} < min 10. min = C_{left} + C_{right} 11. endif 12. endfor 13. return min Output: the optimal cost of a binary tree for the sub-range keys[i], ..., keys[j].
Analysis: