A Recursive Resident Evil problem
Welcome to Recursive City (optional challenge).
All logs are base 2.
Problem 1: containOutbreak
Umbrella Corp’s bio-containment system uses the following recursive function to monitor and isolate viral spread across sectors. The system continuously divides the infected zone in half until containment is complete.
Give the tightest possible Big- runtime bound for containOutbreak(N) as a function of . Show your work drawing a recursion tree.
int containOutbreak(int N) {
if (N <= 1) {
return 1;
}
int M = N;
while (M >= 1) {
M = M / 2;
}
return containOutbreak(N / 2);
}
Show solutionHide solution
Each call makes one recursive call on , so the recursion tree is a single chain. Since is halved at every level, the tree has height , which gives levels: level is the root and level is the base case.
| level | work |
|---|---|
| (base case) |
At level , the input is , and the while loop halves until it reaches . That takes about iterations, so the total work is
Our logs are base 2, so . Now we evaluate each sum.
First sum: runs from to , so we add up copies of (one per level):
Second sum: this is an arithmetic sum, so we use with :
Combining:
Let . The leftover term is lower order, so it doesn’t contribute asymptotically:
Note: precisely, the while loop runs times, and the base case does unit of work. These extra ’s add only in total, so they don’t change the bound.
Alternative solution. Using the quotient rule up front, each level’s work simplifies to . So the levels do units of work, and the base case at level does constant work. Reading the tree from the bottom up, this is just an arithmetic sum:
Problem 2: gVirusOutbreak
Bryan Hodukavich, an organ delivery driver for Lightfoot Medical Transport has just breached Raccoon City General. Inside, Umbrella Corporation’s bio-containment network is executing a binary search and quarantine routine. However, the G-Virus has mutated, causing the system to bifurcate its diagnostic scanning into two parallel recursive sub-tasks at every step.
int gVirusOutbreak(int N) {
if (N <= 1) {
return 1;
}
int M = N;
while (M >= 1) {
M = M / 2;
}
return gVirusOutbreak(N / 2) + gVirusOutbreak(N / 2);
}
(a) Runtime
Find the tightest possible Big- bound for gVirusOutbreak(N) as a function of . You may show your work using a recursion tree diagram. Final steps for this runtime analysis involve simplifying a common yet messy summation that appears in math, CS, and courses in probability. A change-of-index, infinite geometric series, algebraic manipulation, and/or calculus may help with this final part.
Show solutionHide solution
Each call makes two recursive calls on , so the number of nodes doubles at every level. The tree has height : level is the root and level is the leaf (base case) level, which has nodes.
| level | # nodes | work per node |
|---|---|---|
The work done by a single node depends on whether it is an internal node or a leaf:
Notice that plugging into the internal-node formula gives . That formula counts while loop iterations, but a leaf never reaches the loop: it hits the base case and returns. So the leaves still do work each, and we count them separately.
Summing over the levels, with nodes at level :
Strategy: Instead of computing the internal-node sum exactly, we trap it between an upper bound and a lower bound. If both bounds grow like the same function of , that function is our Big- bound. We start with the upper bound.
Upper bound. First, we unravel the term using log properties. Then we let , so the upper index becomes :
Next, we change the index. Let , which rewrites to . Substituting into each term:
The endpoints change too. Plugging each original endpoint into :
So runs from down to . Since addition is commutative, the order we add the terms in doesn’t matter, so we can swap the endpoints and put at the bottom of the summation:
We can upper bound by letting run all the way from to . This helps us because adding additional positive terms to a sum only increases the sum:
where is the resulting sum. Now we find :
How can we massage this to look like a toy geometric series that we’re familiar with? Say,
We know this toy series evaluates to , where is the first term of the series and is the common ratio, provided that . Here and :
Now we rewrite . If we take , every term is cut in half:
But lines up like a classic subtraction problem, where we subtract terms with the same denominator:
This is a geometric series we’re familiar with! With and :
We now revisit the original summation with this new knowledge. We don’t know the exact value of the finite sum, but it is at most the infinite sum :
Finally, we return to summing over the levels, with nodes at level . Calling the total work , the internal-node summation is now upper bounded by :
So .
Lower bound. We’re not finished (unfortunately) ☹. We need to find a lower bound now. Recall the summations we found by adding up all the work in the recursion tree:
We focus on the first summation. Recall that we were able to massage it with a change of index, which lets us rewrite it as:
Now we find a lower bound for the factor representing the summation, :
How does this effectively create a lower bound for us to work with? Every is at least , so for every term, and replacing each term with a smaller one can only make the sum smaller. We do this because in rigorous asymptotic analysis, we typically take advantage of lower and upper bounds, and here the lower bound leads us to a finite geometric series:
We’ll apply the same trick we used to find the upper bound (recall ), but now with respect to :
Every middle term cancels, leaving only the first term of and the last term of :
So:
Since , the right-hand side equals . Whenever , we have (for example, makes the right-hand side exactly ). Thus:
Now we multiply both sides of the inequality by . Since , the inequality doesn’t flip:
Tying this back to :
So .
We’ve achieved a lower bound! Since the lower bound and the upper bound are in terms of the same function of ,
we have shown that gVirusOutbreak(N) .
PHEW!
(b) Space complexity
What is the tightest Big- bound on the memory space (call stack) required by gVirusOutbreak(N)?
Show solutionHide solution
. Even though the tree has about nodes, they are never all on the call stack at once. The first call to gVirusOutbreak(N / 2) runs to completion and returns before the second one starts, so the stack only ever holds one root-to-leaf path. That path has frames, and each frame stores only a constant amount of data ( and ), so the space used is .
Problem 3: nemesisProtocol
As Bryan escapes Raccoon City General, Umbrella Corporation unleashes its Nemesis-T Type, a bio-weapon programmed to hunt down every surviving witness. Unlike the earlier containment routines, Nemesis leaves nothing to chance: before splitting its pursuit into two parallel search teams, it sweeps every single one of the sectors in its current zone.
int nemesisProtocol(int N) {
if (N <= 1) {
return 1;
}
for (int i = 0; i < N; i++) {
searchSector(i); // Assume searchSector() runs in Θ(1)
}
return nemesisProtocol(N / 2) + nemesisProtocol(N / 2);
}
(a) Runtime
Find the tightest possible Big- bound for nemesisProtocol(N) as a function of . You may show your work using a recursion tree diagram.
Show solutionHide solution
Each call makes two recursive calls on , so level has nodes, and the tree has height . A node at level has input , so its for loop calls the searchSector method times.
| level | # nodes | work/node | work/level |
|---|---|---|---|
At every internal level, the number of nodes doubles while the work per node halves, so each level does the same amount of work:
The leaf level has base-case nodes doing work each, which is another . Summing over the internal levels plus the leaves:
Unlike gVirusOutbreak, no single level dominates here: every level does the same work, so the total is (work per level) (number of levels).
(b) Space complexity
What is the tightest Big- bound on the memory space (call stack) required by nemesisProtocol(N)? How does it compare to gVirusOutbreak(N), and why?
Show solutionHide solution
, the same as gVirusOutbreak(N). The for loop changes how much time each call takes, but not how deep the recursion goes. As before, the first recursive call finishes before the second one starts, so the stack only ever holds one root-to-leaf path of frames, each storing a constant amount of data ( and ).