29 — Problem Sets, Worked
The rest of the track teaches technique. This file is where technique meets the kind of question that actually appears on a problem set or an exam: the phrasing is terse, the work is yours, and partial credit depends on writing the right three sentences rather than the right thirty.
Every problem here is worked end to end. Cover the solution and attempt it on paper first. Reading a solution you did not attempt produces a strong feeling of understanding and almost no ability, which is the single most common way people lose marks on material they "knew".
Assumes files 22 through 24. Sets A through E are first-half material; set F is divide-and-conquer design.
29.1 What the six problem types are
Problem sets in this material recycle six shapes. Recognizing which one you are looking at is most of the work.
| Set | Shape | The skill being tested |
|---|---|---|
| A | Here is code, give a tight | Translating control flow into a recurrence or a sum |
| B | Here is an expression, give a tight | Log identities, and knowing what the variable is |
| C | Order these functions by growth | Comparing by logs, and the notation's edge cases |
| D | Here is a counting problem, write a recurrence | Conditioning on one decision, exhaustively and disjointly |
| E | Here is a recurrence, give a tight | Level sums and the geometric ratio test |
| F | Here is a problem, design an algorithm | Divide and conquer, plus proving the combine step |
Sets A, B, C and E ask for tight bounds. A correct where a was asked for is a partial answer, and saying when the truth is is simply wrong even though the statement is true.
29.2 Set A: reading a running time off code
A1. A recursion whose parameter goes both up and down
STRANGE(n)
1 if n <= 10
2 return n
3 if n is even
4 return STRANGE(n / 2)
5 else
6 return STRANGE(n + 3)Give a tight bound on the worst-case running time.
The difficulty. Line 6 makes the argument larger. The standard "each call shrinks the input" reasoning does not apply, and the recursion is not obviously terminating, let alone fast.
Step 1: look at two calls at a time, not one. If n is odd, then is even, so the very next call takes the even branch. Odd inputs therefore never appear twice in a row, and it is enough to understand the two-call block
n -> n + 3 -> (n + 3)/2Step 2: show the block still shrinks by a constant factor. We want , which rearranges to , that is . The odd branch is only reached when , so the condition holds every time it matters, with room to spare. Even inputs shrink by a full half in one call, which is better. So:
every two calls reduce
nto at most of its value.
Step 3: count. Starting from n, after calls the argument is at most . The recursion stops once the argument drops to 10 or below, which happens once , that is . Each call does work of its own. So the running time is .
Step 4: the matching lower bound, which is the half people skip. The question said , so an upper bound alone is half an answer. No single call reduces the argument by more than a factor of 2 (halving is the best case; adding 3 makes it worse). So after k calls the argument is at least , and reaching the base case at 10 needs , that is . Hence .
Answer: .
What earns full marks. Three sentences: odd is always followed by even, so two calls take n to ; that is a constant-factor shrink so calls; no call beats halving so .
The trap to avoid. Do not try to write a recurrence like and feed it to the master theorem. It is not that kind of recurrence. When the parameter does not shrink monotonically, argue about the potential function directly (here, the value of n itself) instead of reaching for machinery.
A2. Nested loops on top of a recursion
FINDPAIR(A[1..n])
1 if n == 1
2 return False
3 mid = ceil(n/2)
4 for i = 1 to mid
5 for j = mid + 1 to n
6 if A[i] == A[j]
7 return True
8 return ( FINDPAIR(A[1..mid]) OR FINDPAIR(A[mid+1..n]) )(a) Give a tight bound on the worst-case running time. (b) Say in one sentence what the procedure computes.
(a) The non-recursive work. Lines 4 to 7 are a double loop running mid * (n - mid) times. With mid = ceil(n/2) both factors are , so the loop nest is .
The worst case is what makes the recursion run. If line 7 fires, the procedure returns immediately and does no recursion at all, so the fast case is an array full of duplicates. For the worst case, hand it an array whose entries are all distinct: line 7 never fires, both recursive calls run in full, and
T(n) = 2 T(n/2) + Theta(n^2), T(1) = Theta(1)Solve it. Level i holds subproblems of size , each costing , so the level sum is
2^i * (n / 2^i)^2 = n^2 / 2^iConsecutive levels have ratio , a decreasing geometric series, so the root dominates and the total is .
(By the master theorem, , , , and is polynomially larger, so case 3, with the regularity condition holding at . Either route is fine; the level sum is faster to write.)
Answer: .
(b) It returns True exactly when contains a repeated value, and False when all n entries are distinct.
Why that is right, since "one sentence" still has to be a true sentence. If two equal entries exist at positions , follow the recursion down: at every level they are either in the same half, in which case the recursion keeps them together, or in different halves, in which case the double loop at that level compares them directly and returns True. They cannot stay together forever, because subarrays shrink to length 1. So some level splits them and catches them.
The lesson from A2. Notice that the worst case came from the early exit, not from the loops. Any time code can return early, ask "what input prevents the early return?" before writing the recurrence, because that input is the worst case.
29.3 Set B: asymptotics of an expression
No algorithms here, just algebra. Two identities carry almost all of these:
a^(log_b c) = c^(log_b a) the swap identity
n^(1/lg n) = 2 a constant, not a function of nThe second is worth proving to yourself once, because it looks wrong: .
B1. lg^3(n) + 5^(lg lg n) + lg^2(lg^4 n)
Recall the convention from 21.6: means , not .
Take the terms one at a time.
- .
- . Apply the swap identity with
nreplaced by : . Since , this is . - .
Now compare. The first is , the second is , and since the first dominates the second. The third is only , far below both. A sum is of its largest term (22.12, answer 8).
Answer: .
The trap: reading as exponential. It is not. Anything of the form c^(log of something) is a polynomial in that something, and the swap identity is how you see it.
B2. The number of bits needed to write 12^n in binary
The number of bits to write an integer in binary is . So
lg(12^n) + 1 = n lg 12 + 1 = Theta(n)Answer: .
Why this problem exists. It is the input-size point from 22.11 in disguise. The value is exponential; the number of bits to write it down is linear. Confusing the two is what makes pseudo-polynomial algorithms look polynomial, and that distinction is the entire content of half of file 28.
B3. A population doubles every year. Give an asymptotic bound on its size 100 years from now.
Starting from one billion, the answer is . That is a specific, enormous, fixed number. It does not depend on n, or on anything else that grows.
Answer: .
The point. Asymptotic notation describes how a quantity behaves as some variable goes to infinity. If nothing is going to infinity, every answer is , no matter how large the constant. is the same joke as in 22.1, and the reflex the question is testing is identify the variable before answering. If a problem gives you a fixed horizon, there is no variable, and the answer is a constant.
B4. sum_{i=1}^{n} log(n/i)
Split the logarithm and pull the constant out:
sum_{i=1}^{n} log(n/i) = sum_{i=1}^{n} (log n - log i)
= n log n - sum_{i=1}^{n} log i
= n log n - log(n!)Now use Stirling (21.6), in the form that matters:
ln(n!) = n ln n - n + O(log n)so, working in base for cleanliness and converting at the end,
sum = n ln n - (n ln n - n + O(log n)) = n - O(log n) = Theta(n)Answer: .
The sanity check that catches an algebra slip: the terms run from down to , and the average term is a constant (most of the i values are within a constant factor of n, and for those is ). n terms with constant average gives , which agrees.
Why the crude bound is not enough. Bounding every term by gives , which is true and loose. The question asked for , so the cancellation is compulsory.
29.4 Set C: ordering functions by growth
The full-length version of this exercise is the single best hour you can spend on asymptotics, because everything that can go wrong with the notation goes wrong somewhere in the list.
The five moves
1. Take logs. is strictly increasing, so implies . Use it on anything with an exponent. The converse fails: does not give , since n and have logs within a constant factor. So logs prove strict separations, never ties.
2. The swap identity. . It turns into and into .
3. Watch for disguised constants. . Anything whose log is bounded is .
4. Bounded oscillation is . sin(n) + 3 lives in forever. It has no limit, so the limit method of 22.5 does not apply, but the definition does: it is between two positive constants, so it is . Mentioning that the limit does not exist and the definition still settles it is exactly the kind of remark that earns a mark.
5. Know the two slow functions cold. grows slower than , and it absorbs towers: and , both . Any fixed number of s applied inside a changes it by a constant.
The worked list
Order these 28 functions from smallest to largest, grouping ties.
sin(n) + 3 n^(2/lg n) lg*(2^n) lg*(lg lg n)
lg(lg^3 n) 7 + lg lg n (lg n)^(1/3) sum_{i=1}^{n} 1/i
ceil(lg n) lg(n!)/n (lg n)^50 n^(2/lg lg n)
n^(1/99) n 3^(log_3 n) n lg^2 n
(sqrt n)^3 9^(log_3 n) sum_{i=1}^{n} i (n-7)^2.5
sum_{i=1}^{n} i^2 (lg* n)^(lg n) (lg n)^(lg n) n^(lg lg n)
n^(sqrt n) 1.001^n 2^n n!Functions on the same line are of one another. Each line is of the line below.
1. sin(n) + 3, n^(2/lg n) Theta(1)
2. lg*(2^n), lg*(lg lg n) Theta(lg* n)
3. lg(lg^3 n), 7 + lg lg n Theta(lg lg n)
4. (lg n)^(1/3)
5. sum 1/i, ceil(lg n), lg(n!)/n Theta(lg n)
6. (lg n)^50
7. n^(2/lg lg n)
8. n^(1/99)
9. n, 3^(log_3 n) Theta(n)
10. n lg^2 n
11. (sqrt n)^3 Theta(n^1.5)
12. 9^(log_3 n), sum i Theta(n^2)
13. (n-7)^2.5
14. sum i^2 Theta(n^3)
15. (lg* n)^(lg n)
16. (lg n)^(lg n), n^(lg lg n)
17. n^(sqrt n)
18. 1.001^n
19. 2^n
20. n!Line by line, why
Line 1. sin(n) + 3 is bounded in , so by move 4. , a constant by move 3. Two functions that look nothing alike and are the same size.
Line 2. and , both by move 5.
Line 3. . And differs by an additive constant. Both .
Line 4. beats , because taking logs gives versus , and the first is larger. Any positive power of beats any number of stacked logs, the same way any positive power of n beats any power of .
Line 5. (21.6). , since the ceiling changes it by less than 1. And using from 22.12. Three different-looking routes to the same place.
Line 6. is polylogarithmic, above every fixed power of below 50 and below the next entry.
Line 7. . Take logs: . Compare against polylog, whose log is : since beats for every constant , this is larger than every polylog. Compare against , whose log is : since , this is smaller than every polynomial. It sits strictly between the two families. Such functions are called quasi-polynomial, and the list contains one precisely to check you do not assume everything is either polylog or polynomial.
Line 8. is a genuine polynomial, however small the exponent, so it beats everything above it.
Line 9. by the definition of a logarithm. Do not overthink it.
Line 10. is above n and below every , since .
Line 11. .
Line 12. : use the swap identity, or note so . And .
Line 13. , because subtracting a constant inside a fixed power changes the value by a constant factor for large n.
Line 14. .
Line 15. . Take logs: . Compare against any polynomial , whose log is . Since , however slowly, eventually exceeds for every constant . So this function is above every polynomial, despite being built from the slowest-growing function in the list. That inversion is the whole point of the entry: is tiny, but copies of it multiplied together is not.
Line 16. by the swap identity, so the two entries are literally the same function written two ways, hence of each other. Their common log is , which beats line 15's because dominates .
Line 17. has log , which beats because beats outright.
Line 18. has log , and n beats . This is the entry that catches people: a base barely above 1 still beats , because exponential beats everything sub-exponential eventually, and "eventually" is the only word in the definition.
Line 19. over : the ratio . Two exponentials with different bases are never of each other.
Line 20. over : the ratio goes to infinity (22.6). is roughly , whose log is , against n for .
The three answers that lose marks here
- Writing
nand on the same line because their logs agree. Move 1's converse does not hold. - Declaring
sin(n) + 3incomparable because it has no limit. It is bounded, therefore , and the definition never mentioned limits. - Putting down among the logarithms because it is "built from ". Check the log of the whole expression, not the mood of its parts.
29.5 Set D: writing a recurrence from a counting problem
You are given a combinatorial object and asked for a recurrence counting how many there are. You are usually not asked to solve it, and solving it when it was not asked for wastes time you need elsewhere.
The method
- Find one decision that every object in the set has to make. Usually the leftmost piece, the bottom item, the first character.
- Enumerate the possible values of that decision. They have to be exhaustive (every object makes one of them) and disjoint (no object makes two), or you will over- or under-count.
- For each value, the rest of the object is a smaller instance of the same problem. Write down its size.
- Add the cases. Multiply only when two independent choices are made at once.
- State every base case, and state enough of them: a recurrence reaching back
ksteps needskbase cases.
Step 5 is where marks are lost most often, and it costs one line.
D1. Tiling a 2-by-n strip with dominoes
A rectangle is to be covered exactly by
ndominoes, each , placed either horizontally or vertically. Write a recurrence for the number of coverings, .
The decision: what covers the leftmost column? Every tiling covers the two squares of column 1 somehow, and there are exactly two ways:
- One vertical domino fills the column. What remains is a rectangle: ways.
- Two horizontal dominoes, stacked, cover the top and bottom of columns 1 and 2. What remains is a rectangle: ways.
Nothing else can reach column 1: a single horizontal domino covering only one square of the column would leave the other square unreachable, since its only remaining neighbour is below or above it in the same column and that is the vertical case. The two cases are disjoint, because the top-left square is covered by a vertical piece in one and a horizontal piece in the other.
T(n) = T(n-1) + T(n-2) for n >= 3
T(1) = 1 one vertical domino
T(2) = 2 two vertical, or two horizontalThat is the Fibonacci recurrence, so and , though the question did not ask.
Why and are both needed: the recurrence reaches back two steps, so one base case is not enough to start it.
D2. Stacking plates of two thicknesses
You have an unlimited supply of red, blue, and green plates. Red and blue are 1 unit thick; green is 3 units thick. Write a recurrence counting the colour orderings of a stack of total thickness exactly
n.
The decision: what is the bottom plate? Exactly three cases, and they are disjoint because a stack has one bottom plate of one colour:
- Red, thickness 1, leaving thickness to fill: orderings.
- Blue, thickness 1, likewise .
- Green, thickness 3, leaving : .
T(n) = 2 T(n-1) + T(n-3) for n >= 3
T(0) = 1 the empty stack, exactly one way
T(1) = 2 red or blue
T(2) = 4 two independent choices of 1-unit plateThree base cases, because the recurrence reaches back three. And , not 0: there is exactly one way to build nothing, which is to build nothing. Setting silently kills every stack that ends with a green plate at the top, and is the most common error on this type.
A check worth doing. should be . By hand: stacks of three 1-unit plates, plus one all-green stack. Nine. Verifying one small case by hand catches a wrong base case immediately, and takes fifteen seconds.
D3. A variant to attempt yourself
Same plates, but now green plates may not be adjacent to each other. Write a recurrence.
Answer. Condition on the bottom plate again. The trouble is the green case: after placing a green plate you may not place another one directly on top, so "the rest of the stack" is no longer an unconstrained instance of the same problem, and a single-variable recurrence cannot express it.
The fix is the standard one from LIS in 25.5: add a parameter recording the constraint. Let be the number of stacks of thickness n whose bottom plate is not green, those whose bottom plate is green:
T(n, 0) = 2 ( T(n-1, 0) + T(n-1, 1) )
T(n, 1) = T(n-3, 0) the plate above a green one cannot be green
answer = T(n, 0) + T(n, 1)with , , and both zero for negative n. When a constraint refers to a neighbouring choice, put the neighbouring choice in the state. That is the same move as LIS(prev, start) and the same move as MIS(v, bit) for independent sets on trees, and recognizing it is worth more than this particular answer.
29.6 Set E: bounding recurrences by level sums
The instruction "state the level sum" is an instruction to do the tree method (23.5) explicitly rather than quote the master theorem. Write the level sum, take the ratio of consecutive levels, and read off which of the three cases you are in.
The mechanical procedure
For :
level i has a^i subproblems of size n/b^i
level sum L_i = a^i * f(n / b^i)
ratio L_{i+1} / L_i
ratio < 1 (bounded away) -> decreasing, ROOT dominates -> T(n) = Theta(f(n))
ratio = 1 -> every level equal -> T(n) = Theta(f(n) * log_b n)
ratio > 1 (bounded away) -> increasing, LEAVES dominate -> T(n) = Theta(number of leaves)
= Theta(n^(log_b a))The number of leaves is , which is worth re-deriving once and then memorizing.
E1. T(n) = 4 T(n/2) + n
L_i = 4^i * (n / 2^i) = n * 2^iIncreasing by a factor of 2 per level, so the leaves dominate. Leaves: .
.
E2. T(n) = 3 T(n/2) + n^(3/2)
L_i = 3^i * (n / 2^i)^(3/2) = n^(3/2) * (3 / 2^(3/2))^i, so the ratio is . Increasing, leaves dominate. Leaves: .
, about .
Note how close this was: the ratio is only 6 percent above 1, and the answer is only slightly above the root cost . Close cases are exactly why you compute the ratio instead of eyeballing it.
E3. T(n) = 9 T(n/3) + n^2
L_i = 9^i * (n / 3^i)^2 = 9^i * n^2 / 9^i = n^2Every level costs the same. The depth is .
.
E4. T(n) = 5 T(n/3) + n^2
L_i = 5^i * (n / 3^i)^2 = n^2 * (5/9)^iRatio , so the level sums die off geometrically and the root carries the whole cost. The root alone costs , which is also the lower bound, since level 0 is part of the total.
.
E1 to E4 are the complete set of cases for the shape , and it is worth seeing them side by side once, because which one you are in is decided entirely by the ratio :
| Recurrence | Level sums | Dominated by | Answer | |
|---|---|---|---|---|
| E1, | increasing | leaves | ||
| E2, | increasing | leaves | ||
| E3, | all equal | every level | ||
| E4, | decreasing | root |
Note E3 and E4 differ only in , 9 against 5, and that single change moves the answer by a factor. Compute the ratio; never guess from the shape of .
E5. T(n) = T(n/4) + T(n/6) + n
Unequal splits, so the master theorem does not apply. Use the level-ratio lemma from 23.11: for T(n) = T(an) + T(bn) + n, consecutive level sums satisfy . Here
a + b = 1/4 + 1/6 = 5/12 < 1Decreasing geometric, so the root dominates, and the root alone costs n, which is also the lower bound.
.
E6. T(n) = T(n/7) + T(n/11) + sqrt(n)
Same shape, but the combine cost is , not n, so the fractions have to be raised to the matching power before they are summed. A subproblem of size m hands its children
sqrt(m/7) + sqrt(m/11) = sqrt(m) ( 7^(-1/2) + 11^(-1/2) )and . Decreasing geometric, root dominates, root costs .
.
The trap this one sets: summing instead of . The comparison has to be between the costs, not the sizes. Here both sums are below 1 so the answer survives the error, which is precisely why the habit is dangerous.
E7. T(n) = T(sqrt n) + 3
Every level costs 3, so the answer is 3 * depth and the only question is the depth. After i levels the argument is . It reaches a constant when
n^(1/2^i) = c => (1/2^i) lg n = lg c => lg n = 2^i lg c
=> lg lg n = i + lg lg c => i = lg lg n - O(1).
The move to remember: how many square roots until constant is , the same way how many halvings until constant is . Alternatively, substitute , which turns into , immediately . That is the change of variables in 23.9, and it is the faster route once you trust it.
E8. T(n) = 2 T(sqrt n) + lg n
Same substitution, , so , which is the mergesort recurrence: .
.
E9. T(n) = T(lg n) + lg n
Now the argument collapses logarithmically, so the costs going down the tree are
lg n, lg lg n, lg lg lg n, ...and the number of terms is by definition. The first term is ; every later term is at most ; so the tail is at most , which is because grows slower than any iterated log. The first term therefore swallows the entire rest of the tree.
.
The general shape: when the level costs shrink faster than geometrically, the root is not merely dominant, it is essentially the whole answer. Bounding the tail crudely and showing it is o(root) is the standard way to write this, and it is much easier than summing the tail exactly.
29.7 Set F: divide-and-conquer design
Now the problems stop being calculations. You are handed a task and a target running time, and the target is the hint: with nothing sorted in the statement almost always means , and means you are throwing away a constant fraction per step.
What a full-credit answer contains, in this order:
- The recursive structure, in words: what you split, and what you recurse on.
- The combine step, in enough detail to implement.
- Why the combine step is correct. This is the part being graded. It is almost always a lemma of the form "anything the recursion missed has to look like this".
- The recurrence and its solution.
F1. Items that appear more than n/3 times
A[1..n]holds items you can only compare for equality, in time. There is no order on them, so is not a question you may ask. Find every item that appears more than times, in .
First, how many answers can there be? At most two: three items each appearing more than times would need more than n slots. Bounding the size of the output is the observation that makes the whole approach work, so say it first.
The key lemma. If x appears more than times in , and is split into halves and , then x appears more than times in or more than times in .
Proof. Contrapositive. If x appears at most times in and at most times in , then it appears at most times in total, contradicting the assumption. QED
Why it works, stated so it transfers. Two properties are doing the work. Occurrence counts are additive across a partition of the array, ; and the threshold is linear in the size. Together they mean a heavy item cannot hide by spreading itself evenly, because an even spread still leaves it heavy in at least one half.
That is also what tells you the shape of the general result: for a threshold of , at most items can qualify, the same contrapositive goes through unchanged, and the candidate set stays bounded by . The algorithm below is the case and nothing about it is special to 3.
That lemma is the entire algorithm: it says the recursion cannot lose a heavy item, so the candidates from the two halves are guaranteed to include every real answer.
The algorithm.
FREQUENT(A[1..n])
1 if n is small (say n <= 3): return the items of A that occur > n/3 times
2 split A into halves L and R
3 C = FREQUENT(L) union FREQUENT(R) # at most 4 candidates
4 for each c in C
5 count occurrences of c in A by one linear scan using equality tests
6 return those c with count > n/3The base case is worth one line, because the threshold is a fraction rather than a count. On an array of size 1 the single item appears once and ; on size 2 a distinct item appears at least once and . So returning every distinct item is exactly right at the bottom, and there are at most two of them, which keeps the candidate bound true all the way down.
Correctness. By the lemma, every item appearing more than times in is returned by at least one recursive call, so it is in . Line 5 then verifies each candidate against the true threshold, so nothing false survives. Both directions done.
Running time. because each half returns at most 2 candidates, so line 4's loop runs a constant number of times and each iteration is one scan. Hence
T(n) = 2 T(n/2) + O(n) = O(n log n)The two things graders look for. That you bounded the candidate set by a constant (otherwise line 4 is not linear), and that you proved the lemma rather than asserting it. Note also that only equality tests were used, which the problem insisted on: no sorting, no hashing on order, no median.
F2. The outline of a set of boxes
Each box
iis an axis-aligned rectangle sitting on the x-axis, given by : left edge, right edge, height. The outline ofnboxes is the boundary of their union, which is a sequence of horizontal segments. Output the left endpoint of each segment, in order ofx. Assume all x-coordinates are distinct. Give an efficient divide-and-conquer algorithm.
An outline is just a step function of x: the height at any point is the maximum height of the boxes covering it, and the output is the list of points where that maximum changes.
The recursion. Split the boxes into two halves by index, not by position (they can overlap arbitrarily, so there is no clean geometric split). Recursively compute the outline of each half. Then merge two step functions by taking their pointwise maximum.
The merge, which is the whole problem.
MERGE-OUTLINES(P, Q) # P, Q are lists of (x, height), x-increasing
1 i = j = 1; hP = hQ = 0; out = empty
2 while both lists have entries left
3 take whichever of P[i], Q[j] has the smaller x; call it (x, h)
4 update the corresponding current height (hP or hQ) to h, and advance that index
5 cur = max(hP, hQ)
6 if cur differs from the last height appended to out
7 append (x, cur) to out
8 append the remainder of the unfinished list, applying the same rule
9 return outTwo facts the merge quietly depends on. First, no two events ever tie. Every x-coordinate in any outline is the left or right edge of some box, so every value the merge sees is an input coordinate, and the problem promised those are distinct. That is what lets the comparison be a strict ; with ties possible you would need a rule for simultaneous events, and omitting it is a real bug rather than a presentational one. Second, an outline of k boxes has at most points, since each point sits at one of that group's edges. That is the bound which keeps the merge linear, and it is worth stating rather than gesturing at.
The merge invariant. Just after processing an event at x, the running height of the left half is the height of its union immediately to the right of x, and likewise for the right half. It holds because each child outline changes height only at its own listed points, and by the time that event is processed we have consumed exactly those points with coordinate at most x, in increasing order. The union's height just right of x is then the maximum of the two, so scanning every event catches every change.
Why line 6 is compulsory. Without it you emit a point at every input x-coordinate, including the ones where the taller box is unchanged and the maximum does not move. That produces a technically-correct-looking list with redundant entries, and a list of segment starts that do not start segments is a wrong answer. The same suppression removes the artefacts where a shorter box begins or ends underneath a taller one.
Why the pointwise maximum is right. A point is covered by the union of all boxes iff it is covered by the union of the first half or of the second, and the height of the union at x is the max over all covering boxes, which splits over the two halves as max(max over half 1, max over half 2). Maximum is associative, so the recursion is sound.
Base case. One box has outline .
Running time. Each merge is a single linear pass over two lists whose combined length is , since an outline of k boxes has segments.
T(n) = 2 T(n/2) + O(n) = O(n log n)The pattern worth extracting. This is mergesort with instead of , over step functions instead of numbers. Whenever the quantity you want is an associative combination of the inputs, divide and conquer applies mechanically, and the only real work is implementing the combine in linear time.
F3. Counting inversions, and the general trick
An inversion in
A[1..n]is a pair with . A sorted array has 0; a reversed array has . Modify mergesort to count inversions in .
Worked in full at 24.3, so here is only the idea and the reason it generalizes.
Split into halves. Every inversion is entirely in the left half, entirely in the right half, or split between them. The first two are counted by the recursive calls. For the split ones: during the merge, at the moment you take an element from the right half while k elements remain unconsumed in the left half, that element is smaller than all k of them, and each is an inversion. Add k.
count = countLeft + countRight + countSplit
T(n) = 2 T(n/2) + O(n) = O(n log n)The step that is easy to skip. The recursive calls sort the two halves before the merge runs, so it is fair to ask whether that disturbs the count of split inversions. It does not, and the reason is worth a sentence: reordering within a half never changes which pairs are split across the halves, and it never changes the two values in such a pair. The split inversions of the original array are therefore exactly the pairs (left value, right value) with the left one larger, which is a statement about multisets of values rather than about positions, and that is what the merge counts.
Where the belongs, precisely. In the branch that fires once the left half is exhausted, adding the same quantity would contribute zero rather than a wrong number, because the left index only advances when an element is copied from the left and so never passes one-past-the-end. The addition still belongs only in the branch that takes from the right while the left is non-empty, but the reason is that there is nothing left to count, not that the arithmetic would misfire. Knowing which of the two it is matters if you are asked to justify the placement.
The general trick, which is the transferable part: make the recursion return more than the answer. Mergesort already sorts; we asked it to also report a count, and the count came for free because the merge already compares exactly the pairs that matter. The same move appears in closest-pair (24.11), where the recursion returns its points in y-order so the combine drops from to n. When a combine step looks too expensive, ask what the recursive calls could hand back that would make it cheap.
F4. The k-th smallest in the union of two sorted arrays
A[1..n]andB[1..m]are each sorted, and all values are distinct. Find the k-th smallest value of their union in .
rules out merging, which is . It means each step must discard a constant fraction of the work.
There are two standard solutions and they are worth knowing separately, because they generalise differently. Method A searches for the answer's position; method B throws away prefixes.
Method A: binary search the split
The reframing, which is the whole solution. Do not look for the k-th element. Look for where it splits the two arrays.
If the k smallest values of the union are exactly together with , then , so is determined by . That leaves one unknown ranging over an interval of integers, and one unknown over an interval is precisely what a binary search consumes. Turning a two-array search into a one-parameter search is what gives you something to halve.
Sentinels. Treat out-of-range entries as and . They are never returned; they exist so the comparisons need no special cases at the ends. Writing them down is cheaper than four boundary conditions, and forgetting them is where this problem is usually lost.
The search range. We need and , so , and the correct satisfies both constraints so it lies inside.
SELECT-A(A[1..n], B[1..m], k)
1 lo = max(0, k-m); hi = min(k, n)
2 loop
3 i = floor((lo + hi)/2); j = k - i
4 if A[i] > B[j+1]
5 hi = i - 1 # took too many from A
6 else if B[j] > A[i+1]
7 lo = i + 1 # took too few from A
8 else
9 return max(A[i], B[j])Correctness. Call i good if and , where .
A good i gives the answer. The chosen set has exactly elements, so it is enough that every chosen element is at most every unchosen one. Each unchosen element is either some with , hence at least , or some with , hence at least . So it suffices that , and all four inequalities are in hand: two from sortedness, two from goodness. The answer is then the largest chosen element, .
The tests point the right way, monotonically — and this is the part that licenses a binary search rather than a scan, so it is the part to write down. As i grows by one, weakly increases while drops, so weakly decreases. Hence once holds it holds for every larger i; since a good index needs , no good index is at or above the current one, and discarding the upper half is safe. The second test is the mirror image.
The two tests cannot both fire, since that would give , so .
Termination. Each iteration returns or strictly shrinks the interval while keeping every good index inside it, and a good index exists, so the interval cannot empty first.
Running time. The interval holds at most values and halves each iteration, so .
Method B: discard prefixes and shrink k
The idea. Look deep into each array and compare what you find. Whichever is smaller, everything up to and including it is too small to be the answer, and can be thrown away.
SELECT(A, B, k)
1 if A is empty: return B[k]
2 if B is empty: return A[k]
3 if k == 1: return min(A[1], B[1])
4 i = min(|A|, floor(k/2))
5 j = min(|B|, k - i)
6 if A[i] < B[j]
7 return SELECT(A[i+1 ..], B, k - i) # discard A[1..i]
8 else
9 return SELECT(A, B[j+1 ..], k - j) # discard B[1..j]Why discarding is safe, which is the only part being graded. Suppose . How many elements of the union can be less than or equal to ? At most i from (its own prefix) and at most from (everything before , since and is sorted). So the rank of in the union is at most . Every element of A[1..i] has rank at most that, so none of them is the k-th, and all i of them are below it. Discard them and reduce k by i. The symmetric argument covers the other branch.
Running time. Lines 4 and 5 ensure whenever both arrays are long enough, so one of them is at least and the discarded prefix has at least elements in the common case. Thus k at least halves each call:
T(k) = T(k/2) + O(1) = O(log k) = O(log(n + m))Two details that cost marks if skipped. The clamps in lines 4 and 5, without which you index off the end of the shorter array. And the base case, without which the recursion can stall with and make no progress.
If the analysis gives , that is fine and equal: log n + log m = log(nm) <= 2 log(n+m) = O(log(n+m)).
Which to write
Method A generalises better: it is the one that extends to "median of two sorted arrays" and to more than two arrays, and its monotonicity argument is exactly the kind of justification these questions are marked on. Method B is faster to write under time pressure and its safety argument is shorter. Either earns full credit provided you actually argue the step that makes it work — the monotonicity of the predicate in A, the rank bound in B. Stating the algorithm and asserting the bound is the half-answer both versions invite.
29.8 The rubric, stated plainly
Across all six sets, marks come off for the same handful of things.
| Lost mark | Fix |
|---|---|
| given where was asked | Always give the lower bound too, even if it is one line |
| A recurrence with no base case | One line, and enough of them for how far back the recurrence reaches |
| A combine step asserted, not proved | State the lemma: "anything the recursion missed must look like this" |
| An algorithm with no running time | Recurrence plus its solution, every time |
| Cases that overlap or miss | Say explicitly that the cases are exhaustive and disjoint |
| "Clearly" or "obviously" doing real work | If it were clear you would not need the word |
| Answering in terms of the wrong variable | Identify what is growing before writing any bound |
And the habit that prevents most of them: check one small case by hand. A recurrence, an ordering, a combine step, and a base case all reveal their errors at in about fifteen seconds.
Next: nothing. This is the end of the track. Go back to 21 — The Course Track: Orientation for the study plan, or start over at 22 and see how much less it takes the second time.