





















































Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
Lecture notes on algorithms, specifically lectures 1-10, given by Avrim Blum and Manuel Blum at Carnegie Mellon University. The notes cover topics such as asymptotic analysis, recurrences, probabilistic analysis, and randomized quicksort. The document also includes an introduction to algorithms, the study of algorithms, and the importance of specifications and guarantees. The notes provide examples of algorithms such as Karatsuba multiplication and Strassen's matrix multiplication algorithm.
Typology: Lecture notes
1 / 61
This page cannot be seen from the preview
Don't miss anything!






















































1.1 Overview
The purpose of this lecture is to give a brief overview of the topic of Algorithms and the kind of thinking it involves: why we focus on the subjects that we do, and why we emphasize proving guarantees. We also go through an example of a problem that is easy to relate to (multiplying two numbers) in which the straightforward approach is surprisingly not the fastest one. This example leads naturally into the study of recurrences, which is the topic of the next lecture, and provides a forward pointer to topics such as the FFT later on in the course.
Material in this lecture:
1.2 Introduction
This course is about the design and analysis of algorithms — how to design correct, efficient algorithms, and how to think clearly about analyzing correctness and running time.
What is an algorithm? At its most basic, an algorithm is a method for solving a computational problem. Along with an algorithm comes a specification that says what the algorithm’s guarantees are. For example, we might be able to say that our algorithm indeed correctly solves the problem in question and runs in time at most f (n) on any input of size n. This course is about the whole package: the design of efficient algorithms, and proving that they meet desired specifications. For each of these parts, we will examine important techniques that have been developed, and with practice we will build up our ability to think clearly about the key issues that arise.
2
It is often helpful when thinking about algorithms to imagine a game where one player is the algorithm designer, trying to come up with a good algorithm for the problem, and its opponent (the “adversary”) is trying to come up with an input that will cause the algorithm to run slowly. An algorithm with good worst-case guarantees is one that performs well no matter what input the adversary chooses. We will return to this view in a more formal way when we discuss randomized algorithms and lower bounds.
1.4 An example: Karatsuba Multiplication
One thing that makes algorithm design “Computer Science” is that solving a problem in the most obvious way from its definitions is often not the best way to get a solution. A simple example of this is multiplication.
Say we want to multiply two n-bit numbers: for example, 41 × 42 (or, in binary, 101001 × 101010). According to the definition of what it means to multiply, what we are looking for is the result of adding 41 to itself 42 times (or vice versa). You could imagine actually computing the answer that way (i.e., performing 41 additions), which would be correct but not particularly efficient. If we used this approach to multiply two n-bit numbers, we would be making Θ(2n) additions. This is exponential in n even without counting the number of steps needed to perform each addition. And, in general, exponential is bad.^1 A better way to multiply is to do what we learned in grade school:
1010010 101001
11010111010 = 1722
More formally, we scan the second number right to left, and every time we see a 1, we add a copy of the first number, shifted by the appropriate number of bits, to our total. Each addition takes O(n) time, and we perform at most n additions, which means the total running time here is O(n^2 ). So, this is a simple example where even though the problem is defined “algorithmically”, using the definition is not the best way of solving the problem.
Is the above method the fastest way to multiply two numbers? It turns out it is not. Here is a faster method called Karatsuba Multiplication, discovered by Anatoli Karatsuba, in Russia, in 1962. In this approach, we take the two numbers X and Y and split them each into their most-significant half and their least-significant half:
X = 2n/^2 A + B A B Y = 2n/^2 C + D C D
(^1) This is reminiscent of an exponential-time sorting algorithm I once saw in Prolog. The code just contains the definition of what it means to sort the input — namely, to produce a permutation of the input in which all elements are in ascending order. When handed directly to the interpreter, it results in an algorithm that examines all n! permutations of the given input list until it finds one that is in the right order.
We can now write the product of X and Y as
XY = 2 nAC + 2n/^2 BC + 2n/^2 AD + BD. (1.1)
This does not yet seem so useful: if we use (1.1) as a recursive multiplication algorithm, we need to perform four n/2-bit multiplications, three shifts, and three O(n)-bit additions. If we use T (n) to denote the running time to multiply two n-bit numbers by this method, this gives us a recurrence of
T (n) = 4 T (n/2) + cn, (1.2)
for some constant c. (The cn term reflects the time to perform the additions and shifts.) This recurrence solves to O(n^2 ), so we do not seem to have made any progress. (In the next lecture we will go into the details of how to solve recurrences like this.)
However, we can take the formula in (1.1) and rewrite it as follows:
(2n^ − 2 n/^2 )AC + 2n/^2 (A + B)(C + D) + (1 − 2 n/^2 )BD. (1.3)
It is not hard to see — you just need to multiply it out — that the formula in (1.3) is equivalent to the expression in (1.1). The new formula looks more complicated, but, it results in only three multiplications of size n/2, plus a constant number of shifts and additions. So, the resulting recurrence is
T (n) = 3 T (n/2) + c′n, (1.4)
for some constant c′. This recurrence solves to O(nlog^2 3 ) ≈ O(n^1.^585 ).
Is this method the fastest possible? Again it turns out that one can do better. In fact, Karp discov- ered a way to use the Fast Fourier Transform to multiply two n-bit numbers in time O(n log^2 n). Sch¨onhage and Strassen in 1971 improved this to O(n log n log log n), which was until very recently the asymptotically fastest algorithm known.^2 We will discuss the FFT later on in this course.
Actually, the kind of analysis we have been doing really is meaningful only for very large numbers. On a computer, if you are multiplying numbers that fit into the word size, you would do this in hardware that has gates working in parallel. So instead of looking at sequential running time, in this case we would want to examine the size and depth of the circuit used, for instance. This points out that, in fact, there are different kinds of specifications that can be important in different settings.
1.5 Matrix multiplication
It turns out the same basic divide-and-conquer approach of Karatsuba’s algorithm can be used to speed up matrix multiplication as well. To be clear, we will now be considering a computational model where individual elements in the matrices are viewed as “small” and can be added or multi- plied in constant time. In particular, to multiply two n-by-n matrices in the usual way (we take the
(^2) F¨urer in 2007 improved this by replacing the log log n term with 2O(log∗^ n), where log∗ (^) n is a very slowly growing function discussed in Lecture 14. It remains unknown whether eliminating it completely and achieving running time O(n log n) is possible.
2.1 Overview
In this lecture we discuss the notion of asymptotic analysis and introduce O, Ω, Θ, and o notation. We then turn to the topic of recurrences, discussing several methods for solving them. Recurrences will come up in many of the algorithms we study, so it is useful to get a good intuition for them right at the start. In particular, we focus on divide-and-conquer style recurrences, which are the most common ones we will see.
Material in this lecture:
2.2 Asymptotic analysis
When we consider an algorithm for some problem, in addition to knowing that it produces a correct solution, we will be especially interested in analyzing its running time. There are several aspects of running time that one could focus on. Our focus will be primarily on the question: “how does the running time scale with the size of the input?” This is called asymptotic analysis, and the idea is that we will ignore low-order terms and constant factors, focusing instead on the shape of the running time curve. We will typically use n to denote the size of the input, and T (n) to denote the running time of our algorithm on an input of size n.
We begin by presenting some convenient definitions for performing this kind of analysis.
Definition 2.1 T (n) ∈ O(f (n)) if there exist constants c, n 0 > 0 such that T (n) ≤ cf (n) for all n > n 0.
7
Informally we can view this as “T (n) is proportional to f (n), or better, as n gets large.” For example, 3n^2 + 17 ∈ O(n^2 ) and 3n^2 + 17 ∈ O(n^3 ). This notation is especially useful in discussing upper bounds on algorithms: for instance, we saw last time that Karatsuba multiplication took time O(nlog^2 3 ).
Notice that O(f (n)) is a set of functions. Nonetheless, it is common practice to write T (n) = O(f (n)) to mean that T (n) ∈ O(f (n)): especially in conversation, it is more natural to say “T (n) is O(f (n))” than to say “T (n) is in O(f (n))”. We will typically use this common practice, reverting to the correct set notation when this practice would cause confusion.
Definition 2.2 T (n) ∈ Ω(f (n)) if there exist constants c, n 0 > 0 such that T (n) ≥ cf (n) for all n > n 0.
Informally we can view this as “T (n) is proportional to f (n), or worse, as n gets large.” For example, 3n^2 − 2 n ∈ Ω(n^2 ). This notation is especially useful for lower bounds. In Chapter 5, for instance, we will prove that any comparison-based sorting algorithm must take time Ω(n log n) in the worst case (or even on average).
Definition 2.3 T (n) ∈ Θ(f (n)) if T (n) ∈ O(f (n)) and T (n) ∈ Ω(f (n)).
Informally we can view this as “T (n) is proportional to f (n) as n gets large.”
Definition 2.4 T (n) ∈ o(f (n)) if for all constants c > 0 , there exists n 0 > 0 such that T (n) < cf (n) for all n > n 0.
For example, last time we saw that we could indeed multiply two n-bit numbers in time o(n^2 ) by the Karatsuba algorithm. Very informally, O is like ≤, Ω is like ≥, Θ is like =, and o is like <. There is also a similar notation ω that corresponds to >.
In terms of computing whether or not T (n) belongs to one of these sets with respect to f (n), a convenient way is to compute the limit:
nlim→∞
T (n) f (n)
If the limit exists, then we can make the following statements:
For example, suppose T (n) = 2n^3 + 100n^2 log 2 n + 17 and f (n) = n^3. The ratio of these is 2 + (100 log 2 n)/n + 17/n^3. In this limit, this goes to 2. Therefore, T (n) = Θ(f (n)). Of course, it is possible that the limit doesn’t exist — for instance if T (n) = n(2 + sin n) and f (n) = n then the ratio oscillates between 1 and 3. In this case we would go back to the definitions to say that T (n) = Θ(n).
(n/2)(cn/2) = cn^2 /4. So, it is Θ(n^2 ). Similarly, a recurrence T (n) = n^5 + T (n − 1) unrolls to:
T (n) = n^5 + (n − 1)^5 + (n − 2)^5 +... + 1^5 , (2.3)
which solves to Θ(n^6 ) using the same style of reasoning as before. In particular, there are n terms each of which is at most n^5 so the sum is at most n^6 , and the top n/2 terms are each at least (n/2)^5 so the sum is at least (n/2)^6. Another convenient way to look at many summations of this form is to see them as approximations to an integral. E.g., in this last case, the sum is at least the integral of f (x) = x^5 evaluated from 0 to n, and at most the integral of f (x) = x^5 evaluated from 1 to n + 1. So, the sum lies in the range [ 16 n^6 , 16 (n + 1)^6 ].
Another good way to solve recurrences is to make a guess and then prove the guess correct induc- tively. Or if we get into trouble proving our guess correct (e.g., because it was wrong), often this will give us clues as to a better guess. For example, say we have the recurrence
T (n) = 7 T (n/7) + n, (2.4) T (1) = 0. (2.5)
We might first try a solution of T (n) ≤ cn for some c > 0. We would then assume it holds true inductively for n′^ < n (the base case is obviously true) and plug in to our recurrence (using n′^ = n/7) to get:
T (n) ≤ 7(cn/7) + n = cn + n = (c + 1)n.
Unfortunately, this isn’t what we wanted: our multiplier “c” went up by 1 when n went up by a factor of 7. In other words, our multiplier is acting like log 7 (n). So, let’s make a new guess using a multiplier of this form. So, we have a new guess of
T (n) ≤ n log 7 (n). (2.6)
If we assume this holds true inductively for n′^ < n, then we get:
T (n) ≤ 7[(n/7) log 7 (n/7)] + n = n log 7 (n/7) + n = n log 7 (n) − n + n = n log 7 (n). (2.7)
So, we have verified our guess.
It is important in this type of proof to be careful. For instance, one could be lulled into thinking that our initial guess of cn was correct by reasoning “we assumed T (n/7) was Θ(n/7) and got T (n) = Θ(n)”. The problem is that the constants changed (c turned into c + 1) so they really weren’t constant after all!
The final method we examine, which is especially good for divide-and-conquer style recurrences, is the use of a recursion tree. We will use this to method to produce a simple “master formula” that can be applied to many recurrences of this form. Consider the following type of recurrence:
T (n) = aT (n/b) + cnk^ (2.8) T (1) = c,
for positive constants a, b, c, and k. This recurrence corresponds to the time spent by an algorithm that does cnk^ work up front, and then divides the problem into a pieces of size n/b, solving each one recursively. For instance, mergesort, Karatsuba multiplication, and Strassen’s algorithm all fit this mold. A recursion tree is just a tree that represents this process, where each node contains inside it the work done up front and then has one child for each recursive call. The leaves of the tree are the base cases of the recursion. A tree for the recurrence (2.8) is given below.^1
cnk ^
aa aaa