Tutorial 1: Asymptotic Notation and Insertion Sort
Q1. Other worst cases for insertion sort
The worst case for insertion sort occurs when the input list contains the keys in decreasing order: . Suppose the performance of insertion sort is measured by the total number of comparisons between array elements (not counting the number of swaps). Show at least two other initial arrangements of keys that are also worst cases for insertion sort.
Concepts: when does one insertion make the most comparisons?
Insertion sort takes the next element, slot[i], and
compares it with its left neighbour, again and again.
- If the left neighbour is bigger, the two swap.
- If the left neighbour is not bigger, the loop stops. That costs one comparison.
- If the element reaches index 0, the loop stops with no further comparison.
So iteration makes between 1 and comparisons. The worst total is . Every iteration must make comparisons to reach it.
Answer
Two other worst cases:
In each list, the -th key still needs comparisons to find its place. But a comparison is not always followed by a swap.
- . Every key up to 1 is smaller than everything on its left, so it travels to index 0. The last key, 2, swaps past . Then it compares with 1, which is smaller, and stops. That last comparison has no swap, but it is still the -th comparison.
- . Each new key is smaller than every key on its left except 1. It swaps all the way to index 1, then compares with 1 and stops. Again comparisons.
General method: test an input for the comparison worst case
Iteration makes all comparisons exactly when the new element reaches index 1. After that, either it moves to index 0, or the element at index 0 stops it. Both cost comparisons.
So an input is a worst case for comparisons when each new element is smaller than every element on its left, except possibly the smallest one.
- Check this for each position from left to right.
- Comparisons and swaps are different counts. Swaps equal the number of inversions (pairs in the wrong order). Only the reversed array has the most swaps, .
Extension 1.1. Insertion sort runs on the array . Count the comparisons and the swaps. Is this array a worst case for comparisons? Is it a worst case for swaps?
Answer (Extension 1.1)
| Iteration | Element | What happens | Comparisons | Swaps |
|---|---|---|---|---|
| 1 | 4 | 4 against 2: not smaller, stop | 1 | 0 |
| 2 | 3 | swaps past 4, then 3 against 2: stop | 2 | 1 |
| 3 | 1 | swaps past 4, 3 and 2, reaches index 0 | 3 | 3 |
Comparisons: . So it is a worst case for comparisons. Each new element is smaller than everything on its left except the smallest, as the method says.
Swaps: 4, which equals the inversions . The maximum is 6. So it is not a worst case for swaps.
Q2. Theta is O and Omega together
Prove that, for any two functions and , we have if and only if and .
Answer
The → direction. Suppose . By definition, there are such that
Pick the constants from here. The pair satisfies the definition of . The pair satisfies the definition of . So lies in both sets.
The ← direction. Suppose and . Then, for some constants and :
Pick . Both inequalities hold from on, so merge them:
That is the definition of .
General method: prove a fact about O, Omega and Theta
- Write out each definition you are given, with its own constants: “ such that … for all ”. Give each one a new name (, then ).
- Write out the definition you must reach.
- Build its constants from the ones you have. Two tools cover most
proofs:
- Take the maximum of the thresholds. Each fact holds from its own . All of them hold from .
- Multiply or add the constants. If and , then .
Extension 2.1. Prove that is transitive: if and , then .
Answer (Extension 2.1)
From the first fact: for all . From the second: for all .
Take . For , both hold, so
With , this is the definition of . The proof used both tools of the method: the maximum threshold and the product of constants.
Q3. Two functions that cannot be compared
Give an example of non-negative functions and such that and .
Answer
Take
Proof that . Suppose, for a contradiction, that . Then for some positive constants and :
Let be the smallest even number strictly larger than . In particular, .
Put into . Since is even, , so . This contradicts .
Proof that . The official solution leaves this as an exercise. The proof is the same, with an odd number. Suppose for all . Let be an odd number larger than . Then , so , which gives . This contradicts .
General method: show that f is not O(g)
- Assume the constants exist: for all .
- Choose one value of that is larger than and breaks the inequality. Usually you pick larger than both and , from the part of the domain where is small.
- Put it in and reach a contradiction such as with .
To build two functions that cannot be compared, make one of them jump between a value below the other and a value above it, for example on even and odd .
Extension 3.1. Let and for even and for odd . Is ? Is ?
Answer (Extension 3.1)
Neither.
- : suppose for . Take an odd . Then , so , so . Contradiction.
- : suppose for . Take an even . Then , so . Contradiction.
Here jumps above on even and below it on odd , as the method says.
Q4. A loop invariant for a primality test
Consider the following function for testing primality of a natural number :
def isPrime(n):
p = True
for i = 2 to floor(sqrt(n)):
if n is divisible by i then:
p = False
return p
State a (non-trivial) loop invariant for the function above, and use your invariant to prove the function’s correctness.
Concepts: the three parts of a loop-invariant proof
A loop invariant is a statement that is true before every iteration of the loop.
- Initialization: it is true before the first iteration.
- Maintenance: if it is true before one iteration, it is true before the next.
- Termination: when the loop stops, the invariant shows that the answer is correct.
Answer
First rewrite the for loop as a while loop.
Then you can see the value of i when the loop stops.
def isPrime(n):
p = true
i = 2
while (i <= floor(sqrt(n))):
if n is divisible by i then:
p = false
i++
return p
Invariant: “ is true if and only if there is no integer such that and divides .”
Initialization. At the start, . No integer satisfies , and is true. So the invariant holds before the loop.
Maintenance. After one full iteration, has grown by 1. The iteration tested the number that is now . There are two cases.
- If divides , the code set to false. The invariant for the new holds, because divides .
- If does not divide , is unchanged. By the inductive hypothesis, the old value of was correct for the range . The range grew by one number, , and that number does not divide . So the answer to “is there a divisor in the range?” did not change, and is still correct. (The official solution asks “why?”. This is the reason.)
Termination. The loop stops when . By the invariant, is true if and only if no integer with divides .
This condition means is prime, for every . (The official solution says “prove it!”. Here is the proof.) Suppose is not prime, so with . Then , so , and because is a whole number. So a composite always has a divisor in the range, and is false. A prime has no divisor in the range, and stays true.
General method: write a loop-invariant proof
- Rewrite a
forloop as awhileloop, so the loop variable has a clear value at every check and at the end. - State the invariant as what the loop has done so far, in terms of the loop variable: “the first elements are sorted”, “ is the largest of ”, “no divisor below ”.
- Initialization: put in the start value of the loop variable. The range is usually empty, so the statement is easy.
- Maintenance: follow one iteration. Split into cases
by the
ifinside the loop. - Termination: put in the stop value. The invariant must now say exactly what the function returns.
Extension 4.1. State a loop invariant for the function below, and use it to prove that the function returns the largest element of , for .
def findMax(A, n):
m = A[0]
i = 1
while i < n:
if A[i] > m:
m = A[i]
i = i + 1
return m
Answer (Extension 4.1)
Invariant: “ is the largest element of .”
- Initialization. , and , the largest (only) element of .
- Maintenance. The iteration looks at . If , then is the new largest of , and becomes . Otherwise is still the largest. Then grows by 1, so is the largest of for the new .
- Termination. The loop stops at . Then is the largest of , the whole array.
Tutorial 2: Divide and Conquer and Mergesort
Q1. Maximum and minimum together in about 1.5n comparisons
Use the divide and conquer approach to design an algorithm that finds both the largest and the smallest elements in an array of integers, assuming that is a power of two, i.e., for some . Show that your algorithm does at most about comparisons of the elements.
Answer: the algorithm
void findMinMax(int[] ar, int start, int end, int[] minMax)
// minMax is an array of two integers to return the minimum
// and maximum found
{
int tempMinMax1[] = new int[2];
int tempMinMax2[] = new int[2];
int mid;
if (start == end) { // one element array
minMax[0] = ar[start];
minMax[1] = ar[start];
}
else if (end - start == 1) { // two element array
if (ar[start] > ar[end]) {
minMax[1] = ar[start];
minMax[0] = ar[end];
} else {
minMax[0] = ar[start];
minMax[1] = ar[end];
}
}
else { // array longer than two elements
mid = (start + end) / 2;
findMinMax(ar, start, mid, tempMinMax1);
findMinMax(ar, mid+1, end, tempMinMax2);
if (tempMinMax1[0] < tempMinMax2[0])
minMax[0] = tempMinMax1[0];
else minMax[0] = tempMinMax2[0];
if (tempMinMax1[1] > tempMinMax2[1])
minMax[1] = tempMinMax1[1];
else minMax[1] = tempMinMax2[1];
}
}
- One element: it is both the min and the max. No comparison.
- Two elements: one comparison decides both the min and the max.
- More: solve each half, then compare the two minimums and the two maximums: 2 comparisons.
Answer: the count
Let be the number of comparisons for elements. Then the recurrence is
If , where is a positive integer, the two halves are equal:
Expand it:
Since , we get , and . So
General method: count the comparisons of a divide-and-conquer algorithm
- Read the base cases off the code, with their cost. Here: 1 element costs 0, 2 elements cost 1.
- Write the recurrence: (number of recursive calls) (size of each) + (comparisons in the combine step).
- Expand it until the size reaches a base case. Count the expansions: from down to size 2 takes halvings.
- Add up the combine costs, usually as a geometric series .
The saving here comes from the base case of 2: one comparison does the work of two. Compare it with the plain scan, which compares each element with both the current min and the current max: comparisons.
Extension 1.1. Remove the two-element base case from
findMinMax, so the recursion stops only at one element. How
many comparisons does it make for
?
What does this show?
Answer (Extension 1.1)
Now and . Expand all the way, times:
That is no better than the plain scan. The result depends on the two-element base case. At the bottom level there are pairs. Without the base case, each pair costs 2 comparisons (min against min, max against max) instead of 1.
Q2. Tracing mergesort
Show how merge sort sorts each of the arrays below, and give the number of comparisons among array elements in each case.
- 14 40 31 28 3 15 17 51
- 23 23 23 23 23 23 23 23
Answer (first array)
The array is first divided into two equal parts. Each part is sorted by mergesort, which divides it again, until each part holds one item:
| Level | Parts |
|---|---|
| Split 1 | [14 40 31 28] [3 15 17 51] |
| Split 2 | [14 40] [31 28] [3 15] [17 51] |
| Split 3 | [14] [40] [31] [28] [3] [15] [17] [51] |
Then the parts are merged back:
| Merge | Result | Comparisons |
|---|---|---|
| Each pair | [14 40] [28 31] [3 15] [17 51] | 4 |
| Each pair of pairs | [14 28 31 40] [3 15 17 51] | 5 |
| Final | [3 14 15 17 28 31 40 51] | 7 |
In total, comparisons are made.
Where the 5 comes from: merging [14 40] with [28 31] compares 14:28, 40:28, 40:31, then the right list is empty (3). Merging [3 15] with [17 51] compares 3:17, 15:17, then the left list is empty (2).
Answer (second array)
The splits are the same as for the first array, down to eight single 23s. Then:
| Merge | Result | Comparisons |
|---|---|---|
| Each pair | [23 23] [23 23] [23 23] [23 23] | 4 |
| Each pair of pairs | [23 23 23 23] [23 23 23 23] | 4 |
| Final | [23 23 23 23 23 23 23 23] | 4 |
In total, comparisons.
Why only 4 at each level: with equal keys, a merge of elements costs only comparisons. In the lecture’s in-place merge, one comparison of two equal keys places both of them (Case 3). In a buffer merge that takes from the left on a tie, the left list empties after comparisons. Either way, the count is the best case.
General method: count the comparisons of one merge
Each comparison sends one element to the output. The comparisons stop when one list is empty. So for two lists with elements in total:
- Most: , when the last two elements are in different lists (the lists interleave).
- Fewest: the size of the smaller list, when all of it is smaller than all of the other list.
Trace mergesort bottom-up, level by level, and add one count per merge. Check the total against the bounds for : best , worst .
Extension 2.1. Mergesort runs on the sorted array 1 2 3 4 5 6 7 8. How many comparisons does it make? Check the answer, and the two answers above, against the best and worst cases for .
Answer (Extension 2.1)
In every merge, the left list is entirely smaller, so it empties first. A merge of elements costs :
- pairs: ;
- pairs of pairs: ;
- final: .
Total 12, the best case .
The worst case is . So the three arrays give 12 (sorted), 12 (all equal) and 16 (first array). All lie between 12 and 17, as they must.
Q3. Finding duplicates
Suppose we have an unsorted array of elements, and we want to know if the array contains any duplicate elements.
- Outline (clearly) an efficient method for solving this problem.
Answer (a)
First, sort the array in increasing (or decreasing) order. Then scan the sorted array once and check for equal adjacent elements. After sorting, equal elements always sit next to each other.
Bool ExistDuplicate(int A[], int n) {
MergeSort(A, n); // or Heapsort
for (int i = 1; i < n; i++) {
if (A[i] == A[i-1])
return true;
}
return false;
}
- What is the asymptotic order of the running time of your method in the worst case? Justify your answer.
Answer (b)
The worst-case time for sorting is , using mergesort (or heapsort). The time for the scan is . Therefore, the overall worst-case running time is .
Do not use quicksort here: its worst case is .
- Suppose we know the elements are integers from the range , so other operations besides comparing keys may be done. Give an algorithm for the same problem that is specialized to use this information. Give the asymptotic order of the worst case running time for this solution. It should be of lower order than your solution for part (a).
Answer (c)
Count how many elements have each key value. Use an array of counters, one for each possible value, and use the key value as the index.
- We can quit as soon as any count becomes 2.
- We scan the -element array once, and each element takes constant time to count. So the overall worst-case running time is .
- It uses extra memory for the counters. Setting them all to 0 also costs .
General method: two ways to bring equal items together
Many problems ask whether two items are equal, or close, or related. Comparing every pair costs . Two faster ways:
- Sort, then scan neighbours. Related items end up next to each other. Cost: with mergesort or heapsort, plus for the scan. This needs only comparisons.
- Use the key as an index. If the keys are integers in a range of size , an array of counters answers in time and memory. This beats sorting only when is , or at least .
Extension 3.1. An unsorted array holds integers. Decide whether two of them add up to a given number . Use only comparisons and additions. Aim for .
Answer (Extension 3.1)
Sort the array with mergesort: . Then scan from both ends:
i = 0; j = n - 1;
while (i < j) {
if (A[i] + A[j] == x) return true;
else if (A[i] + A[j] < x) i++; // need a bigger sum
else j--; // need a smaller sum
}
return false;
Each step moves or by one, so the scan is . Total: . It is the “sort, then scan” method: after sorting, the scan never needs to look back.
Extension 3.2. The integers now come from the range . Is the counter method from (c) still better than sorting?
Answer (Extension 3.2)
No. The counter array now needs entries. Setting them to 0 alone costs time, and they take memory. Sorting and scanning costs , which is better. The counter method pays off only when the range is small compared with .
Tutorial 3: Comparison-Sort Limits, Counting Sort and Quicksort
Q1. The shallowest leaf of a decision tree
Recall the lower bound on the worst-case runtime of any comparison sort, which was shown by considering the possible decision trees of the algorithm. What is the smallest possible depth of a leaf in a decision tree for a comparison sort?
Concepts: decision trees
A comparison sort on elements can be drawn as a decision tree. Each internal node is one comparison. Each leaf is one final order. One run of the algorithm follows one path from the root to a leaf, and the number of comparisons is the length of that path.
- The height (the longest path) is the worst case. It is at least , because the tree needs at least leaves.
- The shallowest leaf is the best case. This question asks for it.
Answer
The smallest possible depth of a leaf is .
Proof sketch.
- Suppose, for a contradiction, that there is a leaf of depth .
- Construct a graph whose vertices are the elements of the array. Connect if the algorithm compared the elements at these positions on the path to that leaf. With comparisons, the graph has only edges, so it cannot be connected.
- In particular, you can split the vertices into two non-empty parts and , such that no element of was compared to any element of .
- So the algorithm knows nothing about the order of any element of against any element of .
- So the order that the leaf gives must be wrong (not sorted) on some input. For example, take an input that agrees with every comparison made, then add a large constant to every element of . The comparisons give the same results, so the run reaches the same leaf, but the correct order has changed.
Insertion sort reaches depth on a sorted array, so is achieved.
General method: bounds from a decision tree
- Worst case (height): the tree has at least as many leaves as there are possible answers, . A binary tree of height has at most leaves. So . For sorting, .
- Best case (shallowest leaf): every element must be linked to every other by some chain of comparisons. A connected graph on vertices needs edges.
Extension 1.1. Any comparison sort runs on 3 distinct elements. How many comparisons does it need in the worst case? In the best case?
Answer (Extension 1.1)
- Worst case: the tree needs leaves. Height 2 gives at most leaves, which is too few. So the height is at least .
- Best case: at least . Insertion sort reaches it on a sorted input: , then , and it stops.
Q2. Running the last loop of counting sort forwards
Recall the final for loop in counting sort, responsible
for placing the elements into their sorted positions, which took the
form:
(i.e., iterating from the end of the input list through to the beginning). Suppose the order of iteration was reversed by swapping the line above with:
Would this new version of counting sort still be correct? Justify your answer. What else do you observe about this modified algorithm?
Answer
The two versions differ only in the last loop:
for i <- 0 to k for i <- 0 to k
do C[i] <- 0; do C[i] <- 0;
for j <- 0 to length[A] - 1 for j <- 0 to length[A] - 1
do C[A[j]] <- C[A[j]] + 1; do C[A[j]] <- C[A[j]] + 1;
for i <- 1 to k for i <- 1 to k
do C[i] <- C[i] + C[i-1]; do C[i] <- C[i] + C[i-1];
for j <- length[A] - 1 downto 0 => for j <- 0 upto length[A] - 1
do begin do begin
C[A[j]] <- C[A[j]] - 1; C[A[j]] <- C[A[j]] - 1;
B[C[A[j]]] <- A[j]; B[C[A[j]]] <- A[j];
end-for end-for
return B return B
Example of the “new” counting sort on , with keys 0 to 4:
- Histogram: .
- Cumulative sum: .
- Placement loop run, from :
| Output after the step | ||
|---|---|---|
| start | [ , , , ] | |
| 0 | 3 | [ , 3, , ] |
| 1 | 4 | [ , 3, , 4] ← which 4 is this? |
| 2 | 1 | [1, 3, , 4] |
| 3 | 4 | [1, 3, 4, 4] |
- It would still be correct. Every element still lands in the block of slots for its key, so the output is sorted.
- But it would no longer be stable. The first 4 in took the last slot for 4. The order of equal-valued elements is reversed, instead of being preserved.
General method: check whether a sort is stable
Take two equal keys, before in the input. Follow the algorithm and ask: which one ends at the higher index?
- For counting sort, the last loop fills each key’s slots from the right, because it decrements before it writes. So the element it meets first gets the rightmost slot. To keep before , the loop must meet first: it must run backwards.
- Correctness and stability are separate questions. A sort can be correct and not stable.
Extension 2.1. Radix sort sorts the two-digit numbers 12 and 11 (in that input order). Each pass uses the forward counting sort from this question. Show the result after each pass. Is it sorted?
Answer (Extension 2.1)
- Pass 1, last digit: the digits are 2 and 1. The keys differ, so the pass sorts them correctly: 11, 12.
- Pass 2, first digit: both first digits are 1, so the keys are equal. The forward loop reverses equal keys: 12, 11.
The result, 12 then 11, is not sorted. Radix sort needs a stable inner sort. The second pass must keep the order that the first pass set up.
Q3. Quicksort with the median of three
Instead of using the middle element as pivot, quicksort also can use the median of first, middle, and last elements (sometimes called the “median of three” strategy). If this pivot selection scheme is used, how many key comparisons will quicksort do in the worst case to sort elements? (Remember to count the comparisons done in choosing the pivot.)
Answer: comparisons in one partition
Consider a subrange of elements to be partitioned.
- Choosing the pivot requires 3 key comparisons in the worst case.
- Now elements remain to be compared to the pivot. It is easy to avoid comparing the other two candidates again: swap them to the two ends of the subrange. Their side is already known.
- So comparisons are done by the time the partition is finished.
- This is one more than if the middle element is simply chosen as the pivot ().
Answer: the worst case
- An example of the worst case: E[first] = 100, E[(first + last)/2] = 99, E[last] = 98, and all other elements are over 100.
- Then 99 becomes the pivot. 98 is the only element smaller than the pivot, and elements are greater than the pivot.
- The remaining keys can be chosen so that the worst case repeats: the median of three always comes out as the second-smallest key in the range.
- The larger subrange shrinks by only 2 at each level of recursion. So quicksort needs calls to partition, on ranges of size (suppose is even). The total number of comparisons is
- When is odd, the total number of comparisons is also about .
- So the worst-case time complexity with the median-of-3 strategy is .
Error in the official solution: it writes the middle position as “E[first + last/2]”. The middle index is . Without the brackets, it would be first + (last/2).
General method: the worst case of quicksort under any pivot rule
- Find the worst pivot the rule can pick: how many elements can the adversary force onto the small side? Say the small side gets elements, so each partition removes only elements (the pivot plus ).
- The ranges then have sizes . That is about partitions.
- Each partition of elements costs about comparisons. The sum is about .
Any rule that looks at a constant number of elements leaves constant, so the worst case stays . Middle element: , about . Median of three: , about .
Extension 3.1. The pivot is now the median of five elements: first, last, and three others at fixed positions. What is the worst-case number of comparisons, roughly?
Answer (Extension 3.1)
The adversary makes the five sampled elements the five smallest of the range. Their median is then the third-smallest of the range. Only 2 elements go to the small side, so each partition removes elements.
By the method: about comparisons, plus a few more per partition to find the median. Still .
Q4. Red, white and blue
Each of elements in an array may have one of the key values red, white, or blue. Give an efficient algorithm for rearranging the elements so that all the reds come before all the whites, and all the whites come before all the blues. (It may happen that there are no elements of one or two of the colours.) The only operations permitted on the elements are examination of a key to find out what colour it is, and a swap, or interchange, of two elements (specified by their indices). What is the asymptotic order of the worst case running time of your algorithm? (There is a linear-time solution.)
Answer: the four regions
At an intermediate step, the elements are divided into four regions:
- the first contains only reds;
- the second contains only whites;
- the third contains elements whose colours are not known yet;
- the fourth contains only blues.
There are three indexes: r (end of the reds), u (first unknown) and b (the edge of the blues).
Answer: the algorithm and its cost
The official algorithm:
int r; // index of last red
int u; // index of first unknown
int b; // index of first blue
r = 0; u = 0; b = n-1;
while (u < b)
if (E[u] == red)
Interchange E[r] and E[u];
r++;
u++;
else if (E[u] == white)
u++;
else // (E[u] == blue)
Interchange E[b] and E[u]
b--;
- With each iteration of the while loop, either u increases by one or b decreases by one. So there are iterations.
- Each iteration takes constant time, so the overall time is linear, .
Error in the official code. With
b = n-1 and the blue case “swap, then b--”,
the index b points at the last unknown
element, not the first blue. The loop while (u < b) then
stops one element early: the element at u == b is never
examined. Example:
.
It starts with r = 0, u = 0, b = 1. E[0] is white, so u = 1. Now
u < b is false and the loop ends, leaving white before
red.
The fix is one character: while (u <= b). (Chapter 4
uses the other common form: b = n, and in the blue case
b-- first, then swap, with
while (u < b).)
General method: partition in one pass with regions
- Draw the array as regions, one per group, plus one unknown region. Name the index at each border.
- Write what each region holds. This is the loop invariant.
- For each possible value of the next unknown element, write the swap and the index moves that put it in its region and keep the invariant.
- Check that every iteration shrinks the unknown region by exactly one. Then the loop runs times: .
- Test the code by hand on two-element inputs. Off-by-one errors in the stop condition show up at once.
Extension 4.1. In the corrected algorithm, what is the largest number of swaps for an array of elements? Give an input that reaches it.
Answer (Extension 4.1)
Each iteration makes at most one swap, and there are iterations. So at most swaps.
An array of
blues reaches it. Every iteration finds a blue at u and
swaps it with the element at b (also a blue), then moves
b down. That is
swaps, even though nothing needed to move. Each iteration still shrinks
the unknown region by one, as the method requires.
Tutorial 4: Heapsort, Priority Queues, BFS and DFS
Q1. Building a max-heap of dates
Given an array with content (of type date): 1 Jul, 30 Jan, 22 Mar, 22 Dec, 30 May, 21 Feb, 3 Nov, 7 Jun, 22 Feb, 21 Nov, 30 Dec; all of the same year. Suppose an earlier date is considered larger than a later date; for example, “30 Jan” is larger than “30 Dec”. In order to sort these dates by heapsort, we construct a max-heap. Show the contents of the array (representing the max-heap) after the heap construction phase.
Concepts: fixHeap and the build order
- In a max-heap, every node is at least as large as its children. Node has children and .
fixHeaprepairs one node whose two subtrees are already heaps. At each level it makes 2 comparisons: one to find the larger child, one to compare that child with the sinking key. If the child is larger, the child moves up.- To build a heap, fix every node that has a child, children before
parents. The lecture’s recursive
heapifyingdoes this in post-order: left subtree, right subtree, then the root. The loop from node down to 1 gives the same heap. - Here “larger” means “earlier in the year”.
Answer
heapifying fixes the nodes in post-order: 4, 5, 2, 3,
then 1.
| Fix at node | Key | What happens | Comparisons | Swaps |
|---|---|---|---|---|
| 4 | 22 Dec | Children 7 Jun, 22 Feb. 22 Feb is larger and beats 22 Dec, so 22 Feb moves up. 22 Dec goes to node 9. | 2 | 1 |
| 5 | 30 May | Children 21 Nov, 30 Dec. 30 May is larger than both. No move. | 2 | 0 |
| 2 | 30 Jan | Children 22 Feb, 30 May. 30 Jan is larger than both. No move. | 2 | 0 |
| 3 | 22 Mar | Children 21 Feb, 3 Nov. 21 Feb is larger, so it moves up. | 2 | 1 |
| 1 | 1 Jul | 30 Jan moves up to node 1. Then 22 Feb moves up to node 2. Then 7 Jun moves up to node 4. 1 Jul ends at node 8. | 6 | 3 |
Heapifying performs 14 date comparisons and 5 swaps in the heap construction phase. The resulting array is:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|
| 30 Jan | 22 Feb | 21 Feb | 7 Jun | 30 May | 22 Mar | 3 Nov | 1 Jul | 22 Dec | 21 Nov | 30 Dec |
General method: trace heap construction by hand
- Draw the array as a tree, level by level. Number the nodes.
- List the nodes with children: down to 1. (Post-order gives the same final heap.)
- For each one, sink its key: compare the two children (1), then compare the larger child with the key (1). If the child is larger, it moves up and the key continues down. A node with one child costs only 1 comparison at that level.
- Count per fix: moves = levels sunk; comparisons = about 2 per level visited.
- Check the result: every parent beats both children.
Extension 1.1. Insert the date 15 Jan into the final heap from Q1. Show the array and count the comparisons.
Answer (Extension 1.1)
Insert puts 15 Jan at the next free position, node 12, then bubbles it up while it beats its parent.
- The parent of 12 is node 6 (22 Mar). 15 Jan is earlier, so they swap. (1 comparison)
- The parent of 6 is node 3 (21 Feb). Swap. (1 comparison)
- The parent of 3 is node 1 (30 Jan). 15 Jan is earlier. Swap. (1 comparison)
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 15 Jan | 22 Feb | 30 Jan | 7 Jun | 30 May | 21 Feb | 3 Nov | 1 Jul | 22 Dec | 21 Nov | 30 Dec | 22 Mar |
3 comparisons, one per level. Insert costs at most the height, .
Q2. Heap construction on a decreasing array
An array of distinct keys in decreasing order is to be sorted (into increasing order) by heapsort.
- How many comparisons of keys are done in the heap construction phase if there are 10 elements?
Answer (a)
.
| Nodes compared | Comparisons |
|---|---|
| 4, 8, 9 | 2 |
| 5, 10 | 1 |
| 2, 4, 5 | 2 |
| 3, 6, 7 | 2 |
| 1, 2, 3 | 2 |
- How many are done if there are elements? Show how you derive your answer.
Answer (b)
Node is an internal node (not a leaf) if and only if , so . Thus:
- if is even, there are internal nodes;
- if is odd, there are internal nodes.
Since the keys are in decreasing order, fixHeap does only one or two key comparisons each time it is called, and no key moves.
- If is even, the last internal node has only one child. So fixHeap does only one comparison at that node, and two at each other internal node. Total: .
- If is odd, each of the internal nodes has two children. fixHeap does two comparisons at each. Total: .
In both cases, the total is key comparisons.
- Is an array in decreasing order the best case, the worst case, or an intermediate case for this algorithm? Justify your answer.
Answer (c)
It is the best case. No key moves. So in the heapifying algorithm, fixHeap only compares the root with its two children, and never needs to continue down, as it does in an intermediate or worst case.
Every internal node must be compared with its children at least once, so no input can do fewer comparisons.
General method: count heap-construction comparisons
- Best case (no key moves): each internal node costs 2, or 1 if it has a single child. There are internal nodes, and only node (when is even) has one child. The total is always .
- Any other input: trace it with the method of Q1. Each extra level a key sinks costs up to 2 more comparisons.
- Worst case: still , because most nodes sit near the bottom and can sink only a little.
Extension 2.1. Build a max-heap from the increasing array 1 2 3 4 5 6 7 8 9 10. Count the comparisons. Compare with part (a).
Answer (Extension 2.1)
Use the loop order 5, 4, 3, 2, 1.
| Fix at node | What happens | Comparisons | Array after |
|---|---|---|---|
| 5 (key 5) | One child, 10: 1 comparison. 10 moves up. | 1 | 1 2 3 4 10 6 7 8 9 5 |
| 4 (key 4) | Children 8, 9: 9 is larger and beats 4. 9 moves up. | 2 | 1 2 3 9 10 6 7 8 4 5 |
| 3 (key 3) | Children 6, 7: 7 moves up. | 2 | 1 2 7 9 10 6 3 8 4 5 |
| 2 (key 2) | Children 9, 10: 10 moves up (2). At node 5, one child (5): 5 moves up (1). | 3 | 1 10 7 9 5 6 3 8 4 2 |
| 1 (key 1) | 10 moves up (2), then 9 (2), then 8 (2). 1 ends at node 8. | 6 | 10 9 7 8 5 6 3 1 4 2 |
Total: comparisons, against 9 for the decreasing array. The increasing array is not the best case, because keys must sink.
Q3. Merging k sorted lists in O(n log k)
Given lists with a total of numbers, where and each list has been sorted in decreasing order, design an algorithm to merge the lists into one list sorted in decreasing order, in running time .
Answer
One algorithm is to use a maximizing heap.
- First, insert the elements from the heads of the sorted lists into a maximizing heap. This takes time.
- Then, use getMax() to obtain the maximum element from the heap, and append it at the end of the merged list (initially empty). Suppose this element came from the -th list. Then insert the element from the next position of the -th list into the heap, using fixHeap: it takes the place of the root and sinks.
- Continue in this fashion until all the lists have been exhausted. (When list is empty, the last heap element takes the root’s place instead, and the heap shrinks by one.)
Analysis. For one element, an insertion using fixHeap takes time, because the heap never holds more than elements. Hence for the elements, the running time is .
Example. Input: three sorted arrays , , . Output: a merged list .
| Step | Output | Comes from | Inserted next | Heap after |
|---|---|---|---|---|
| 1 | 9 | 7 | 8, 4, 7 | |
| 2 | 8 | 6 | 7, 4, 6 | |
| 3 | 7 | 5 | 6, 4, 5 |
Continue in this fashion until all the input lists are exhausted.
General method: keep a heap of size k, not n
When the answer comes out in order but only a few items can be “next” at any time, keep only those candidates in a heap.
- Here, the next output is always one of the current heads. So the heap holds items, and each step costs , not .
- Total = (number of outputs) (cost per heap step) = .
- Use a max-heap for lists in decreasing order, a min-heap for increasing order.
Extension 3.1. A simpler plan merges list 1 with list 2, then the result with list 3, and so on, with the merge from mergesort. Each list has elements. How long does it take?
Answer (Extension 3.1)
Merging two lists costs about their total length.
- Merge joins a list of elements with one of , so it costs about .
- Summed over : about .
That is , worse than . The early elements are copied again in every merge. The heap touches each element only once.
Q4. BFS on an adjacency matrix
The running time analysis of BFS discussed in lectures assumed that the input graph was represented by an adjacency list. What is the running time if the input graph was instead represented by an adjacency matrix? How would you modify the algorithm to handle this form of input?
Answer
The BFS from the lecture:
function bfs(v1):
queue := {v1}
mark v1 as visited
v1.d = 0
while queue is not empty:
v := queue.removeFirst()
for each unvisited neighbour n of v:
n.d = v.d + 1
mark n as visited
queue.addLast(n)
Adjacency list.
- We iterate through Adj[] once for every vertex.
- This has worst-case runtime per vertex (when all edges are adjacent to ), but is typically lower.
- The overall runtime is , by considering the total length of the adjacency lists.
Adjacency matrix.
- We must iterate through the whole row of in the matrix .
- This always has runtime .
- Since we do this for every vertex, the overall runtime is .
The modification: replace “for each unvisited neighbour of ” with a scan of row :
for n = 1 to |V|:
if A[v][n] == 1 and n is unvisited:
n.d = v.d + 1
mark n as visited
queue.addLast(n)
General method: the cost of a graph traversal
Total = (work per vertex) + (cost of finding the neighbours of every vertex once).
| Storage | Neighbours of one vertex | All vertices | BFS or DFS total |
|---|---|---|---|
| Adjacency list | |||
| Adjacency matrix |
The algorithm is the same. Only the neighbour loop changes. The same table holds for DFS.
Extension 4.1. A graph has 1000 vertices and 3000 edges. Roughly how many basic steps does BFS take with each storage? Which storage suits this graph?
Answer (Extension 4.1)
- Adjacency list: about . (An undirected graph stores each edge twice, so about 7000.)
- Adjacency matrix: about .
The graph is sparse (), so the list is far better.
Q5. DFS without recursion
Rewrite the depth-first search procedure taught in lectures in a way that doesn’t use recursion.
Answer
We can use a last-in, first-out (LIFO) stack.
function dfs():
mark all vertices as unvisited
while there exists an unvisited vertex v:
dfsVisit(v)
// Explore all vertices reachable from v in depth-first order
function dfsVisit(v):
S = empty stack
S.push(v)
while S is non-empty:
w = S.pop()
mark w as visited
for each unvisited neighbour x of w:
S.push(x)
A gap in the official code. A vertex can be pushed more than once before it is visited. Example: the triangle –, –, –. Visiting pushes and . Popping visits it and pushes again, because is still unvisited. Now is on the stack twice, and the code visits it twice. Add one line after the pop:
w = S.pop()
if w is visited: continue // w was pushed more than once
mark w as visited
General method: turn a recursion into a loop with a stack
- The call stack holds “work still to do”. Replace it with your own stack: push what the recursion would call, pop to continue.
- The stack returns the last item pushed. So to visit the neighbours in the same order as the recursive version, push them in reverse order.
- Check “already visited” when you pop, because an item can be pushed several times.
- BFS is the same loop with a queue in place of the stack.
Extension 5.1. An undirected graph has edges –, – and –. Neighbours are listed in alphabetical order. Give the visiting order of the recursive DFS from , and of the stack version (with the visited check). How do you make the stack version match?
Answer (Extension 5.1)
- Recursive: , then its first neighbour , then ’s neighbour . Back up to , then . Order: .
- Stack: pop , push then . Pop (last pushed): visit it. Pop : visit it, push . Pop . Order: .
Push the neighbours in reverse alphabetical order ( then ). Then is popped first, and the order becomes .
Tutorial 5: Dijkstra’s Algorithm and Unique MSTs
Q1. Tracing Dijkstra’s algorithm
Apply Dijkstra’s algorithm on the graph represented by the following adjacency matrix to find the shortest distances and shortest paths from vertex 1 to all other vertices. Show the contents of the array , as well as the and attributes after each iteration of the while loop.
Answer: the graph
Row , column holds the weight of the edge from to .
Answer: the trace
After initialization:
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| S | 0 | 0 | 0 | 0 | 0 |
| d | 0 | ∞ | ∞ | ∞ | ∞ |
| pi | null | null | null | null | null |
1st iteration: vertex 1 joins . Its edges set = 4, 2, 6, 8.
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| S | 1 | 0 | 0 | 0 | 0 |
| d | 0 | 4 | 2 | 6 | 8 |
| pi | null | 1 | 1 | 1 | 1 |
2nd iteration: vertex 3 () joins. Edge 3→4: .
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| S | 1 | 0 | 1 | 0 | 0 |
| d | 0 | 4 | 2 | 3 | 8 |
| pi | null | 1 | 1 | 3 | 1 |
3rd iteration: vertex 4 () joins. Edge 4→2: , not smaller than 4. Edge 4→5: .
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| S | 1 | 0 | 1 | 1 | 0 |
| d | 0 | 4 | 2 | 3 | 6 |
| pi | null | 1 | 1 | 3 | 4 |
4th iteration: vertex 2 () joins. Edge 2→5: , not smaller than 6. (Edge 2→4 leads to a vertex already in .)
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| S | 1 | 1 | 1 | 1 | 0 |
| d | 0 | 4 | 2 | 3 | 6 |
| pi | null | 1 | 1 | 3 | 4 |
5th iteration: vertex 5 joins.
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| S | 1 | 1 | 1 | 1 | 1 |
| d | 0 | 4 | 2 | 3 | 6 |
| pi | null | 1 | 1 | 3 | 4 |
Shortest paths (follow back to 1): 1 – 2, 1 – 3, 1 – 3 – 4, 1 – 3 – 4 – 5.
General method: trace Dijkstra in a table
- One column per vertex, rows , , . Start: at the source, elsewhere.
- Each iteration: among the vertices with , pick the smallest (break ties by the rule the question gives). Set its .
- For each edge from it to a vertex with : if , write the new and set . Equal is not smaller: leave it.
- Checks: vertices join in non-decreasing ; a vertex in never changes again.
- Read each path backwards through .
Extension 1.1. Change the weight of edge 3→4 from 1 to 5, and run Dijkstra from vertex 1 again. Give the final and .
Answer (Extension 1.1)
- Iteration 1: vertex 1. = (0, 4, 2, 6, 8), = (null, 1, 1, 1, 1).
- Iteration 2: vertex 3 (). 3→4: , not smaller than 6. No change.
- Iteration 3: vertex 2 (). 2→4: , no. 2→5: , so and .
- Iteration 4: vertex 4 (). 4→5: , no.
- Iteration 5: vertex 5.
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| d | 0 | 4 | 2 | 6 | 7 |
| pi | null | 1 | 1 | 1 | 2 |
Vertex 4 is now reached by the direct edge, and vertex 5 through vertex 2.
Q2. Counting shortest paths
Let be a weighted graph, and let and be distinct vertices. In the graph, there may be more than one shortest path from to . Explain how to modify Dijkstra’s shortest-path algorithm to determine the number of distinct shortest paths from to . Assume all edge weights are positive.
Answer
Add a counter v.count: the number of shortest paths from
the source to
found so far.
shortest_paths( Graph g, Node source )
// The array count[] records the number of shortest paths from
// source to each of the vertices
{
for each vertex v {
v.d = infinity;
v.pi = null pointer;
S[v] = 0;
v.count = 0;
}
source.d = 0;
source.count = 1;
put all vertices in queue, Q, in v.d's order;
while not Empty(Q) {
u = ExtractCheapest( Q );
S[u] = 1; /* Add u to S */
for each vertex v adjacent to u
if (S[v] != 1 && v.d > u.d + w[u,v]) {
remove v from Q;
v.d = u.d + w[u,v];
v.pi = u;
// the shortest paths to u extends to v, the
// previously known shortest path(s) to v is replaced
v.count = u.count;
insert v into Q according to its v.d;
}
else if (v.d == u.d + w[u, v]) {
// additional shortest path(s) through u to v is found
v.count += u.count;
}
}
}
At the end, z.count is the answer.
- A strictly shorter path through
:
every old path to
is no longer shortest. The new shortest paths are exactly the shortest
paths to
,
plus the edge. So
v.count = u.count. - An equally short path through
:
these are new shortest paths, added to the old ones. So
v.count += u.count.
Error in the official code: it sets
source.d = 1 after source.d = 0. It must be
source.count = 1. Otherwise every count stays 0, and the
source’s distance is wrong.
General method: extend Dijkstra to compute more than distances
Attach an extra value to each vertex, and update it in the relax step, with two cases:
| Relax result | Distance | Extra value |
|---|---|---|
| replace | replace with a value built from | |
| keep | combine with a value built from |
- Count of shortest paths: replace with
u.count; combine by adding. - Fewest edges among the shortest paths: replace with
u.edges + 1; combine by taking the minimum.
Why positive weights matter: every vertex before on a shortest path has a strictly smaller , so it leaves the queue before . Its count is final before it is passed on. With a zero-weight edge, and can have the same , and may leave the queue first and miss ’s paths.
Extension 2.1. A graph has edges (1), (1), (1), (1) and (2). Run the counting algorithm from , breaking ties alphabetically. How many shortest paths reach ?
Answer (Extension 2.1)
| Step | Vertex out | Effect |
|---|---|---|
| 1 | (d 0, count 1) | : d 1, count 1. : d 1, count 1. : d 2, count 1. |
| 2 | (d 1) | : , equal: count(t) = 1 + 1 = 2. |
| 3 | (d 1) | : , equal: count(t) = 2 + 1 = 3. |
| 4 | done |
3 shortest paths, each of length 2: , , . The first relax of used “replace”. The next two used “combine”.
Q3. Distinct weights give a unique MST
Show that every connected weighted graph whose edge weights are all different has a unique minimum spanning tree.
Answer
Proof by contradiction. Suppose there exists a connected weighted graph whose edge weights are all different, with two distinct minimum spanning trees and .
Among all the edges where and differ, that is, from the set , select the edge of minimum weight. This edge is unique, because all edge weights are different. Assume, without loss of generality, that . Now, must contain some other path from to , which is not simply the edge .
In particular, there must be some edge along this path in which does not occur in . If all edges in the path occurred in , then would contain a cycle (that path plus ). Notice that is a spanning tree: adding makes one cycle, and removing from that cycle breaks it again.
However, from the way we selected , we know that must have greater weight than : is also a differing edge, and is the lightest of them. Thus, is a spanning tree of smaller weight than . This contradicts the assumption that is a minimum spanning tree.
General method: the exchange argument for spanning trees
Most MST proofs use the same swap:
- Add an edge that is not in tree . This creates exactly one cycle.
- Remove any other edge of that cycle. The result is again a spanning tree.
- Its weight changes by . If that is negative, was not minimum.
To prove uniqueness, pick the lightest edge where two trees differ, and swap it into the other tree. To prove that an edge is in no MST, show that some swap would remove it and lower the weight.
Extension 3.1. Does a graph whose edge weights are all different always have a unique shortest path between two vertices? Give a proof or a counter-example.
Answer (Extension 3.1)
No. Take edges – (1), – (2) and – (3). All weights differ. Both and have length 3.
The MST argument works because two trees can be compared by swapping one edge for one edge. A path length is a sum along a path, and two different sums of distinct numbers can still be equal.
Tutorial 6: Prim, Kruskal and Union-Find
Q1. Tracing Prim’s algorithm
Execute Prim’s algorithm for finding a minimum spanning tree on the graph below, starting at vertex G. Show the contents of the array , as well as the and attributes after each iteration of the while loop.
Concepts: what d means in Prim
In Prim’s algorithm, is the weight of the cheapest single edge from to the tree so far, and is the tree vertex at the other end. The update is , not as in Dijkstra. Each iteration adds the vertex outside the tree with the smallest .
Answer
After initialization (the official tables mark G in from the start):
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| d | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 0 |
| pi | null | null | null | null | null | null | null |
1st iteration: G’s edges update A, B, D and E.
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| d | 3 | 6 | ∞ | 1 | 3 | ∞ | 0 |
| pi | G | G | null | G | G | null | null |
2nd iteration: edge GD becomes part of the MST. D’s edges: E gets 2 (better than 3), F gets 5.
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
| d | 3 | 6 | ∞ | 1 | 2 | 5 | 0 |
| pi | G | G | null | G | D | D | null |
3rd iteration: edge DE becomes part of the MST. E’s edges: C gets 2.
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
| d | 3 | 6 | 2 | 1 | 2 | 5 | 0 |
| pi | G | G | E | G | D | D | null |
4th iteration: edge EC becomes part of the MST. C’s edges: B gets 4 (better than 6).
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 0 | 0 | 1 | 1 | 1 | 0 | 1 |
| d | 3 | 4 | 2 | 1 | 2 | 5 | 0 |
| pi | G | C | E | G | D | D | null |
5th iteration: edge GA becomes part of the MST. A’s edges: B gets 2 (better than 4). F: 7 is not better than 5.
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 1 | 0 | 1 | 1 | 1 | 0 | 1 |
| d | 3 | 2 | 2 | 1 | 2 | 5 | 0 |
| pi | G | A | E | G | D | D | null |
6th iteration: edge AB becomes part of the MST.
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 1 | 1 | 1 | 1 | 1 | 0 | 1 |
| d | 3 | 2 | 2 | 1 | 2 | 5 | 0 |
| pi | G | A | E | G | D | D | null |
7th iteration: edge DF becomes part of the MST.
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| S | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| d | 3 | 2 | 2 | 1 | 2 | 5 | 0 |
| pi | G | A | E | G | D | D | null |
The MST edges are GD, DE, EC, GA, AB and DF. The total weight is , the sum of the final values.
General method: trace Prim in a table
It is Dijkstra’s table with one change in the update.
- Pick the vertex with and the smallest . Set . The edge joins the tree.
- For each neighbour with : if the edge weight is smaller than , set to that weight and .
- The MST weight is the sum of the final values.
Extension 1.1. Run Dijkstra’s algorithm from G on the same graph, breaking ties alphabetically. Which edges form the shortest-path tree? Compare its weight with the MST.
Answer (Extension 1.1)
- G: A 3, B 6, D 1, E 3.
- D (1): E: , not smaller. F: .
- A (3, before E by the tie rule): B: , so . F: 10, no.
- E (3): C: .
- B (5, before C): C: 9, no.
- C (5), then F (6).
The shortest-path tree is GD, GA, GE, AB, EC, DF. Its weight is , more than the MST’s 15. Dijkstra uses GE, to make E’s distance from G small. Prim uses DE, the cheapest edge that reaches E.
Q2. Tracing Kruskal’s algorithm with weighted QuickUnion
For the same graph as in Problem 1 above, execute Kruskal’s algorithm to find a minimum spanning tree. Use the weighted QuickUnion algorithm shown in lectures for your union-find data structure.
Concepts: weighted QuickUnion
id[v]is the parent of . A root hasid[r] == r.sz[r]is the size of the tree with root .union(p, q): find the root of and the root of . If , do nothing. Ifsz[i] < sz[j], setid[i] = j. Otherwise (a tie included), setid[j] = i. Then add the sizes at the new root.- Only a root gets a new parent. A non-root never changes.
Answer: the official trace
The edges in increasing weight: GD 1; AB 2, DE 2, EC 2; AG 3, GE 3; BC 4; DF 5; BG 6; AF 7.
The official tables, one per edge examined. Only roots carry a size.
| After | Edge | id (A B C D E F G) | Sizes of roots | Result |
|---|---|---|---|---|
| start | A B C D E F G | all 1 | ||
| 1st | GD (1) | A B C D E F D | D 2, others 1 | added |
| 2nd | AB (2) | A A C D E F D | A 2, D 2 | added |
| 3rd | DE (2) | A A C D D F D | A 2, D 3 | added |
| 4th | EC (2) | A A E D D F D | A 2, D 4 | added |
| 5th | AG (3) | G A E D D F D | D 6, F 1 | added |
| 6th | GE (3) | G A E D D F D | D 6, F 1 | rejected: cycle |
| 7th | BC (4) | D D D D D F D | D 6, F 1 | rejected: cycle |
| 8th | DF (5) | G A E D D D D | D 7 | added |
The MST is GD, AB, DE, EC, AG, DF, with weight , the same as Prim’s.
Answer: the trace by the lecture code (errors in the official tables)
The official tables do not follow the lecture’s weighted QuickUnion code in three places:
- 4th step (EC): the official sets
id[C] = E. But E is not a root. The roots are C (size 1) and D (size 3), so the code hangs the smaller root under the larger:id[C] = D. - 5th step (AG): the official sets
id[A] = G. The roots are A (size 2) and D (size 4), so the code setsid[A] = D. - 7th step: the official table shows D for every
vertex except F. Nothing was added, so
id[]cannot change. That row lists each vertex’s root (whatfindreturns), not theidarray.
Here is the trace by the lecture code. The union is called with the edge’s endpoints in alphabetical order, so the tie rule (“’s root goes under ’s root”) matches the official first step.
| Edge | Roots found | Action | id (A B C D E F G) | Sizes of roots |
|---|---|---|---|---|
| DG (1) | D, G: tie | id[G] = D |
A B C D E F D | D 2, others 1 |
| AB (2) | A, B: tie | id[B] = A |
A A C D E F D | A 2, C 1, D 2, E 1, F 1 |
| DE (2) | D (2), E (1) | id[E] = D |
A A C D D F D | A 2, C 1, D 3, F 1 |
| CE (2) | C (1), D (3) | id[C] = D |
A A D D D F D | A 2, D 4, F 1 |
| AG (3) | A (2), D (4) | id[A] = D |
D A D D D F D | D 6, F 1 |
| EG (3) | D, D | reject | D A D D D F D | D 6, F 1 |
| BC (4) | D (B→A→D), D | reject | D A D D D F D | D 6, F 1 |
| DF (5) | D (6), F (1) | id[F] = D |
D A D D D D D | D 7 |
The algorithm stops, because it has
edges. The tree is the same as in the official answer. Only the
id entries of A and C differ.
General method: trace Kruskal with union-find
- Sort the edges by weight. Fix the order of equal weights (usually alphabetical).
- For each edge
:
climb
id[]from to its root, and from to its root. - Same root: reject. The edge would close a cycle.
- Different roots: union. Only the roots change. The smaller tree’s root points to the larger tree’s root. On a tie, ’s root goes under ’s root. Add the sizes.
- Stop after accepted edges.
Extension 2.1. Run the same edge sequence with
plain QuickUnion: union(p, q) sets
id[find(p)] = find(q), with no sizes. What is the deepest
vertex at the end? Compare with the weighted version.
Answer (Extension 2.1)
| Edge | Roots | Action |
|---|---|---|
| DG | D, G | id[D] = G |
| AB | A, B | id[A] = B |
| DE | G, E | id[G] = E |
| CE | C, E | id[C] = E |
| AG | B, E | id[B] = E |
| EG | E, E | reject |
| BC | E, E | reject |
| DF | E, F | id[E] = F |
The final root is F. The path D → G → E → F has depth 3, and so has A → B → E → F. In the weighted version, the deepest vertex is B (B → A → D), at depth 2. Weighting keeps every depth at most .
Q3. Detecting a cycle with union-find
Design an algorithm using a union-find data structure to determine whether or not an undirected graph contains a cycle.
Answer
The algorithm is as follows:
- Start with every vertex in its own component.
- Process every edge one by one.
- If
and
are from different components, then
union(u, v). - Otherwise, return TRUE: and are already joined by a path, so the edge closes a cycle.
- After all edges are processed, return FALSE.
Complexity: with weighted QuickUnion and path compression.
General method: union-find for “already connected?” questions
Use union-find when edges arrive one at a time and you must keep asking “are these two already connected?”.
connected(u, v)before you add an edge tells you whether the edge closes a cycle.- Each accepted edge lowers the number of components by exactly one.
- A quick check first: a forest on vertices has at most edges. So if , the graph has a cycle, with no work at all.
Extension 3.1. Run the algorithm on the graph with vertices 1 to 5 and the edges (1, 2), (3, 4), (2, 3), (4, 1), (4, 5), in that order. When does it stop, and what does it return?
Answer (Extension 3.1)
- (1, 2): different. Union. Components {1, 2}, {3}, {4}, {5}.
- (3, 4): different. Union. {1, 2}, {3, 4}, {5}.
- (2, 3): different. Union. {1, 2, 3, 4}, {5}.
- (4, 1): same component. Return TRUE.
The cycle is 1 – 2 – 3 – 4 – 1. The edge (4, 5) is never examined.
Q4. Kruskal with small integer weights
Can we improve the runtime of Kruskal’s algorithm if we know that the edge weights of the input graph are between 1 and ? Explain your answer.
Answer
Yes. The time complexity of Kruskal’s algorithm is .
The first term comes from sorting the edge weights with a comparison sort. Since we know they are between 1 and , we can use counting sort instead, in time.
Thus, the running time of Kruskal’s algorithm becomes , which is asymptotically faster.
General method: speed up an algorithm whose bottleneck is a sort
- Write the running time as a sum of steps, and find the largest term.
- If that term is a comparison sort, , look at the keys. Integers in a range allow counting sort, . Keys with digits in base allow radix sort, .
- Recompute the total. The next largest term now dominates.
Extension 4.1. The edge weights are now integers between 1 and . Is counting sort still a good idea? What can you do instead?
Answer (Extension 4.1)
Counting sort would cost . For a sparse graph, that is worse than .
Instead, write each weight as a two-digit number in base : , with . Radix sort with and sorts the edges in . The total stays almost linear.
Tutorial 8: Solving Recurrences
Q1. Solving recurrences by iteration
Solve the following recurrences by the iteration method.
- , and for , .
Answer (1)
Expand the recurrence again and again. Each step replaces by its definition.
After expansions the pattern is . The argument reaches the base case when , so :
Put . The bracket is a geometric sum with terms, and its value is :
This is the exact value. Check: , and .
Now drop the , because it only makes the value smaller. Then change the 2 to a 3, because :
So .
- , and for , a power of 2, .
Answer (2)
Expand three times.
Each expansion adds exactly . The reason: after expansions there are copies of . Each copy adds when it expands. So the new cost is . The copies double and the sizes halve, and the two changes cancel.
After expansions, . The size reaches 1 when , so :
Check: , and .
The term dominates . So .
General method: the iteration method
- Expand the recurrence 2 or 3 times. Write each line in full, and do not simplify too early.
- Find the pattern after expansions. Write the answer as: (number of copies of ) (smaller input) + (cost added so far).
- Count the expansions to the base case. For the input shrinks by 1 each time, so . For the input halves each time, so and .
- Replace (base) by its value and add up the extra costs.
- Recognise the sum.
- Same cost each level (here ): cost number of levels.
- Costs that grow by a constant factor (): a geometric sum. . The largest term decides the order.
- Costs that grow by a constant step (, , …): an arithmetic sum. It gives order.
- State the exact value if you can. Check it on . Then simplify to a big-O bound.
Extension 1.1. Let and, for , . Solve it by the iteration method.
Answer (Extension 1.1)
Expand:
The input shrinks by 1 each time, so we stop after expansions. The extra costs are . This is an arithmetic sum:
Check: , and . Also , and .
So . In the general method this is step 5, the arithmetic sum.
Extension 1.2. Let and, for a power of 2, . Solve it by the iteration method.
Answer (Extension 1.2)
After expansions there are copies of , each of size . Each copy adds when it expands. So level adds . The costs now grow by a factor of 2 per level, so this is a geometric sum.
Here , and . So:
Check: , and .
So . Compare with Q1 (2): there the level costs were equal, so the answer had a factor. Here the level costs grow, so the last level wins.
Q2. Solving recurrences by substitution
Solve the recurrences in Problem 1 by the substitution method.
- , and for , .
Concepts: the substitution method
- Substitution has two steps: guess the bound, then prove it by induction.
- To prove
:
- Base case: show it for the smallest .
- Inductive step: assume it for smaller inputs. Put the assumption into the recurrence. Show it for the next input.
- The proof must end with the same expression that you assumed. If you assume , you must end with exactly. Ending with does not prove it.
Answer (1)
Guess . We first try to prove for .
- Base case: .
- Inductive step: assume . We must show .
This proves only , not . The proof fails. The expressions must be the same, so we have to get rid of the “”.
The fix is to prove a stronger claim: for .
- Base case: .
- Inductive step: assume . We must show .
So for all . Thus .
Why subtracting 2 works: the recurrence multiplies the assumption by 3, so the “” becomes “”. The “” in the recurrence then cancels only part of it, and “” is left, which is below the target “”. A plain has no room to absorb the extra 2.
- , and for , a power of 2, .
Answer (2)
Guess . We try for simplicity: prove . We start at , not , because and .
Base case with . , but . So is false. The constant is too small. We try instead, and prove for .
Base case with . , and . So holds.
Inductive step. Write with . Assume . We must show .
The last step holds because . So for , and .
A note on (new): the base case is not the only problem with . The step would give , which is larger than . With the factor 8 on leaves room: .
General method: the substitution method
- Guess the bound. Use the iteration method, a recursion tree or the master method to get the guess.
- State what you prove, with a constant: for .
- Base case. Pick so that . Check the base value. If it fails, make bigger.
- Inductive step. Assume the claim for (or ). Put it into the recurrence. Show the claim for the next input.
- End with the same expression that you assumed. Do not accept “ + something”.
- If you are off by a lower-order term (a constant, or ), prove a stronger claim: subtract that term. Use (or ). The subtracted term is multiplied by the recurrence, and that creates the room you need.
- If the base case fails, move the base case (use ) or increase . Only the inequality for large matters.
Extension 2.1. Let and, for a power of 2, . Prove by the substitution method.
Answer (Extension 2.1)
First try: . Assume . Then
We reach , not . The proof fails. The recurrence leaves an extra , and no choice of removes it.
Fix: subtract a lower-order term. Prove for .
- Inductive step. Assume . Then This is at most when . Take .
- Base case. . This needs . Take .
So for all powers of 2, and . This matches the exact value from Extension 1.2. The “” trick of the general method (step 6) found it.
Q3. Solving recurrences by the master method
Solve the following recurrences by the master method.
Concepts: the master theorem
For with and , compare with .
- Case 1: for some ( is polynomially smaller). Then .
- Case 2: (a tie). Then .
- Case 3: for some ( is polynomially larger), and for some (regularity). Then .
Answer (1)
Here , and .
, and .
and have the same order. This is case 2:
Answer (2)
Here , and .
, and . The constant does not change the order.
This is case 2:
Answer (3)
Here , and .
. We have . So is polynomially larger, with . This points to case 3.
Case 3 also needs the regularity condition. Check with :
So with . The condition holds. By case 3:
General method: the master method
- Read (the number of subproblems), (the shrink factor) and (the cost outside the recursive calls). Write as a power of (for example ).
- Compute the critical exponent . Then is the cost of the leaves. Note that gives .
- Compare
with
.
- Smaller by a polynomial factor : case 1, answer .
- Same order: case 2, answer . Constants such as or do not matter.
- Larger by a polynomial factor: case 3, answer , but only if regularity holds.
- For case 3, compute and compare it with . You need a constant with . For the ratio is , so the check is .
- If is only a log factor away (for example against ), no basic case applies. Use the general case 2 when with . Otherwise the master theorem does not apply.
Extension 3.1. Solve by the master method.
Answer (Extension 3.1)
Here , and .
. We have . So is polynomially smaller, with . This is case 1:
No regularity check is needed in case 1.
Extension 3.2. Solve by the master method.
Answer (Extension 3.2)
Here , and .
. Now is larger than , but only by a factor . That is not a polynomial factor, so case 3 does not apply. Write . This is the general case 2 with :
Q4. Linear homogeneous recurrences with constant coefficients
Determine which of the following are linear homogeneous recurrence relations with constant coefficients. Also find the degree of those that are.
Concepts: what the words mean
A recurrence is a linear homogeneous recurrence relation with constant coefficients if it has the form
where are constants and .
- Linear: each appears to the power 1, alone, and is not multiplied by another .
- Homogeneous: there is no extra term that lacks an (no , no ).
- Constant coefficients: each is a number, not a function of .
- Degree : how far back the furthest term reaches.
Answer (1)
Yes. The coefficients 4 and 5 are constants. The furthest term is , so the degree is 3. (The missing term just has coefficient 0.)
Answer (2)
No. The coefficient changes with . It is not constant coefficient.
Answer (3)
Yes. The coefficients are both 1. The furthest term is , so the degree is 4.
Answer (4)
No. The term is squared. It is not linear.
Answer (5)
No. The term does not contain any . It is not homogeneous.
General method: the four checks
Look at the right side of the recurrence, term by term.
- Linear? Every is to the power 1 and is not multiplied by another (no , no ).
- Homogeneous? Every term has an in it. A lone , constant or breaks it.
- Constant coefficients? Each multiplier is a plain number. A multiplier that holds (such as or ) breaks it.
- Degree: if all three checks pass, the degree is the largest in .
Only a recurrence that passes all three can be solved with the characteristic equation (Q5).
Extension 4.1. Classify each recurrence. Give the degree if it qualifies, and the failed check if it does not.
- (b) (c) (d)
Answer (Extension 4.1)
- Yes. The only coefficient is 1. The furthest term is , so the degree is 5.
- Yes. The coefficients and are constants. The degree is 2. A negative or fractional coefficient is fine.
- No. The “” is a term without an . It is not homogeneous (check 2).
- No. The coefficient changes with . It is not constant coefficient (check 3).
Q5. Solving linear homogeneous recurrences
Solve the following recurrence relations together with the initial conditions given.
- for , ,
Concepts: the characteristic equation
- For , try . This gives . Divide by to get the characteristic equation: .
- Two distinct roots : the general solution is .
- One repeated root : the general solution is .
- The two constants and come from the two initial conditions and .
Answer (1)
The characteristic equation is , so .
There are two distinct roots: and . So .
Put in the initial conditions to find and .
- : . Multiply by 2: .
- : .
Subtract the second equation from the first: , so . Then .
Check: from the recurrence, . From the formula, .
- for , ,
Answer (2)
The characteristic equation is , so .
There are two distinct roots: and . So .
- : . Multiply by 2: .
- : .
Add the two equations: , so . Then .
Check: from the recurrence, . From the formula, .
- for all , ,
Answer (3)
The characteristic equation is , so .
There is a single (repeated) root: . So .
- : .
- : , so .
Check: from the recurrence, . From the formula, .
General method: the characteristic equation
- Check the recurrence is linear, homogeneous, with constant coefficients (Q4).
- Write the characteristic equation: replace by and move everything to the left. For it is .
- Find the roots. Factor, or use the quadratic formula.
- Write the general solution.
- Distinct roots : .
- A repeated root : . The extra factor gives the second independent solution (new): without it, there is only one constant and two initial conditions cannot be met.
- Put and into the general solution. Set them equal to and . Solve the two equations for and .
- Check on : compute it from the recurrence and from the formula. The two must agree.
Extension 5.1. Solve for , with and .
Answer (Extension 5.1)
The characteristic equation is , so . There is a repeated root . So .
- : .
- : , so and .
Check: from the recurrence, . From the formula, .
This is the repeated-root case of Q5 (3), with a root other than 1. Note that the root multiplies the whole bracket: , not .
Tutorial 9: Amortized Analysis
Q1. A counter with DECREMENT
Show that if a DECREMENT operation were included in the k-bit counter example, n operations could cost as much as O(nk) time.
Concepts: what a counter operation costs
- A k-bit counter is an array of bits. The cost of an operation is the number of bits it flips.
- INCREMENT flips the trailing 1s to 0, then flips
one 0 to 1. Adding 1 to
0111gives1000: 4 flips. - DECREMENT does the reverse. It flips the trailing
0s to 1, then flips one 1 to 0. Subtracting 1 from
1000gives0111: 4 flips. - Without DECREMENT, the lecture proves that INCREMENTs cost in total. A long carry always leaves many trailing 0s, so the next INCREMENTs are cheap. The proof depends on that.
Answer
Start the counter at : a 0, then ones ( bits). Then do INCREMENT, DECREMENT, INCREMENT, DECREMENT, and so on. This is pairs of INCREMENT and DECREMENT.
- INCREMENT turns into . All bits flip.
- DECREMENT turns back into . All bits flip again.
- The counter is back in its start state, so the next pair costs the same.
Every operation takes flips, which is . There are operations. The total is .
So with DECREMENT, the average cost per operation can still be : the amortized cost is no better than the worst case.
General method: can a new operation break an amortized bound?
- Find the expensive state of the structure. Here it is (a long carry for INCREMENT).
- Ask: after the expensive operation, is the structure still in an expensive state? If the new operation can push it back, the cheap operations that “paid” for the expensive one never happen.
- Build a sequence that alternates the two operations. Each one costs the worst case. Add up: .
- One such sequence is enough to show that the amortized bound is lost.
Extension 1.1. Add DECREMENT to the counter. Try the potential = number of 1s in the counter, which proves per INCREMENT. Show that it fails for DECREMENT. Take DECREMENT on ( bits).
Answer (Extension 1.1)
- Before: has one 1. .
- After: has ones. .
- Actual cost: all bits flip, so .
- Amortized cost .
For this operation the amortized cost is , not . Here goes up by after an expensive operation. A good potential does the reverse: it drops when an operation is expensive, and pays for it. The number of 1s cannot do that for DECREMENT, because DECREMENT turns 0s into 1s. This matches Q1: no potential can fix it, because the sequence in Q1 really costs .
Q2. Aggregate analysis: power-of-2 costs
Suppose we perform a sequence of n operations on a data structure in which the i-th operation costs i if i is an exact power of 2, and 1 otherwise. Use aggregate analysis to determine the amortized cost per operation.
Concepts: aggregate analysis
- Aggregate analysis finds the worst-case total cost of any sequence of operations. The amortized cost per operation is .
- Every operation gets the same amortized cost, even if some are cheap and some are expensive.
- Here the costs are given for each , so you can add them up directly.
Answer
Take . The costs are:
Split the operations into two groups.
- Powers of 2. Operation costs . The sum is .
- All others. Each costs 1. There are about of them.
The official answer adds the two groups:
So the amortized cost per operation is , that is .
Small error in the official slide. The powers of 2 up to are . That is values, and their sum is . So the other operations number , not . The exact total is
Check with (): . The exact formula gives . The slide formula gives 20. (The slide also writes the sum as , which should read .) The conclusion does not change: , so .
When is not a power of 2. Let be the largest power of 2 with . The expensive operations up to step are the powers of 2 from to . There are of them, and their costs add to . The other operations cost 1 each. So
Because , we have . Then
So for every , a power of 2 or not. The amortized cost is .
Do not bound by here. The extra cheap operations make larger than the value at . For : , but . (new)
General method: aggregate analysis recipe
- Split the operations into cheap (cost ) and expensive ones.
- Count the cheap ones. The count is at most , and each costs 1, so they add at most .
- List the costs of the expensive ones. They usually grow by a constant factor each time, so they form a geometric series. A geometric series is dominated by its last term: .
- Add the totals, bound by , and divide by .
- The answer is a constant if the last (largest) term is at most . (new)
Extension 2.1. The i-th operation costs i if i is an exact power of 3, and 1 otherwise. Use aggregate analysis to find the amortized cost per operation.
Answer (Extension 2.1)
Let be the largest power of 3 not above . So .
- Powers of 3. The costs are . The sum is .
- All others. At most operations, each cost 1. They add at most .
Total: . The amortized cost per operation is .
Check with : costs (operations 1, 3 and 9 are powers of 3). The total is 19, and .
Q3. A potential function for a min-heap
Consider an ordinary binary min-heap data structure with n elements supporting the instructions INSERT and EXTRACT-MIN in O(log n) worst-case time. Give a potential function Φ such that the amortized cost of INSERT is O(log n) and the amortized cost of EXTRACT-MIN is O(1), and show that it works. Does this have any effect on the running time analysis of Heapsort?
Concepts: the potential method
- A potential function maps the state of the structure to a number. It is stored “credit”.
- The amortized cost of an operation is
- Add up over operations: . If , the amortized total is an upper bound for the actual total.
- A min-heap keeps the smallest key at the root. INSERT adds a key at the bottom and moves it up. EXTRACT-MIN removes the root and moves the last key down from the root. Both take in the worst case.
Answer
The potential. Let be the number of elements in the heap. Define
where is a constant to choose. An empty heap has , and always.
INSERT. becomes .
- Actual cost: .
- Change of potential: . The first term is at most , a constant, because . So .
- Amortized cost: .
EXTRACT-MIN. becomes .
- Actual cost: at most , for constants and .
- Change of potential: . Since , we get . So .
- Amortized cost: . Choose . Then the amortized cost is at most .
So INSERT is and EXTRACT-MIN is .
Heapsort. Heapsort builds a heap, then does EXTRACT-MINs. Each has amortized cost . It looks as if the extraction phase costs only . That is wrong. The potential has to come from earlier INSERTs. Heapsort builds the heap without INSERTs, so no one paid for the potential. The heap starts with and drains to . Use the sum rule:
So the running time of Heapsort stays . The potential function has no effect on the analysis.
General method: choose a potential function
- Find the expensive operation and what makes it expensive. Here, EXTRACT-MIN costs , so the potential must fall by about at each extraction.
- Make high just before an expensive operation and low just after it. falls by about when one element leaves. The cheap operation (INSERT) must raise by about the same amount, and that is what it can afford.
- Keep always. Here for an empty heap. Then the amortized total is an upper bound on the actual total.
- Compute (after minus before, never the reverse) for each operation. Bound it: a clean inequality such as is enough. (new)
- Choose the constant ( here) so that the potential drop cancels the actual cost.
- If an algorithm starts with (as Heapsort does), add back to the amortized total to get the actual total.
Extension 3.1. Start from an empty heap. Do INSERTs, then EXTRACT-MINs. Use the potential from Q3 to bound the total actual cost. Is it consistent with a direct count?
Answer (Extension 3.1)
- INSERTs: amortized.
- EXTRACT-MINs: amortized.
- Amortized total: . The heap starts and ends empty, so . The actual total equals the amortized total, so it is also .
Direct count: each of the operations costs at worst, so the total is . The two answers agree.
The difference from Heapsort: here the INSERTs build up the potential, so the EXTRACT-MINs use credit that was really paid. (new)
Q4. Contracting a table by 2/3
Suppose that instead of contracting a table by halving its size when its load factor drops below 1/4, we contract it by multiplying its size by 2/3 when its load factor drops below 1/3. Show that the amortized cost of a DELETE operation that uses this strategy is bounded above by a constant.
Concepts: dynamic tables and load factor
- A dynamic table is an array that changes its size. The load factor is the number of stored elements divided by the table size.
- When the table is full, INSERT expands it: it makes a bigger array and copies every element. This costs one unit per element copied.
- When the table is nearly empty, DELETE contracts it, so memory is not wasted. This also copies every element.
- The rule must keep the load factor away from both triggers after a resize. Then many operations happen before the next resize, and they pay for it.
- In the lecture, the rule is: expand by 2 when full, contract by 1/2 when the load factor drops below 1/4. Here, the question changes the contraction rule only.
Answer
The potential. Let be the table, its number of elements and its size.
is 0 when the table is exactly half full. It grows as the load factor moves away from 1/2.
Case 1/3 < load factor 1/2, no contraction. Here , so . A DELETE lowers num by 1. So goes up by 2.
- Actual cost: 1.
- Amortized cost .
Contraction is triggered. Let the size before be .
- The load factor reaches 1/3, so the number of elements is about .
- The table shrinks to , and it then holds about elements. So , and
- Before the delete, the table held elements. So
- Actual cost: one unit to delete, and about units to copy the remaining elements. So .
- Amortized cost:
The terms cancel. The amortized cost of DELETE is a constant.
Note on the exact boundary (new). The slide uses elements before the delete, so the table holds exactly after it. A strict “below 1/3” trigger would make and . Then and . The sum is still . Only the constant changes.
Case load factor , no contraction (added, not on the slide). Now . A DELETE lowers num by 1, so goes down by 2. Amortized cost . A negative amortized cost is fine: the credit is stored in and it is bounded below by 0. So every DELETE costs at most the constant 3.
General method: check a resizing rule
- Find the load factor right after a resize. Is it far from both triggers (the full table that expands it, and the low load that contracts it)? Here, after contracting to , the table holds elements. The load factor is .
- Count the operations needed to reach a trigger again. From load 1/2, DELETEs lower it to 1/3, and INSERTs fill it. So there are operations before the next resize.
- A resize costs (one copy of each element). Spread over the operations, that is each.
- Choose to match. is 0 at the load factor that a resize leaves. It rises by 2 for each operation that moves away from it. It equals about at the trigger, exactly the cost of the resize. (new)
- Check each case: ordinary operation (the potential changes by a constant) and resize (the copy cost cancels against ).
Extension 4.1. A table doubles its size when it is full. It halves its size when its load factor drops below 1/2. (This differs from Q4: the contraction trigger is 1/2, not 1/3.) Show a sequence of operations where every few operations cost . Which check of the general method does the rule fail?
Answer (Extension 4.1)
Say a table of size has just halved to size . It holds elements, so one slot is free. Follow this sequence.
- INSERT: the table fills up ( elements, size ). No resize.
- INSERT: the table is full, so it doubles to size and copies elements. Now elements, load factor just above 1/2.
- DELETE: elements. Load factor is exactly 1/2, not below. No resize.
- DELETE: elements. Load factor is below 1/2, so the table halves to and copies elements.
The table is back in its start state. Four operations cost about copies. The same four operations can repeat for ever. The cost per operation is about . The amortized cost is not .
Check that fails: step 1 of the method. After halving, the load factor is nearly 1, so the very next INSERT expands again. After doubling, the load factor is just above 1/2, so a DELETE or two triggers a contraction. The resize leaves the table next to a trigger, so no operations pay for it.
The lecture’s rule (contract at 1/4, to size ) leaves a load factor of 1/2 after contraction. That is operations away from both triggers.
Q5. Fibonacci with memoization
Recall the recursive computation of Fibonacci numbers using memoization.
Draw the pruned recursion tree containing all recursive calls needed to calculate Fib(6).
In order to calculate the first n Fibonacci numbers, the memoization approach presented in the lecture needs an array of size O(n). Can the computation be performed using only O(1) memory space?
Concepts: memoization and the recursion tree
- The Fibonacci numbers are , and .
- The regular recursion tree has one node for every call of the plain recursive function. Each node has two children, and . The same sub-problem appears many times: is computed twice and three times in the tree for . That is why the plain version takes time.
- Memoization keeps a dictionary of solved sub-problems. Before a call, check the dictionary. If the answer is there, use it. If not, call, then store the answer.
- In the pruned tree, a sub-problem is expanded only the first time. Every later appearance is a leaf that reads the dictionary.
Answer (a)
The regular tree for has 25 nodes. calls and . calls and , and so on. appears twice, three times, five times. Each appearance is expanded in full.
The pruned tree expands each once. The calls run left first, so the left spine goes down to the base cases. As each call returns, it stores its answer. Then the right child of each node finds the answer in the dictionary.
- Green (base case): and under , and under .
- Yellow (computed for the first time and stored): .
- Purple (read from the dictionary): under , under , and under .
The tree has 11 nodes, against 25 in the regular tree. The slide’s grey nodes are the calls that never happen.
Answer (b)
Yes. depends only on and . So after computing , the numbers before are never read again. Keep only the last two numbers, in 2 variables.
a = 0; b = 1; // F(0), F(1)
for j = 2 to n {
c = a + b; // F(j)
a = b; // F(j-1) becomes the older one
b = c; // F(j) becomes the newer one
}
return b; // F(n), for n >= 1
Time is , the same as the array. Memory is 3 variables (, , ), which is .
Trace for :
| start | 2 | 3 | 4 | 5 | 6 | |
|---|---|---|---|---|---|---|
a |
0 | 1 | 1 | 2 | 3 | 5 |
b |
1 | 1 | 2 | 3 | 5 | 8 |
The loop returns .
(new) This version uses a loop, not recursion. A recursive version still uses stack space for its nested calls, even with no array.
General method: memoization
- Draw the regular tree. Mark the repeated sub-problems.
- Prune it. Expand a sub-problem only the first time. Any later appearance becomes a leaf that reads the dictionary.
- Count the calls. There is one expanded call for each distinct sub-problem, and each expanded call makes a bounded number of other calls. So the total is (distinct sub-problems) (work per call). For Fibonacci: sub-problems, each, so time.
- Look at what each answer needs. If each row depends only on the last few, keep only those: space. If you need to rebuild the whole solution later, keep the full table.
Extension 5.1. Count the calls that the memoized
fibDP(n) makes, including the calls that only read the
dictionary. Give the answer for
and for general
.
Answer (Extension 5.1)
Count the nodes of the pruned tree.
- The left spine has nodes . That is nodes.
- has two children, and . That is 2 nodes.
- Each of has one right child (). That is nodes.
Total: .
For : . This matches the 11 nodes in the figure. Check for small : gives 1 node (), and gives 3 nodes ().
Only of these 11 nodes do real work (the yellow and green nodes). The other are purple dictionary reads. The count is either way. The regular tree has nodes, which is 25 for .
Tutorial 10: Dynamic Programming
Q1. LCS of CAGAG and ACTGG
Find the length of the longest common subsequence and a longest common subsequence of CAGAG and ACTGG by the dynamic programming algorithm in the lecture notes.
Concepts: the c table and the h table
- is the length of an LCS of the first letters of and the first letters of . Row 0 and column 0 are 0, because an empty string has no common letters with anything.
- records which neighbour gave . It is the arrow you follow to read the answer back.
- The lecture code uses three cases, in this order:
x[i] == y[j]gives\(diagonal, take the letter). Otherwisec[i-1][j] >= c[i][j-1]gives|(up, drop ). Otherwise—(left, drop ). The>=is the tie rule: when both neighbours are equal, go up. - Chapter 10 explains why this is correct. This page only fills and traces the tables.
Answer
The lecture code, with = CAGAG ( = 5) and = ACTGG ( = 5):
for i = 1 to n
for j = 1 to m
if x[i] == y[j] {
c[i][j] = c[i-1][j-1] + 1;
h[i][j] = '\';
}
else if c[i-1][j] >= c[i][j-1] {
c[i][j] = c[i-1][j];
h[i][j] = '|';
}
else {
c[i][j] = c[i][j-1];
h[i][j] = '—';
}
The table:
| c | ∅ | A | C | T | G | G |
|---|---|---|---|---|---|---|
| ∅ | 0 | 0 | 0 | 0 | 0 | 0 |
| C | 0 | 0 | 1 | 1 | 1 | 1 |
| A | 0 | 1 | 1 | 1 | 1 | 1 |
| G | 0 | 1 | 1 | 1 | 2 | 2 |
| A | 0 | 1 | 1 | 1 | 2 | 2 |
| G | 0 | 1 | 1 | 1 | 2 | 3 |
The
table (the lecture shows | in column 0 and —
in row 0):
| h | ∅ | A | C | T | G | G |
|---|---|---|---|---|---|---|
| ∅ | — | — | — | — | — | — |
| C | | | | | \ | — | — | — |
| A | | | \ | | | | | | | | |
| G | | | | | | | | | \ | \ |
| A | | | \ | | | | | | | | |
| G | | | | | | | | | \ | \ |
The length of an LCS is .
Now follow the arrows from :
-
is
\. G = G. Write down G. Go to . -
is
|. Go up to . -
is
\. G = G. Write down G. Go to . -
is
|. Go up to . -
is
—. Go left to . -
is
\. C = C. Write down C. Go to . Row 0 stops the walk.
The letters come out backwards: G, G, C. Read them in reverse. LCS = CGG, length 3.
General method: fill and trace an LCS table by hand
- Draw a grid with down the side and across the top. Add a row 0 and a column 0 of zeros.
- Fill row by row, left to right. For each cell, compare the two
letters that label it.
- Same letters: copy the diagonal cell and add 1. Mark
\. - Different letters: take the larger of the cell above and the cell to
the left. If they are equal, take the one above (
|). Mark the one you took.
- Same letters: copy the diagonal cell and add 1. Mark
- The bottom-right cell is the length.
- Start at the bottom-right cell. Follow the marks. Write down a
letter only at a
\. Stop at row 0 or column 0. - Reverse what you wrote.
- Check: the answer must be a subsequence of both strings, and its length must equal .
The grid has cells and each costs , so the time is . The trace walks at most steps.
Extension 1.1. Find the LCS of = ABAB and = BABA. Use the lecture tie rule.
Answer (Extension 1.1)
| c | ∅ | B | A | B | A |
|---|---|---|---|---|---|
| ∅ | 0 | 0 | 0 | 0 | 0 |
| A | 0 | 0 | 1 | 1 | 1 |
| B | 0 | 1 | 1 | 2 | 2 |
| A | 0 | 1 | 2 | 2 | 3 |
| B | 0 | 1 | 2 | 3 | 3 |
Two cells in full:
- : B = B, so .
- :
B
A. The cell above is 3 and the cell to the left is 3. They tie, so take
the one above:
,
mark
|.
Trace:
-
is
|. Go up to . - : A = A. Write A. Go to .
- : B = B. Write B. Go to .
- : A = A. Write A. Go to . Stop.
Reversed: ABA, length 3. BAB is also an LCS of length 3. An LCS is not unique. The tie rule decides which one the trace finds.
Q2. The H-number
The H-number is defined as follows: , and for :
- , when is odd;
- , when is even.
- Give a recursive algorithm to compute for an arbitrary as suggested by the recurrence equation given for . Draw the tree that represents the recursive calls made when is computed.
Concepts: subproblem graph and repeated calls
- A subproblem is one value the answer needs, here one . A subproblem graph has one node per subproblem and an arrow from to when computing needs .
- The graph has no cycles, because every arrow goes to a smaller index. So it is a DAG. You can solve the subproblems in an order where every arrow points to something already solved. That is the order that goes from the sinks (here ) up to the answer.
- A call tree is different. It has one node for each call. If two calls ask for the same , the tree shows both. Repeated nodes in the call tree are the wasted work that dynamic programming removes.
Answer (a)
int hn(int n) {
if (n == 0) return 1;
else {
S = 0;
if (n mod 2) j = n-1;
else j = n-2;
for (k = 0; k <= j; k = k+2)
S += hn(k);
}
return S;
}
The loop starts at and steps by 2. So it adds , where is the largest even number below . For odd this is . For even this is . Both cases of the definition reduce to a sum over the even indices below .
The tree has 16 nodes. The call for appears 8 times and the call for appears 4 times.
Answer (a), call counts
(new) Let be the number of calls made by , counting the first call. Then , , , and . In general . The recursive algorithm takes exponential time, because it solves the same again and again.
- Draw the subproblem graph for and .
Answer (b)
uses only the even indices: nodes 8, 6, 4, 2, 0. Each node has an arrow to every even node below it. There are 5 nodes and 10 arrows.
needs . So the graph for is the graph for plus one new node 9, with an arrow to each of 8, 6, 4, 2, 0. There are 6 nodes and 15 arrows.
Both graphs have 5 or 6 nodes, not 9 or 10, so a DP needs few subproblems. A valid order to solve them is first, then , , and so on up to the target.
- Write an iterative algorithm using dynamic programming (bottom-up). Analyze time and space complexity.
Answer (c)
int hn_DP(int n)
{ // Make use of an array S[0..n]
S[0] = 1;
for (i = 1; i <= n; i++) {
S[i] = 0;
if (i mod 2) j = i-1;
else j = i-2;
while (j >= 0) {
S[i] += S[j];
j -= 2;
}
}
return S[n];
}
The array stores every as it is found. Each only needs smaller indices, and those are already stored.
- Space: , for the array .
- Time: . The loop for adds about values. The total is about .
The slide then asks: Is there a faster algorithm? Yes. See the next drawer.
Answer (c), a faster algorithm
(new) The slide asks the question and gives no answer. Here is one.
For odd with , the terms are . For even , the terms are the same list. So:
- for .
For odd with , the terms are plus . That second part is exactly . So:
- for .
- . The rule does not hold, because has only one term.
Check against the definition:
| n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| H(n) | 1 | 1 | 1 | 2 | 2 | 4 | 4 | 8 | 8 |
Each step looks only at the previous value, so one variable is enough. Loop from to : if is odd, double the value. If is even, keep it. Start from 1. This runs in time and space. (In fact for , but the loop shows the DP idea.)
General method: from a recurrence to a DP
- Write the recurrence. Find its subproblems: the values the answer calls for.
- Draw the subproblem graph. Check that it is a DAG (every arrow goes to a smaller problem). If a call tree repeats nodes, DP will help.
- Fix an order that solves every subproblem after the ones it points to. This is the reverse of a topological order. Here it is .
- Store each answer in an array indexed by the subproblem. Fill it in that order.
- Time (number of subproblems) (work per subproblem). Here: subproblems, each adds up to stored values, so .
- Space the size of the array, unless a later step needs only the last few values.
- Look for a pattern in the recurrence. When two subproblems share almost all their terms, one can be computed from the other in time. That is how drops to .
Extension 2.1. Use the rule to compute and . Check against the definition.
Answer (Extension 2.1)
Start from .
- (9 is odd).
- (10 is even).
- (11 is odd).
Check from the definition: . It matches.
Q3. Binomial coefficients
The binomial coefficients can be defined by the recurrence equation:
- for and ;
- for ;
- for .
is also called “n choose k”. It is the number of ways to choose distinct objects from a set of objects.
- Give a recursive algorithm as suggested by the recurrence equation given for .
Answer (a)
int C(int n, int k)
{
if (k == 0) return 1;
if (n == 0) return 0;
return C(n-1, k-1) + C(n-1, k);
}
The two base cases come first: gives 1 and gives 0. Because the test is first, , as the definition says.
- Draw the subproblem graph for .
Answer (b)
Each node with and has two arrows: to and to . Nodes with or are base cases and have no arrows. The graph has 18 nodes, not the 24 cells of a full grid, because the answer never needs , , , , , .
Several nodes have two incoming arrows. For example is needed by both and . A plain recursion would solve it twice.
- Implement a top-down dynamic programming algorithm with memoization. State the data structure used for the dictionary.
Answer (c)
The dictionary is a two-dimensional array
int dic[n+1][k+1], with every entry set to
-1 at the start. The value -1 means “not computed yet”. A real
answer is never negative, so no clash is possible.
int C(int n, int k, int [][] dic)
{
int c1, c2;
if (k == 0) {
dic[n][0] = 1;
return 1;
}
if (n == 0) {
dic[0][k] = 0;
return 0;
}
if (dic[n-1][k-1] == -1)
c1 = C(n-1, k-1, dic);
else c1 = dic[n-1][k-1];
if (dic[n-1][k] == -1)
c2 = C(n-1, k, dic);
else c2 = dic[n-1][k];
dic[n][k] = c1 + c2;
return dic[n][k];
}
Before each recursive call, the code looks in dic. It
calls only if the entry is -1. So each subproblem is solved once, and
every later request reads the stored value.
- Time: . There are at most subproblems and each does work besides its calls.
- Space: for the array (plus for the recursion stack).
- Implement a bottom-up approach and analyze its time and space complexity.
Answer (d)
int C(int n, int k)
{
int dic[n+1][k+1];
for (i = 1; i <= k; i++) dic[0][i] = 0;
for (i = 0; i <= n; i++) dic[i][0] = 1;
for (i = 1; i <= n; i++)
for (j = 1; j <= k; j++)
dic[i][j] = dic[i-1][j-1] + dic[i-1][j];
return dic[n][k];
}
The first two loops fill the base cases: row 0 and column 0. The double loop then fills row by row. Each cell reads the row above, which is already full.
- Time: .
- Space: .
The slide also gives a more optimized version:
int C(int n, int k) // more optimized
{
int dic[n+1][k+1];
for (i = 1; i <= k; i++)
dic[0][i] = 0;
for (i = 0; i <= n-k; i++)
dic[i][0] = 1;
for (i = 1; i <= n; i++)
for (j = max(i-(n-k), 1); j <= k; j++)
dic[i][j] = dic[i-1][j-1] + dic[i-1][j];
return dic[n][k];
}
It fills only the cells that really uses. Each step down a row adds 0 or 1 to , so from cell you can reach only if . That means , which is the loop start. Cells to the left of that are never read, so the code skips them. For the same reason only the column 0 cells with are needed. This is the shape of the graph in part (b). The loops visit about cells. The worst-case bound is still time and space.
General method: memoized and bottom-up DP
Both methods use the same subproblem graph. They differ only in the order.
| Top-down (memoization) | Bottom-up | |
|---|---|---|
| Order | Follows the recursion. Starts at the answer. | You choose it: a reverse topological order. |
| Table start | All entries -1 | Base cases filled first |
| Solves | Only the subproblems the answer needs | All the cells in the loops |
| Cost | Stack space and call overhead | None. Loops only. |
- Time is (number of distinct subproblems) (work per subproblem). For : .
- Space is the table. If a row needs only the row above, you can keep one row. See Extension 3.1.
- To restrict the loops (the optimized version), ask which cells can reach the answer in the graph, and loop over those.
Extension 3.1. Fill the bottom-up table for . Then show that one row of size is enough.
Answer (Extension 3.1)
Cell is the sum of the cell above and the cell above-left.
| i \ j | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 0 |
| 2 | 1 | 2 | 1 | 0 |
| 3 | 1 | 3 | 3 | 1 |
| 4 | 1 | 4 | 6 | 4 |
| 5 | 1 | 5 | 10 | 10 |
So . Two cells in full: . .
(new) Row
reads only row
.
Keep one array row[0..k], with row[0] = 1 and
the rest 0. For each
from 1 to
,
run
downward from
to 1 and do row[j] = row[j] + row[j-1]. Going downward
matters: row[j-1] must still hold the old value from row
when you read it. Going upward would use the new value and give a wrong
answer. Time is still
.
Space falls to
.
Q4. Knapsack
Given a knapsack of size 10 and 4 objects with:
- Profits (p): {10, 40, 30, 50},
- Sizes (s): {5, 4, 6, 3},
determine the subset maximizing profit using dynamic programming.
Concepts: the 0/1 knapsack table
- Each object is taken once or not at all. The goal is the largest total profit with total size at most .
- is the best profit that uses only objects in a knapsack of size . The rows are capacities . The columns are objects . Column 0 (no objects) and row 0 (no space) are 0.
- To fill a cell, ask about object . Leave it: the value is . Take it (only if ): the value is . Keep the larger.
- Chapter 10 has the full argument.
Answer
The lecture code (the slide calls the sizes w):
for r = 1 to C
for c = 1 to n
profit[r][c] = profit[r][c-1];
if (w[c] <= r)
if (profit[r][c] < profit[r-w[c]][c-1] + p[c])
profit[r][c] = profit[r-w[c]][c-1] + p[c];
The table with . Objects: 1 (size 5, profit 10), 2 (size 4, profit 40), 3 (size 6, profit 30), 4 (size 3, profit 50).
| r \ c | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0 | 0 | 50 |
| 4 | 0 | 0 | 40 | 40 | 50 |
| 5 | 0 | 10 | 40 | 40 | 50 |
| 6 | 0 | 10 | 40 | 40 | 50 |
| 7 | 0 | 10 | 40 | 40 | 90 |
| 8 | 0 | 10 | 40 | 40 | 90 |
| 9 | 0 | 10 | 50 | 50 | 90 |
| 10 | 0 | 10 | 50 | 70 | 90 |
The best profit is .
Four cells in full:
- : object 1 has size 5, and . It does not fit. Copy the left cell: 0.
- : . Take it: . Value 10.
- : leave object 3: . Take it: . , so the value is 70.
- : leave object 4: . Take it: . , so the value is 90.
The slide gives the table and the code only. It does not name the subset. Trace it back. (new) At cell : if , object is taken, so move to . If they are equal, object is left, so move to .
- : . Take object 4 (size 3). Go to .
- : . Leave object 3. Go to .
- : . Take object 2 (size 4). Go to .
- : . Leave object 1. Done.
The subset is {object 2, object 4}, with total size and total profit .
General method: fill a knapsack table and trace it back
- Make a grid. Rows are capacities . Columns are objects . Fill row 0 and column 0 with 0.
- Fill row by row, left to right. For cell
:
- Copy the cell on the left (leave object ).
- If , compute . This looks one column left and rows up. If it is larger, use it.
- The bottom-right cell is the best profit.
- Trace back from . If the cell differs from its left neighbour, take object and go to . If equal, go to . Stop at column 0.
- Check: the sizes of the taken objects add to at most , and the profits add to the bottom-right cell.
Time and space are both . This is pseudo-polynomial: it depends on the number , not on the number of bits in .
Extension 4.1. A knapsack has size 10. There are three objects with sizes {6, 5, 5} and profits {7, 5, 5}. A greedy rule takes objects in order of profit per unit of size, and stops when the next one does not fit. Show that it is not optimal.
Answer (Extension 4.1)
The ratios are , and . Greedy takes object 1 first (size 6). The space left is 4. Neither remaining object (size 5) fits. Greedy ends with profit 7.
The DP table gives the optimum:
| r \ c | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 5 | 0 | 0 | 5 | 5 |
| 6 | 0 | 7 | 7 | 7 |
| 10 | 0 | 7 | 7 | 10 |
Cell : leave object 3 gives . Take it gives . So the value is 10. The trace takes objects 3 and 2 (sizes 5 + 5 = 10). The optimum is 10.
Greedy fails because an object cannot be split. It commits to object 1 and wastes 4 units of space. The DP tries both choices for every object.
Q5. Alignment of two sequences
Let and be sequences of characters from {‘a’, ‘c’, ‘g’, ‘t’}. Alignment involves inserting ’_’ so both sequences have the same length. The cost is the number of insertions. Example: = “ctatg”, = “ttaagc”, = “ct_at_g_”, = “_tta_agc”. The cost is 5.
- Give a recursive definition of alignment(, ).
Answer (a)
Let be the least cost to align the first characters of with the first characters of . The official analysis:
- Two base cases. (i) is empty: must have ‘’ characters, so the cost is . (ii) is empty: must have ’’ characters, so the cost is .
- Both sequences are not empty. Look at the last
characters. There are two possibilities.
- . No insertion is needed. The last character of and of is this character. Before it comes the best alignment of the shorter strings: .
- . Either align the last character of with ‘’, and find . Or align the last character of with ’’, and find . Take the smaller and add 1 for the new ’_’.
The recursive definition:
- if
- otherwise
(new) A mismatch is never paired with a mismatch. The alignment may only insert ‘’, so two different letters cannot share a column. The only choices are to put a ’’ under one of them.
- Draw the subproblem graph for alignment(3, 4).
Answer (b)
The nodes are all pairs with and :
| (0, 0) | (0, 1) | (0, 2) | (0, 3) | (0, 4) |
| (1, 0) | (1, 1) | (1, 2) | (1, 3) | (1, 4) |
| (2, 0) | (2, 1) | (2, 2) | (2, 3) | (2, 4) |
| (3, 0) | (3, 1) | (3, 2) | (3, 3) | (3, 4) |
For each where and , there is an edge to , and . Nodes in row 0 and column 0 are base cases and have no edges.
The graph has 20 nodes and edges. Every edge goes up, left, or up-left, so the graph is a DAG. Solving the rows from top to bottom, and each row from left to right, solves every node after the nodes it points to.
(The recurrence uses the diagonal edge only when the characters match, and the two other edges only when they do not. The graph shows all the edges that any input could use.)
- Design a dynamic programming algorithm of alignment(, ) using the bottom-up approach.
Answer (c)
for (r = 0 to n1) cost[r][0] = r;
for (c = 1 to n2) cost[0][c] = c;
for (r = 1 to n1)
for (c = 1 to n2)
if (S1[r] == S2[c])
cost[r][c] = cost[r-1][c-1];
else if (cost[r-1][c] < cost[r][c-1])
cost[r][c] = cost[r-1][c] + 1;
else
cost[r][c] = cost[r][c-1] + 1;
return cost[n1][n2];
The first two lines fill the base cases. The loops then follow the
order of the graph in part (b). The else if picks the
smaller of the cell above and the cell to the left, then adds 1. This is
the
of part (a).
- Time: .
- Space: .
(new) The table for = “ctatg” (rows) and = “ttaagc” (columns):
| cost | ∅ | t | t | a | a | g | c |
|---|---|---|---|---|---|---|---|
| ∅ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| c | 1 | 2 | 3 | 4 | 5 | 6 | 5 |
| t | 2 | 1 | 2 | 3 | 4 | 5 | 6 |
| a | 3 | 2 | 3 | 2 | 3 | 4 | 5 |
| t | 4 | 3 | 2 | 3 | 4 | 5 | 6 |
| g | 5 | 4 | 3 | 4 | 5 | 4 | 5 |
The answer is , as the question states. Two cells in full:
- : t = t, so it copies the diagonal cell .
- :
c
t. The cell above is
and the cell to the left is
.
The test “above
left” fails, so the
elsebranch runs: .
General method: design a DP from the last decision
- Define the subproblem as a prefix pair: is the answer for the first items of one input and the first of the other.
- Ask what happens to the last item of each prefix. List every choice. Here: the last characters are matched in one column (free if equal), or one of them sits above a ’_’ (cost 1).
- Each choice leaves a smaller prefix pair. Write as the best of those choices.
- Write the base cases: one prefix empty.
- The subproblem graph is the grid of pairs. Edges go to smaller , smaller , or both. Fill the grid row by row.
- Time is (number of cells) (choices per cell) . Space is the grid.
This is the same shape as the LCS in Q1. Both are two-string prefix-pair tables.
Extension 5.1. Show that . Check it on “ctatg” and “ttaagc”.
Answer (Extension 5.1)
(new) Look at an alignment. Each column has two characters, or one character and a ‘’. Call a column matched if it holds the same character in both rows. The matched columns, read left to right, spell a common subsequence of and . Every other character of or sits above or below a ’’.
- If columns are matched, characters of and of are used. The other characters of and of each need one ’_’.
- The cost is .
- The cost is smallest when is largest. The largest is the length of an LCS. So the minimum cost is .
(The two characters in a column are never different letters, because an alignment can only insert ’_’.)
Check on the example. Take = “ctatg” and = “ttaagc”. An LCS is “tag”: the t at position 2 of and position 1 or 2 of , then a, then g. It has length 3.
Length 4 is not possible. The LCS table of the two strings has bottom-right cell 3:
| c | ∅ | t | t | a | a | g | c |
|---|---|---|---|---|---|---|---|
| ∅ | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| c | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| t | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
| a | 0 | 1 | 1 | 2 | 2 | 2 | 2 |
| t | 0 | 1 | 2 | 2 | 2 | 2 | 2 |
| g | 0 | 1 | 2 | 2 | 2 | 3 | 3 |
So . This matches the cost 5 in the question and the cell in part (c).