An Introduction To The Usa Computing Olympiad: Darren Yao
An Introduction To The Usa Computing Olympiad: Darren Yao
Darren Yao
June 13, 2020
R / C++ Edition
Foreword
This book was written as a comprehensive and up-to-date training resource for the USA
Computing Olympiad. The goal was to create an “Art of Problem Solving” of sorts for the
USACO: a one-stop-shop guide to prepare competitive programmers for the Bronze and Silver
divisions of the USACO contests.
My primary motivation for writing this book was the struggle to find the right resources
when I first started doing USACO contests. When I eventually reached the Platinum division,
new competitors often asked me for help in structuring their competitive programming
practice. Since I always found myself explaining that the USACO lacked comprehensive
training resources, I decided to write this book.
I would like to thank a number of people for their contributions to this book. In particular,
Michael Cao for writing sections 10.6 and 10.7 and helping with content revisions, Jason
Chen for writing section 14.2 and extensive help with both content and LaTeX formatting,
Stephanie Wu for section 4.1 and revisions, and Aaryan Prakash, Rishab Parthasarathy, and
Kevin Wang for their valuable and constructive feedback on early draft versions of the book.
Furthermore, I’d like to thank Jason, Stephanie, and Rishab again for helping translate
code into C++ for this edition of the book, on very short notice.
I’d also like to thank the USACO discord community for supporting me through my
competitive programming journey; it was because of them that my competitive programming
successes, and this book, are possible.
Cover design by Dylan Yu.
Author’s Profile
Darren Yao is an active competitor in the USA Computing Olympiad, where he qualified
for the Platinum division in his third contest. You can find his website at https://darrenyao.
com/.
Copyright
2020
c by Darren Yao
All rights reserved. No part of this book may be reproduced or used in any manner without
the prior written permission from the copyright owner.
i
Contents
I Basic Techniques 1
1 The Beginning 2
1.1 Competitive Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Contests and Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3 Competitive Programming Practice . . . . . . . . . . . . . . . . . . . . . . . 3
1.4 About This Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Elementary Techniques 6
2.1 Input and Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
II Bronze 20
5 Simulation 21
5.1 Example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.2 Example 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.3 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
6 Complete Search 24
6.1 Example 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
6.2 Generating Permutations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
6.3 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
ii
CONTENTS iii
III Silver 30
8 Sorting and Comparators 31
8.1 Comparators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
8.2 Sorting by Multiple Criteria . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
8.3 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
9 Greedy Algorithms 34
9.1 Introductory Example: Studying Algorithms . . . . . . . . . . . . . . . . . . 34
9.2 The Scheduling Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
9.3 When Greedy Fails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
9.4 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
10 Graph Theory 38
10.1 Graph Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
10.2 Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
10.3 Graph Representations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
10.4 Graph Traversal Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
10.5 Floodfill . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
10.6 Disjoint-Set Data Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
10.7 Other Types of Graphs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
10.8 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
11 Prefix Sums 58
11.1 Prefix Sums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
11.2 Two Dimensional Prefix Sums . . . . . . . . . . . . . . . . . . . . . . . . . . 59
11.3 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
12 Binary Search 62
12.1 Binary Search on the Answer . . . . . . . . . . . . . . . . . . . . . . . . . . 62
12.2 Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
12.3 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
IV Problem Set 79
15 Parting Shots 80
Part I
Basic Techniques
1
Chapter 1
The Beginning
2
CHAPTER 1. THE BEGINNING 3
practice with should be of the appropriate difficulty. You don’t necessarily need to complete
all the exercises at the end of each chapter, just do what you think is right for you. A
problem at the right level of difficulty should be one of two types: either you struggle with
the problem for a while before coming up with a working solution, or you miss it slightly and
need to consult the solution for some small part. If you instantly come up with the solution,
a problem is likely too easy, and if you’re missing multiple steps, it might be too hard.
In general, especially on harder problems, I think it’s fine to read the solution relatively
early on, as long as you’re made several different attempts at it and you can learn effectively
from the solution.
• On a bronze problem, read the solution after 15-20 minutes of no meaningful progress,
after you’ve exhausted every idea you can think of.
• On a silver problem, read the solution after 30-40 minutes of no meaningful progress.
When you get stuck and consult the solution, you should not read the entire solution at
once, and you certainly shouldn’t look at the solution code. Instead, it’s better to read the
solution step by step until you get unstuck, at which point you should go back and finish the
problem, and implement it yourself. Reading the full solution or its code should be seen as a
last resort.
• Do not use online IDEs that display your code publicly, like the free version of ideone.
This allows other users to copy your code, and you may get flagged for cheating.
The primary purpose of this book is to compile all of the topics needed for a beginner in
one book, and provide all the resources needed, to make the process of studying for contests
easier.
A brief outline of the book is as follows:
• Chapter 2 covers how to handle input/output in C++, the different data types used
to store variables, and finally, binary representations of integers and bit operations on
integers.
• Chapter 3 introduces time and space complexity and how to structure algorithm designs
around these constraints.
• Chapter 4 covers data structures in the C++ standard library, such as stacks, sets,
maps, and the various types of queues.
• Chapter 5 covers simulation problems where the solution is simply implementing the
problem description.
• Chapter 9 introduces greedy algorithms and some common problems that can be solved
using greedy algorithms
• Chapter 10 deals with graph theory: graph representations, traversal algorithms, and
other related topics.
• Chapter 13 focuses on some number theory concepts that often appear on contests.
• Chapter 15 is a collection of practice problems to review all the topics covered in this
book, and provide further preparation for contests.
Chapter 2
Elementary Techniques
#include <cstdio>
int main() {
freopen("template.in", "r", stdin);
freopen("template.out", "w", stdout);
}
If <fstream> is used (note that if you use <fstream>, you must replace cin and cout
with fin and fout):
6
CHAPTER 2. ELEMENTARY TECHNIQUES 7
#include <fstream>
int main() {
ifstream fin("template.in");
ofstream fout("template.out");
}
For CodeForces, CSES, and other contests that use standard input and output, simply
use the standard input / output from <iostream>.
When using C++, arrays should be declared globally if at all possible. This
avoids the common issue of initialization to garbage values. If you declare an array locally,
you’ll need to initialize the values to zero.
In programming contests, there is a strict limit on program runtime. This means that in
order to pass, your program needs to finish running within a certain timeframe. For USACO,
this limit is 2 seconds for C++ submissions. A conservative estimate for the number of
operations the grading server can handle per second is 108 (but is really closer to 5 · 108 given
good constant factors).
int a = 5;
int b = 7;
int c = 4;
int d = a + b + c + 153;
8
CHAPTER 3. TIME/SPACE COMPLEXITY AND ALGORITHM ANALYSIS 9
int i = 0;
while(i < n){
// constant time node here
i++;
}
Because we ignore constant factors and lower order terms, the following examples are also
O(n):
We can find the time complexity of multiple loops by multiplying together the time
complexities of each loop. This example is O(nm), because the outer loop runs O(n)
iterations and the inner loop O(m).
In this example, the outer loop runs n iterations, and the inner loop runs anywhere
between 1 and n iterations (which is a maximum of n). Since Big O notation calculates
worst-case time complexity, we must take the factor of n from the inner loop. Thus, this code
is O(n2 ).
If an algorithm contains multiple blocks, then its time complexity is the worst time
complexity out of any block. For example, the following code is O(n2 ).
CHAPTER 3. TIME/SPACE COMPLEXITY AND ALGORITHM ANALYSIS 10
The following code is O(n2 + nm), because it consists of two blocks of complexity O(n2 )
and O(nm), and neither of them is a lower order function with respect to the other.
• Prime
√ factorization of an integer, or checking primality or compositeness of an integer:
O( n)
• Sorting: usually O(n log n) for default sorting algorithms (mergesort and quicksort
used in std::sort())
CHAPTER 3. TIME/SPACE COMPLEXITY AND ALGORITHM ANALYSIS 11
• Iterating through all subsets of size k of the input elements: O(nk ). For example,
iterating through all triplets is O(n3 ).
Here are conservative upper bounds on the value of n for each time complexity. You can
probably get away with more than this, but this should allow you to quickly check whether
an algorithm is viable.
n Possible complexities
n ≤ 10 O(n!), O(n7 ), O(n6 )
n ≤ 20 O(2n · n), O(n5 )
n ≤ 80 O(n4 )
n ≤ 400 O(n3 )
2
n ≤ 7500 O(n
√)
n ≤ 7 · 104 O(n n)
n ≤ 5 · 105 O(n log n)
n ≤ 5 · 106 √ O(n) √
n ≤ 1012 O( n log n), O( n)
n ≤ 1018 O(log2 n), O(log n), O(1)
Chapter 4
A data structure determines how data is stored. (is it sorted? indexed? what operations
does it support?) Each data structure supports some operations efficiently, while other
operations are either inefficient or not supported at all. This chapter introduces the data
structures in the C++ standard library that are frequently used in competitive programming.
The C++ standard library data structures are designed to store any type of data. We put
the desired data type within the <> brackets when declaring the data structure, as follows:
vector<string> v;
This creates a vector structure that only stores objects of type string.
For our examples below, we will primarily use the int data type, but note that you can
use any data type including string and user-defined structures.
Essentially every standard library data structure supports the size() method, which
returns the number of elements in the data structure, and the empty() method, which returns
true if the data structure is empty, and false otherwise.
4.1 Iterators
Before the data structures are introduced, you should understand an iterator. An iterator
allows you to traverse a container by providing a pointer. For example, vector.begin()
returns an iterator pointing to the first element of the vector. Apart from the standard way
of traversing a vector (by treating it as an array), you can also use iterators:
However, a more generic way to do this is with a for-each loop and auto (C++11 and later
versions) that automatically infers the type of an object:
12
CHAPTER 4. BUILT-IN DATA STRUCTURES 13
for(auto element : v) {
cout << element; //prints the values in the vector
}
vector<int> v;
for(int i = 1; i <= 10; i++){
v.push_back(i);
}
When declaring a dynamic array we can give it an initial size, so it doesn’t resize itself as
we add elements to it. The following code initializes a vector with initial size 30:
vector<int> v(30);
However, we need to be careful that we only add elements to the end of the vector;
insertion and deletion in the middle of the vector is O(n).
vector<int> v;
v.push_back(2); // [2]
v.push_back(3); // [2, 3]
v.push_back(7); // [2, 3, 7]
v.push_back(5); // [2, 3, 7, 5]
v[1] = 4; // sets element at index 1 to 4 -> [2, 4, 7, 5]
v.erase(v.begin() + 1); // removes element at index 1 -> [2, 7, 5]
// this remove method is O(n); to be avoided
v.push_back(8); // [2, 7, 5, 8]
v.erase(v.end()-1); // [2, 7, 5]
// here, we remove the element from the end of the list; this is O(1).
v.push_back(4); // [2, 7, 5, 4]
v.push_back(4); // [2, 7, 5, 4, 4]
v.push_back(9); // [2, 7, 5, 4, 4, 9]
cout << v[2]; // 5
v.erase(v.begin(), v.begin()+3); // [4, 4, 9]
// this erases the first three elements; O(n)
CHAPTER 4. BUILT-IN DATA STRUCTURES 14
To iterate through a static or dynamic array, we can use either the regular for loop or the
for-each loop.
vector<int> v;
v.push_back(1); v.push_back(7); v.push_back(4); v.push_back(5); v.push_back(2);
int arr[] = {1, 7, 4, 5, 2};
for(int i = 0; i < v.size(); i++){
cout << v[i] << " ";
}
cout << endl;
for(int element : arr){
cout << element << " ";
}
cout << endl;
In order to sort a dynamic array, use sort(v.begin(), v.end()), whereas static arrays
require sort(arr, arr + N) where N is the number of elements to be sorted. The default
sort function sorts the array in ascending order.
In array-based contest problems, we’ll use one-, two-, and three-dimensional static arrays
most of the time. However, we can also have static arrays of dynamic arrays, dynamic arrays
of static arrays, and so on. Usually, the choice between a static array and a dynamic array is
just personal preference.
stack<int> s;
s.push(1); // [1]
s.push(13); // [1, 13]
s.push(7); // [1, 13, 7]
cout << s.top() << endl; // 7
s.pop(); // [1, 13]
cout << s.size() << endl; // 2
CHAPTER 4. BUILT-IN DATA STRUCTURES 15
Queues
A queue is a First In First Out (FIFO) data structure that supports three operations
of push, insertion at the back of the queue, pop, deletion from the front of the queue, and
front, which retrieves the element at the front without removing it, all in O(1) time.
queue<int> q;
q.push(1); // [1]
q.push(3); // [3, 1]
q.push(4); // [4, 3, 1]
q.pop(); // [4, 3]
cout << q.front() << endl; // 3
Deques
A deque (usually pronounced “deck”) stands for double ended queue and is a combination
of a stack and a queue, in that it supports O(1) insertions and deletions from both the
front and the back of the deque. The four methods for adding and removing are push_back,
pop_back, push_front, and pop_front.
deque<int> d;
d.push_front(3); // [3]
d.push_front(4); // [4, 3]
d.push_back(7); // [4, 3, 7]
d.pop_front(); // [3, 7]
d.push_front(1); // [1, 3, 7]
d.pop_back(); // [1, 3]
Priority Queues
A priority queue supports the following operations: insertion of elements, deletion of
the element considered highest priority, and retrieval of the highest priority element, all in
O(log n) time according to the number of elements in the priority queue. Priority is based
on a comparator function, and in C++ the highest element is put at the front of the queue.
The priority queue is one of the most important data structures in competitive programming,
so make sure you understand how and when to use it.
priority_queue<int> pq;
pq.push(7); // [7]
pq.push(2); // [2, 7]
pq.push(1); // [1, 2, 7]
pq.push(5); // [1, 2, 5, 7]
cout << pq.top() << endl; // 7
pq.pop(); // [1, 2, 5]
CHAPTER 4. BUILT-IN DATA STRUCTURES 16
pq.pop(); // [1, 2]
pq.push(6); // [1, 2, 6]
Unordered Sets
The unordered set works by hashing, which is assigning a unique code to every vari-
able/object which allows insertions, deletions, and searches in O(1) time, albeit with a high
constant factor, as hashing requires a large constant number of operations. However, as the
name implies, elements are not ordered in any meaningful way, so traversals of an unordered
set will return elements in some arbitrary order. The operations on an unordered set are
insert, which adds an element to the set if not already present, erase, which deletes an
element if it exists, and count, which returns 1 if the set contains the element and 0 if it
doesn’t.
unordered_set<int> s;
s.insert(1); // [1]
s.insert(4); // [1, 4] in arbitrary order
s.insert(2); // [1, 4, 2] in arbitrary order
s.insert(1); // [1, 4, 2] in arbitrary order
// the add method did nothing because 1 was already in the set
cout << s.count(1) << endl; // 1
set.erase(1); // [2, 4] in arbitrary order
cout << s.count(5) << endl; // 0
s.erase(0); // [2, 4] in arbitrary order
// if the element to be removed does not exist, nothing happens
Ordered Sets
The second type of set data structure is the ordered or sorted set. Insertions, deletions,
and searches on the ordered set require O(log n) time, based on the number of elements
in the set. As well as those supported by the unordered set, the ordered set also allows
CHAPTER 4. BUILT-IN DATA STRUCTURES 17
four additional operations: begin(), which returns an iterator to the lowest element in
the set, end(), which returns an iterator to the highest element in the set, lower_bound,
which returns an iterator to the least element greater than or equal to some element k,
and upper_bound, which returns an iterator to the least element strictly greater than some
element k.
set<int> s;
s.insert(1); // [1]
s.insert(14); // [1, 14]
s.insert(9); // [1, 9, 14]
s.insert(2); // [1, 2, 9, 14]
cout << *s.upper_bound(7) << '\n'; // 9
cout << *s.upper_bound(9) << '\n'; // 14
cout << *s.lower_bound(5) << '\n'; // 9
cout << *s.lower_bound(9) << '\n'; // 9
cout << *s.begin() << '\n'; // 1
auto it = s.end();
cout << *(--it) << '\n'; // 14
s.erase(s.upper_bound(6)); // [1, 2, 14]
The primary limitation of the ordered set is that we can’t efficiently access the k th largest
element in the set, or find the number of elements in the set greater than some arbitrary x.
These operations can be handled using a data structure called an order statistic tree, but
that is beyond the scope of this book.
Maps
A map is a set of ordered pairs, each containing a key and a value. In a map, all keys
are required to be unique, but values can be repeated. Maps have three primary methods:
one to add a specified key-value pairing, one to retrieve the value for a given key, and one to
remove a key-value pairing from the map. Like sets, maps can be unordered (unordered_map
in C++) or ordered (map in C++). In an unordered map, hashing is used to support O(1)
operations. In an ordered map, the entries are sorted in order of key. Operations are O(log n),
but accessing or removing the next key higher or lower than some input k is also supported.
Unordered Maps
In an unordered map m, the m[key] = value operator assigns a value to a key and places
the key and value pair into the map. The operator m[key] returns the value associated with
the key. The count(key) method returns the number of times the key is in the map (which is
either one or zero), and therefore checks whether a key exists in the map. Lastly, erase(key)
and erase(it) removes the map entry associated with the specified key or iterator. All of
these operations are O(1), but again, due to the hashing, this has a high constant factor.
CHAPTER 4. BUILT-IN DATA STRUCTURES 18
unordered_map<int, int> m;
m[1] = 5; // [(1, 5)]
m[3] = 14; // [(1, 5); (3, 14)]
m[2] = 7; // [(1, 5); (3, 14); (2, 7)]
m.erase(2); // [(1, 5); (3, 14)]
cout << m[1] << '\n'; // 5
cout << m.count(7) << '\n' ; // 0
cout << m.count(1) << '\n' ; // 1
Ordered Maps
The ordered map supports all of the operations that an unordered map supports, and
additionally supports lower_bound and upper_bound, returning the iterator pointing to the
lowest entry not less than the specified key, and the iterator pointing to the lowest entry
strictly greater than the specified key respectively.
map<int, int> m;
m[3] = 5; // [(3, 5)]
m[11] = 4; // [(3, 5); (11, 4)]
m[10] = 491; // [(3, 5); (10, 491); (11, 4)]
cout << m.lower_bound(10)->first << " " << m.lower_bound(10)->second << '\n'; //
,→ 10 491
cout << m.upper_bound(10)->first << " " << m.upper_bound(10)->second << '\n'; //
,→ 11 4
m.erase(11); // [(3, 5); (10, 491)]
if (m.upper_bound(10) == m.end())
{
cout << "end" << endl; // Prints end
}
A note on unordered sets and maps: In USACO contests, they’re generally fine, but in
CodeForces contests, you should always use sorted sets and maps. This is because the built-in
hashing algorithm is vulnerable to pathological data sets causing abnormally slow runtimes,
in turn causing failures on some test cases.
Multisets
Lastly, there is the multiset, which is essentially a sorted set that allows multiple copies
of the same element. In addition to all of the regular set operations, the multiset count()
method returns the number of times an element is present in the multiset. The time complexity
of this operation is O(log n + f ) where f is the number of occurrences of the specified element
in the multiset. This is because the log n factor searches for the element, and the f factor
iterates linearly through the sorted set to find the number of occurrences.
CHAPTER 4. BUILT-IN DATA STRUCTURES 19
multiset<int> ms;
ms.insert(1); // [1]
ms.insert(14); // [1, 14]
ms.insert(9); // [1, 9, 14]
ms.insert(2); // [1, 2, 9, 14]
ms.insert(9); // [1, 2, 9, 9, 14]
ms.insert(9); // [1, 2, 9, 9, 9, 14]
cout << ms.count(4) << '\n'; // 0
cout << ms.count(9) << '\n'; // 3
cout << ms.count(14) << '\n'; // 1
The begin(), end(), lower_bound(), and upper_bound() operations work the same way
they do in the normal sorted set.
4.5 Problems
Again, note that CSES’s grader is very slow, so don’t worry if you encounter a Time
Limit Exceeded verdict; as long as you pass the majority of test cases within the time limit,
you can consider the problem solved, and move on.
Bronze
20
Chapter 5
Simulation
In many problems, we can simply simulate what we’re told to do by the problem statement.
Since there’s no formal algorithm involved, the intent of the problem is to assess competence
with one’s programming language of choice and knowledge of built-in data structures. At
least in USACO Bronze, when a problem statement says to find the end result of some
process, or to find when something occurs, it’s usually sufficient to simulate the process.
5.1 Example 1
Alice and Bob are standing on a 2D plane. Alice starts at the point (0, 0), and Bob
starts at the point (R, S) (1 ≤ R, S ≤ 1000). Every second, Alice moves M units to the
right, and N units up. Every second, Bob moves P units to the left, and Q units down.
(1 ≤ M, N, P, Q ≤ 10). Determine if Alice and Bob will ever meet (be at the same point at
the same time), and if so, when.
INPUT FORMAT:
The first line of the input contains R and S.
The second line of the input contains M , N , P , and Q.
OUTPUT FORMAT:
Please output a single integer containing the number of seconds after the start at which Alice
and Bob meet. If they never meet, please output −1.
Solution
We can simulate the process. After inputting the values of R, S, M , N , P , and Q, we can
keep track of Alice’s and Bob’s x- and y-coordinates. To start, we initialize variables for their
respective positions. Alice’s coordinates are initially (0, 0), and Bob’s coordinates are (R, S)
respectively. Every second, we increase Alice’s x-coordinate by M and her y-coordinate by
N , and decrease Bob’s x-coordinate by P and his y-coordinate by Q.
Now, when do we stop? First, if Alice and Bob ever have the same coordinates, then we
are done. Also, since Alice strictly moves up and to the right and Bob strictly moves down
and to the left, if Alice’s x- or y-coordinates are ever greater than Bob’s, then it is impossible
for them to meet. Example code will be displayed below (Here, as in other examples, input
processing will be omitted):
21
CHAPTER 5. SIMULATION 22
5.2 Example 2
There are N buckets (5 ≤ N ≤ 105 ), each with a certain capacity Ci (1 ≤ Ci ≤ 100). One
day, after a rainstorm, each bucket is filled with Ai units of water (1 ≤ Ai ≤ Ci ). Charlie
then performs the following process: he pours bucket 1 into bucket 2, then bucket 2 into
bucket 3, and so on, up until pouring bucket N − 1 into bucket N . When Charlie pours
bucket B into bucket B + 1, he pours as much as possible until bucket B is empty or bucket
B + 1 is full. Find out how much water is in each bucket once Charlie is done pouring.
INPUT FORMAT:
The first line of the input contains N .
The second line of the input contains the capacities of the buckets, C1 , C2 , . . . , Cn .
The third line of the input contains the amount of water in each bucket A1 , A2 , . . . , An .
OUTPUT FORMAT:
Please print one line of output, containing N space-separated integers: the final amount of
water in each bucket once Charlie is done pouring.
Solution:
Once again, we can simulate the process of pouring one bucket into the next. The amount of
milk poured from bucket B to bucket B + 1 is the smaller of the amount of water in bucket
B (after all previous operations have been completed) and the remaining space in bucket
B + 1, which is CB+1 − AB+1 . We can just handle all of these operations in order, using an
array C to store the maximum capacities of each bucket, and an array A to store the current
water level in each bucket, which we update during the process. Example code is below (note
that arrays are zero-indexed, so the indices of our buckets go from 0 to N − 1 rather than
from 1 to N ).
5.3 Problems
1. USACO December 2018 Bronze Problem 1: Mixing Milk
http://www.usaco.org/index.php?page=viewproblem2&cpid=855
4. USACO February 2017 Bronze Problem 3: Why Did the Cow Cross the Road III
http://www.usaco.org/index.php?page=viewproblem2&cpid=713
Complete Search
In many problems (especially in Bronze), it’s sufficient to check all possible cases in
the solution space, whether it be all elements, all pairs of elements, or all subsets, or all
permutations. Unsurprisingly, this is called complete search (or brute force), because it
completely searches the entire solution space.
6.1 Example 1
You are given N (3 ≤ N ≤ 5000) integer points on the coordinate plane. Find the square
of the maximum Euclidean distance (aka length of the straight line) between any two of the
points.
INPUT FORMAT:
The first line contains an integer N .
The second line contains N integers, the x-coordinates of the points: x1 , x2 , . . . , xn (−1000 ≤
xi ≤ 1000).
The third line contains N integers, the y-coordinates of the points: y1 , y2 , . . . , yn (−1000 ≤
yi ≤ 1000).
OUTPUT FORMAT:
Print one integer, the square of the maximum Euclidean distance between any two of the
points.
Solution:
We can brute-force every pair of points and find the square of the distance between them,
by squaring the formula for Euclidean distance: distance2 = (x2 − x1 )2 + (y2 − y1 )2 . Thus,
we store the coordinates in vectors X[] and Y[], such that X[i] and Y[i] are the x- and
y-coordinates of the ith point, respectively. Then, we iterate through all possible pairs of
points, using a variable max to store the maximum square of distance between any pair seen
so far, and if the square of the distance between a pair is greater than our current maximum,
24
CHAPTER 6. COMPLETE SEARCH 25
A couple notes: first, since we’re iterating through all pairs of points, we start the j loop
from j = i + 1 so that point i and point j are never the same point. Furthermore, it makes
it so that each pair is only counted once. In this problem, it doesn’t matter whether we
double-count pairs or whether we allow i and j to be the same point, but in other problems
where we’re counting something rather than looking at the maximum, it’s important to be
careful that we don’t overcount. Secondly, the problem asks for the square of the maximum
Euclidean distance between any two points. Some students may be tempted to maintain the
maximum distance in a variable, and then square it at the end when outputting. However,
the problem here is that while the square of the distance between two integer points is always
an integer, the distance itself isn’t guaranteed to be an integer. Thus, we’ll end up shoving a
non-integer value into an integer variable, which truncates the decimal part. Using a floating
point variable isn’t likely to work either, due to precision errors (use of floating point decimals
should generally be avoided when possible).
CHAPTER 6. COMPLETE SEARCH 26
[1, 2, 3], [2, 1, 3], [3, 1, 2], [1, 3, 2], [2, 3, 1], [3, 2, 1]
do {
check(v); // process or check the current permutation for validity
} while(next_permutation(v.begin(), v.end()));
6.3 Problems
1. USACO February 2020 Bronze Problem 1: Triangles
http://usaco.org/index.php?page=viewproblem2&cpid=1011
7.2 Ad-hoc
Ad-hoc problems are problems that don’t fall into any standard algorithmic category
with well known solutions. They are usually unique problems intended to be solved with
unconventional techniques. In ad-hoc problems, it’s helpful to look at the constraints given in
the problem and devise potential time complexities of solutions; this, combined with details
in the problem statement itself, may give an outline of the solution.
Unfortunately, since ad-hoc problems don’t have solutions consisting of well known
algorithms, we can’t systematically teach you how to do them. The best way of learning how
to do ad-hoc is to practice. Of course, the problem solving intuition from math contests (if
you did them) is quite helpful, but otherwise, you can develop this intutition from practicing
ad-hoc problems.
While solving these problems, make sure to utilize what you’ve learned about the built-in
data structures and algorithmic complexity analysis, from chapters 2, 3, and 4. Since ad-hoc
problems comprise a significant portion of bronze problems, we’ve included a large selection
of them below for your practice.
28
CHAPTER 7. ADDITIONAL BRONZE TOPICS 29
7.3 Problems
Square and Rectangle Geometry
1. USACO December 2017 Bronze Problem 1: Blocked Billboard
http://usaco.org/index.php?page=viewproblem2&cpid=759
Ad-hoc problems
5. USACO January 2016 Bronze Problem 1: Promotion Counting
http://usaco.org/index.php?page=viewproblem2&cpid=591
Silver
30
Chapter 8
8.1 Comparators
C++ has a built-in function for sorting: std::sort(first, last) that sorts the elements
in the range in ascending order. In particular, sort(arr, arr + N) sorts an entire array of
size N , and sort(v.begin(), v.end()) sorts a vector v. However, if we we want to sort
elements in a self-defined order, then we’ll need to use a custom comparator.
Normally, sorting functions rely on moving objects with a lower value in front of objects
with a higher value if sorting in ascending order, and vice versa if in descending order. This
is done through comparing two objects at a time. What a comparator does is compare two
objects as follows, based on our comparison criteria:
Essentially, the comparator determines whether object x belongs to the left of object y in
a sorted ordering.
In addition to returning the correct answer, comparators should also satisfy the following
conditions:
• The function must be consistent with respect to reversing the order of the arguments:
if x 6= y and compare(x, y) is positive, then compare(y, x) should be negative and
vice versa
31
CHAPTER 8. SORTING AND COMPARATORS 32
struct Person {
int height;
int weight;
};
int main() {
Person p;
p.height = 60; // assigns 60 to the height of p
p.weight = 100; // assigns 100 to the weight of p
}
Let’s say we have an array Person arr[N]. To sort the array, we need to make custom
comparator which will be a function, and then pass the function as a parameter into the
build-in sort function:
int main() {
sort(arr, arr+N, cmp); // sorts the array in ascending order by height
}
If we instead wanted to sort in descending order, this is also very simple. Instead of the
cmp function returning return a.height < b.height;, it should do return a.height >
b.height;.
int main() {
sort(arr, arr+N, cmp); // sorts the array in ascending order by height and
,→ then weight if the heights are equal
}
CHAPTER 8. SORTING AND COMPARATORS 33
int main() {
sort(arr, arr+N); // sorts the array in ascending order by height and weight
,→ as a secondary if height is equal
}
8.3 Problems
1. USACO US Open 2018 Silver Problem 2: Lemonade Line
http://www.usaco.org/index.php?page=viewproblem2&cpid=835
Greedy Algorithms
Greedy algorithms are algorithms that select the most optimal choice at each step, instead
of looking at the solution space as a whole. This reduces the problem to a smaller problem at
each step. However, as greedy algorithms never recheck previous steps, they sometimes lead
to incorrect answers. Moreover, in a certain problem, there may be more than one possible
greedy algorithm; usually only one of them is correct. This means that we must be extremely
careful when using the greedy method. However, when they are correct, greedy algorithms
are extremely efficient.
Greedy is not a single algorithm, but rather a way of thinking that is applied to problems.
There’s no one way to do greedy algorithms. Hence, we use a selection of well-known examples
to help you understand the greedy paradigm.
Usually, when using a greedy algorithm, there is a heuristic or value function that
determines which choice is considered most optimal.
34
CHAPTER 9. GREEDY ALGORITHMS 35
In this example, the greedy algorithm selects two events, which is optimal. However, this
doesn’t always work, as shown by the following counterexample:
In this case, the greedy algorithm selects to attend only one event. However, the optimal
solution would be the following:
CHAPTER 9. GREEDY ALGORITHMS 36
Coin Change
This problem gives several coin denominations, and asks for the minimum number of
coins needed to make a certain value. Greedy algorithms can be used to solve this problem
only in very specific cases (it can be proven that it works for the American as well as the
Euro coin systems). However, it doesn’t work in the general case. For example, let the coin
CHAPTER 9. GREEDY ALGORITHMS 37
denominations be {1, 3, 4}, and say the value we want is 6. The optimal solution is {3, 3},
which requires only two coins, but the greedy method of taking the highest possible valued
coin that fits in the remaining denomination gives the solution {4, 1, 1}, which is incorrect.
Knapsack
The knapsack problem gives a number of items, each having a weight and a value, and
we want to choose a subset of these items. We are limited to a certain weight, and we want
to maximize the value of the items that we take.
Let’s take the following example, where we have a maximum capacity of 4:
If we use greedy based on highest value first, we choose item A and then we are done, as
we don’t have remaining weight to fit either of the other two. Using greedy based on value
per weight again selects item A and then quits. However, the optimal solution is to select
items B and C, as they combined have a higher value than item A alone. In fact, there is no
working greedy solution. The solution to this problem uses dynamic programming, which is
beyond the scope of this book.
9.4 Problems
1. USACO December 2015 Silver Problem 2: High Card Wins
http://usaco.org/index.php?page=viewproblem2&cpid=571
3. USACO February 2017 Silver Problem 1: Why Did The Cow Cross The Road
http://www.usaco.org/index.php?page=viewproblem2&cpid=714
Chapter 10
Graph Theory
Graph theory is one of the most important topics at the Silver level and above. Graphs
can be used to represent many things, from images to wireless signals, but one of the simplest
analogies is to a map. Consider a map with several cities and highways connecting the cities.
Some of the problems relating to graphs are:
• If we have a map with some cities and roads, what’s the shortest distance I have to
travel to get from point A to point B?
• Consider a map of cities and roads. Is city A connected to city B? Consider a region to
be a group of cities such that each city in the group can reach any other city in said
group, but no other cities. How many regions are in this map, and which cities are in
which region?
5 2 −1 5
5 2 6 5 2 6
2 3
3 3
Figure 10.1: An undirected unweighted graph (left) and a directed weighted graph (right)
38
CHAPTER 10. GRAPH THEORY 39
A connected component is a set of nodes within which any node can reach any other
node. For example, in this graph, nodes 1, 2, and 3 are a connected component, nodes 4 and
5 are a connected component, and node 6 is its own component.
3 6
1 2 4 5
10.2 Trees
A tree is a special type of graph satisfying two constraints: it is acyclic, meaning there
are no cycles, and the number of edges is one less than the number of nodes. Trees satisfy
the property that for any two nodes A and B, there is exactly one way to travel between A
and B.
2 3
4 5 6 7
The root of a tree is the one vertex that is placed at the top, and is where we usually
start our tree traversals from. Usually, problems don’t tell us where the tree is rooted at, and
it usually doesn’t matter either; trees can be arbitrarily rooted (here, we’ll use the convention
of rooting at index 1).
Every node except the root node has a parent. The parent of a node s is defined as
follows: On the path from the root to s, the node that is one closer to the root than s is the
parent of s. Each non-root node has a unique parent.
Child nodes are the opposite. They lie one farther away from the root than their parent
node. Unlike parent nodes, these are not unique. Each node can have arbitrarily many child
nodes, and nodes can also have zero children. If a node s is the parent of a node t, then t is
the child node of s.
A leaf node is a node that has no children. Leaf nodes can be identified quite easily
because there is only one edge adjacent to them.
CHAPTER 10. GRAPH THEORY 40
In our example tree above, node 1 is the root, nodes 2 and 3 are children of node 1, nodes
4, 5, and 6 are children of 2, and node 7 is child of 3. Nodes 4, 5, 6, and 7 are leaf nodes.
6 7 // 6 nodes, 7 edges
// the following lines represent edges.
1 2
1 4
1 5
2 3
2 4
3 5
4 6
1 4
5 2 6
Graphs can be represented in three ways: Adjacency List, Adjacency Matrix, and Edge
List. Regardless of how the graph is represented, it’s important that it be stored globally
and statically, because we need to be able to access it from outside the main method, and
call the graph searching and traversal methods on it.
CHAPTER 10. GRAPH THEORY 41
Adjacency List
The adjacency list is the most commonly used method of storing graphs. When we use
DFS, BFS, Dijkstra’s, or other single-source graph traversal algorithms, we’ll want to use an
adjacency list. In an adjacency list, we maintain a length N array of lists. Each list stores
the neighbors of one node. In an undirected graph, if there is an edge between node a and
node b, we add a to the list of b’s neighbors, and b to the list of a’s neighbors. In a directed
graph, if there is an edge from node a to node b, we add b to the list of a’s neighbors, but
not vice versa.
4
1 4
3 9 2 3
5 2 6
4 5
1
3
Adjacency lists take up O(N + M ) space, because each node corresponds to one list of
neighbors, and each edge corresponds to either one or two endpoints (directed vs undirected).
In an adjacency list, we can find (and iterate through) the neighbors of a node easily. Hence,
the adjacency list is the graph representation we should be using most of the time.
Often, we’ll want to maintain a array visited, which is a boolean array representing
whether each node has been visited. When we visit node k (0-indexed), we mark visited[k]
true, so that we know not to return to it.
Code for setting up an adjacency list is as follows:
int main(){
CHAPTER 10. GRAPH THEORY 42
If we’re dealing with a weighted graph, we’ll declare an Edge class or struct that stores
two variables: the second endpoint of the edge, and the weight of the edge, and we store an
array of lists of edges rather than an array of lists of integers.
struct Edge
{
int to, weight;
Edge(int dest, int w):
to(dest), weight(w)
{
}
};
Adjacency Matrix
Another way of representing graphs is the adjacency matrix, which is an N by N 2-
dimensional array that stores for each pair of indices (a, b), stores whether there is an
edge between a and b. Start by initializing every entry in the matrix to zero (this is done
automatically in C++ if you declare the array globally), and then for undirected graphs,
for each edge between indices a and b, set adj[a][b] and adj[b][a] to 1 (if unweighted)
or the edge weight (if weighted). If the graph is directed, for an edge from a to b, only set
adj[a][b].
4
1 4
3 9 2 3
5 2 6
4 5
1
3
CHAPTER 10. GRAPH THEORY 43
× 0 1 2 3 4 5
0 0 9 0 4 3 0
1 9 0 5 2 0 0
2 0 5 0 0 4 1
3 4 2 0 0 0 3
4 3 0 4 0 0 0
5 0 0 1 3 0 0
At the Silver level, we generally won’t be using the adjacency matrix much, but it’s helpful
to know if it does come up. The primary use of the adjacency matrix is the Floyd-Warshall
algorithm, which is beyond the scope of this book.
Code for setting up an adjacency matrix is as follows:
int main(){
cin >> n >> m;
for(int i = 0; i < m; i++){ // read in each of the m edges
int a, b;
cin >> a >> b;
a--; b--; // we subtract 1 because our array is zero-indexed
adj[a][b] = 1; // or set equal to w if graph is weighted
adj[b][a] = 1; // or set equal to w if graph is weighted;
// ignore above line if graph is directed
}
return 0;
}
Edge List
The last graph representation is the edge list. Usually, we use this in weighted undirected
graphs when we want to sort the edges by weight (for DSU, for example). In the edge list,
we simply store a single list of all the edges, in the form (a, b, w) where a and b are the nodes
that the edge connects, and w is the edge weight. Note that in an edge list, we do NOT add
each edge twice; there is only one place for us to add the edges, so we only do so once.
CHAPTER 10. GRAPH THEORY 44
4
1 4
3 9 2 3
5 2 6
4 5
1
3
(0, 1, 9), (0, 3, 4), (0, 4, 3), (1, 3, 2), (3, 5, 3), (2, 4, 4), (2, 1, 5), (2, 5, 1)
struct Edge{
int a, b, w;
Edge(int start, int end, int weight):
a(start), b(end), w(weight)
{
}
bool operator<(const Edge & e)
const{ // sort order is ascending, by weight
// to sort in descending order, just negate the value of the compare
,→ function.
return w < e.w;
}
};
Code for the edge list is as follows, using the above edge class:
int main(){
cin >> n >> m;
for(int i = 0; i < m; i++){ // for each of the m edges
int a, b, w;
cin >> a >> b >> w;
a--; b--; // we subtract 1 because our array is zero-indexed
edges.push_back(Edge(a, b, w)); // add the edge to the list
}
CHAPTER 10. GRAPH THEORY 45
sort(edges.begin(), edges.end());
return 0;
}
Depth-first search
Depth-first search continues down a single path as far as possible; once it has no more
vertices to visit along that path, it backtracks until it finds more vertices to visit. Depth-first
search will process all nodes that are reachable (connected by edges) to the starting node.
Let’s look at an example of how this works. Depth first-search can start at any node, but by
convention we’ll start the search at node 1. We’ll use the following color scheme: blue for
nodes we have already visited, red for nodes we are currently processing, and black for nodes
that have not been visited yet.
The DFS starts from node 1 and then proceeds to node 2, as it’s the only neighbor of
node 1:
1 2 3 1 2 3
4 5 4 5
Now, the DFS goes to node 3 and then 5, following a single path to the end until it has no
more nodes to process:
1 2 3 1 2 3
4 5 4 5
Lastly, the DFS backtracks to visit node 4, which was skipped over previously.
1 2 3
4 5
CHAPTER 10. GRAPH THEORY 46
Depth-first search is implemented recursively because it allows for much simpler and shorter
code. The algorithm is as follows:
Algorithm: Recursive implementation for depth-first traversal of a graph
Function DFS
Input : start, the 0-indexed number of the starting vertex
visited(start) ← true
foreach vertex k adjacent to start do
if visited(k) is false then
DFS (k)
end
end
Code:
Breadth-first search
Breadth-first search visits nodes in order of distance away from the starting node; it first
visits all nodes that are one edge away, then all nodes that are two edges away, and so on.
Let’s use the same example graph that we used earlier: The BFS starts from node 1 and
then proceeds to node 2, as it’s the only neighbor of node 1:
1 2 3 1 2 3
4 5 4 5
Now, the BFS goes to node 3, and then node 4, because both of them are two edges away
from node 1:
1 2 3 1 2 3
4 5 4 5
CHAPTER 10. GRAPH THEORY 47
1 2 3
4 5
int v = q.front();
q.pop();
for(int e : adj[v]){
if(dist[e] == -1){
dist[e] = dist[v] + 1;
q.push(e);
}
}
}
}
Iterative DFS
If you encounter stack overflows while using recursive DFS, you can write an iterative
DFS, which is just BFS but with nodes stored on a stack rather than a queue.
10.5 Floodfill
Floodfill is an algorithm that identifies and labels the connected component that a
particular cell belongs to, in a multi-dimensional array. Essentially, it’s DFS, but on a grid,
and we want to find the connected component of all the connected cells with the same number.
For example, let’s look at the following grid and see how floodfill works, starting from the
top-left cell. The color scheme will be the same: red for the node currently being processed,
blue for nodes already visited, and uncolored for nodes not yet visited.
2 2 1
2 1 1
2 2 1
2 2 1
2 1 1
2 2 1
2 2 1
2 1 1
2 2 1
2 2 1
2 1 1
2 2 1
2 2 1
2 1 1
2 2 1
CHAPTER 10. GRAPH THEORY 49
2 2 1
2 1 1
2 2 1
As opposed to an explicit graph where the edges are given, a grid is an implicit graph.
This means that the neighbors are just the nodes directly adjacent in the four cardinal
directions.
Usually, grids given in problems will be N by M , so the first line of the input contains the
numbers N and M . In this example, we will use an two-dimensional integer array to store the
grid, but depending on the problem, a two-dimensional character array or a two-dimensional
boolean array may be more appropriate. Then, there are N rows, each with M numbers
containing the contents of each square in the grid. Example input might look like the following
(varies between problems):
3 4
1 1 2 1
2 3 2 1
1 3 3 3
int grid[MAXN][MAXM];
int n, m;
int main(){
cin >> n >> m;
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
cin >> grid[i][j];
}
}
return 0;
}
When doing floodfill, we will maintain an N × M array of bools to keep track of which
squares have been visited, and a global variable to maintain the size of the current component
we are visiting. Make sure to store the grid, the visited array, dimensions, and the current
size variable globally.
This means that we want to recursively call the search function from the squares above,
below, and to the left and right of our current square. The algorithm to find the size of a
connected component in a grid using floodfill is as follows (we’ll also maintain a 2d visited
CHAPTER 10. GRAPH THEORY 50
array):
Algorithm: Floodfill of a graph
Function main
// Input/output, global vars, etc hidden
for i ← 0 to n − 1 do
for j ← 0 to m − 1 do
if the square at (i, j) is not visited then
currentSize ← 0
floodfill(i, j, grid[i][j])
Process the connected component
end
end
end
Function floodfill
Input : r, c, color
// row and column index of starting square, target color
if r or c is out of bounds then
return
end
if the cell at (r, c) is the wrong color then
return
end
if the square at (r, c) has already been visited then
return
end
visited[r][c] ← true
currentSize ← currentSize + 1
floodfill(r, c + 1, color)
floodfill(r, c − 1, color)
floodfill(r − 1, c, color)
floodfill(r + 1, c, color)
The code below shows the global/static variables we need to maintain while doing floodfill,
and the floodfill algorithm itself.
int main(){
/**
* input code and other problem-specific stuff here
*/
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(!visited[i][j]){
currentSize = 0;
floodfill(i, j, grid[i][j]);
// start a floodfill if the square hasn't
// already been visited, and then
// store or otherwise use the component size for whatever
// it's needed for
}
}
}
return 0;
}
To achieve this, we store sets as trees, with the root of the tree representing the “parent”
of the set. Initially, we store each node as its own set. Then, we combine their sets when we
add an edge between two nodes. The image below illustrates this structure.
CHAPTER 10. GRAPH THEORY 52
However, this naive implementation of a DSU isn’t much better than simply running a
floodfill. As the recursing up the tree of a set to find it’s root can be time-consuming for
trees with long chains, the runtime ultimately degrades to still being O(nm) for n nodes and
m edges.
Now that we understand the general idea of a DSU, we can improve the runtime of this
implementation using an optimization known as path compression. The general idea is to
reassign nodes in the tree as you are recursively calling the find method to prevent long
chains from forming. Here is a rewritten find method representing this idea:
The following image demonstrates how the tree with parent 1 is compressed after find(6)
is called. All of the bolded nodes in the final tree were visited during the recursive operation,
and now point to the root.
CHAPTER 10. GRAPH THEORY 54
With this new optimization, the runtime reduces to O(n log n), far better than our naive
algorithm. Further optimizations can reduce the runtime of DSU to nearly constant. However,
those techniques and the proof of complexity for these optimizations are both unnecessary for
and out of the scope of the USACO Silver division, so they will not be included in this book.
As there does not contain cycles in the graph, a useful property of directed acyclic graphs
is that you can construct an ordering of nodes such that for any edge from node u to node
v, node u appears in the ordering before node v. More formally, such an ordering is known
as a topological sorting of a directed acyclic graph, which can be useful for algorithms
like dynamic programming. However, as such algorithms are out of the scope of this book,
topological sorting simply serves as a clean way of envisioning an ordering of nodes in the
graph.
Bipartite Graphs
A bipartite graph is a graph such that each node can be colored in one of two colors,
such that no two adjacent nodes have the same color. For example, the following graph is
bipartite:
1 2 3
4 5
A graph is bipartite if and only if there are no cycles of odd length. For example, the
following graph is not bipartite, because it contains a cycle of length 3.
1 2 3
The following image depicts how a bipartite graph splits vertices into two “groups”
depending on their color.
CHAPTER 10. GRAPH THEORY 56
end
end
end
return true // bipartite
10.8 Problems
DFS/BFS Problems
1. USACO January 2018 Silver Problem 3: MooTube
http://www.usaco.org/index.php?page=viewproblem2&cpid=788
CHAPTER 10. GRAPH THEORY 57
DSU Problems
Many of these problems do not require DSU. However, they become much easier to do if
you understand it.
Prefix Sums
Index i 0 1 2 3 4 5 6
arr[i] 0 1 6 4 2 5 3
Naively, for every query, we can iterate through all entries from index a to index b to add
them up. Since we have Q queries and each query requires a maximum of O(N ) operations
to calculate the sum, our total time complexity is O(N Q). For most problems of this nature,
the constraints will be N, Q ≤ 105 , so N Q is on the order of 1010 . This is not acceptable; it
will almost always exceed the time limit.
Instead, we can use prefix sums to process these array sum queries. We designate a prefix
sum array prefix. First, because we’re 1-indexing the array, set prefix[0] = 0, then for
indices k such that 1 ≤ k ≤ n, define the prefix sum array as follows:
k
X
prefix[k] = arr[i]
i=1
Basically, what this means is that the element at index k of the prefix sum array stores the
sum of all the elements in the original array from index 1 up to k. This can be calculated
easily in O(N ) by the following formula:
prefix[k] = prefix[k-1] + arr[k]
For the example case, our prefix sum array looks like this:
Index i 0 1 2 3 4 5 6
prefix[i] 0 1 7 11 13 18 21
58
CHAPTER 11. PREFIX SUMS 59
Now, when we want to query for the sum of the elements of arr between (1-indexed)
indices a and b inclusive, we can use the following formula:
b
X b
X a−1
X
arr[i] = arr[i] − arr[i]
i=a i=1 i=1
Using our definition of the elements in the prefix sum array, we have
b
X
arr[i] = prefix[b] − prefix[a-1]
i=a
Since we are only querying two elements in the prefix sum array, we can calculate subarray
sums in O(1) per query, which is much better than the O(N ) per query that we had before.
Now, after an O(N ) preprocessing to calculate the prefix sum array, each of the Q queries
takes O(1) time. Thus, our total time complexity is O(N + Q), which should now pass the
time limit.
Let’s do an example query and find the subarray sum between indices a = 2 and b = 5,
inclusive,
P5 in the 1-indexed arr. From looking at the original array, we see that this is
i=2 arr[i] = 6 + 4 + 2 + 5 = 17.
Index i 0 1 2 3 4 5 6
arr[i] 0 1 6 4 2 5 3
Index i 0 1 2 3 4 5 6
prefix[i] 0 1 7 11 13 18 21
0 0 0 0 0 0
0 1 5 6 11 8
0 1 7 11 9 4
0 4 6 1 3 2
0 7 5 4 2 3
Naively, each sum query would then take O(N M ) time, for a total of O(QN M ). This is
too slow.
Let’s take the following example region, which we want to sum:
CHAPTER 11. PREFIX SUMS 60
0 0 0 0 0 0
0 1 5 6 11 8
0 1 7 11 9 4
0 4 6 1 3 2
0 7 5 4 2 3
Manually summing all the cells, we have a submatrix sum of 7 + 11 + 9 + 6 + 1 + 3 = 37.
The first logical optimization would be to do one-dimensional prefix sums of each row.
Then, we’d have the following row-prefix sum matrix. The desired subarray sum of each row
in our desired region is simply the green cell minus the red cell in that respective row. We do
this for each row, to get (28 − 1) + (14 − 4) = 37.
0 0 0 0 0 0
0 1 6 12 23 31
0 1 8 19 28 32
0 4 10 11 14 16
0 7 12 16 18 21
Now, if we wanted to find a submatrix sum, we could break up the submatrix into a
subarray for each row, and then add their sums, which would be calculated using the prefix
sums method described earlier. Since the matrix has N rows, the time complexity of this is
O(QN ). This is better, but still usually not fast enough.
To do better, we can do two-dimensional prefix sums. In our two dimensional prefix sum
array, we have
Xa X b
prefix[a][b] = arr[i][j]
i=1 j=1
This can be calculated as follows for row index 1 ≤ i ≤ n and column index 1 ≤ j ≤ m:
prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] − prefix[i-1][j-1] + arr[i][j]
The submatrix sum between rows a and A and columns b and B, can thus be expressed as
follows:
X A X B
arr[i][j] = prefix[A][B] − prefix[a-1][B]
i=a j=b − prefix[A][b-1] + prefix[a-1][b-1]
Summing the blue region from above using the 2d prefix sums method, we add the value
of the green square, subtract the values of the red squares, and then add the value of the
gray square. In this example, we have 65 − 23 − 6 + 1 = 37, as expected.
0 0 0 0 0 0
0 1 6 12 23 31
0 2 14 31 51 63
0 6 24 42 65 79
0 13 36 58 83 100
Since no matter the size of the submatrix we are summing, we only need to access 4 values
of the 2d prefix sum array, this runs in O(1) per query after an O(N M ) preprocessing. This
is fast enough.
CHAPTER 11. PREFIX SUMS 61
11.3 Problems
1. USACO December 2015 Silver Problem 3: Breed Counting
http://usaco.org/index.php?page=viewproblem2&cpid=572
5. (2D Prefix Sums) USACO February 2019 Silver Problem 2: Painting the Barn
http://www.usaco.org/index.php?page=viewproblem2&cpid=919
Chapter 12
Binary Search
In other words, we want to reduce the search space to something of the following form,
using a check function as we described above.
Then, we find the point at which true becomes false, using binary search.
Below, we present two algorithms for binary search. The first implementation may be
more intuitive, because it’s closer to the binary search most students learned, while the
62
CHAPTER 12. BINARY SEARCH 63
The binary search function for this is very similar. Find the last value of x such that
check(x) is false, then the answer is x + 1.
12.2 Example
Source: Codeforces Round 577 (Div. 2) Problem C
https://codeforces.com/contest/1201/problem/C
Given an array arr of n integers, where n is odd, we can perform the following operation
on it k times: take any element of the array and increase it by 1. We want to make the
median of the array as large as possible, after k operations.
Constraints: 1 ≤ n ≤ 2 · 105 , 1 ≤ k ≤ 109 and n is odd.
CHAPTER 12. BINARY SEARCH 64
The solution is as follows: we first sort the array in ascending order. Then, we binary
search for the maximum possible median. We know that the number of operations required
to raise the median to x increases monotonically as x increases, so we can use binary search.
For a given median value x, the number of operations required to raise the median to x is
n
X
max(0, x − arr[i])
i=(n+1)/2
If this value is less than or equal to k, then x can be the median, so our check function
returns true. Otherwise, x cannot be the median, so our check function returns false.
Solution code (using the second implementation of binary search):
int n;
long long k;
vector<long long> v;
int main() {
cin >> n >> k;
for(int i = 0; i < n; i++){
int t;
cin >> t;
v.push_back(t);
}
sort(v.begin(), v.end());
12.3 Problems
1. USACO December 2018 Silver Problem 1: Convention
http://www.usaco.org/index.php?page=viewproblem2&cpid=858
66
CHAPTER 13. ELEMENTARY NUMBER THEORY 67
i n v
2 252 {}
2 126 {2}
2 63 {2, 2}
3 21 {2, 2, 3}
3 7 {2, 2, 3, 3}
√
At this point, the for loop terminates, because i is already 3 which is greater than b 7c. In
the last step, we add 7 to the list of factors v, because it otherwise won’t be added, for a
final prime factorization of {2, 2, 3, 3, 7}.
Finding the GCD of two numbers can be done in O(log n) time, where n = min(a, b).
The least common multiple (LCM) of two integers a and b is the smallest integer
divisible by both a and b.
The LCM can easily be calculated from the following property with the GCD:
a·b
lcm(a, b) =
gcd(a, b)
If we want to take the GCD or LCM of more than two elements, we can do so two at a time,
in any order. For example,
Under a prime moduli, division does exist; however it’s rarely used in problems and is
beyond the scope of this book.
13.4 Problems
1. CodeForces VK Cup 2012 Wildcard Round 1
https://codeforces.com/problemset/problem/162/C
Chapter 14
2SUM Problem
Given an array of N elements (1 ≤ N ≤ 105 ), find two elements that sum to X, if they
exist. We can solve this problem using two pointers; set one pointer at the beginning and
one pointer at the end of the array. Then, we consider the sum of the numbers at the indices
of the pointers. If the sum is too small, advance the left pointer towards the right, and if the
sum is too large, advance the right pointer towards the left. Repeat until either the correct
sum is found, or the pointers meet (in which case there is no solution).
Let’s take the following example array, where N = 6 and X = 15
1 7 11 10 5 13
First, we sort the array:
1 5 7 10 11 13
We then place the left pointer at the start of the array, and the right pointer at the end
of the array.
1 5 7 10 11 13
Then, run and repeat this process: If the sum of the pointer elements is less than X,
move the left pointer one step to the right. If the sum is greater than X, move the right
pointer one step to the left. The example is as follows. First, the sum 1 + 13 = 14 is too
small, so we move the left pointer one step to the right.
1 5 7 10 11 13
69
CHAPTER 14. ADDITIONAL SILVER TOPICS 70
Now, 5 + 13 = 18 overshoots the sum we want, so we move the right pointer one step to
the left.
1 5 7 10 11 13
At this point we have 5 + 11 = 16, still too big. We continue moving the right pointer to
the left.
1 5 7 10 11 13
Subarray Sum
Given an array of N (1 ≤ N ≤ 105 ) positive elements, find a contiguous subarray that
sums to X. We can do this in a similar manner to how we did the 2SUM problem: except
this time we start both pointers at the left, and the pointers mark the beginning and end of
the subarray we are currently checking. We advance the right pointer one step to the right if
the total of the current subarray is too small, advance the left pointer one step to the right if
the current total is too large, and we are done when we find the correct total. Let’s look at
the following example, for N = 6 and X = 17.
This subarray contains only the first element. We advance the right pointer towards the
right, until our current sum reaches or exceeds the target sum of X.
6 3 6 8 2 5
6 3 6 8 2 5
6 3 6 8 2 5
6 3 6 8 2 5
CHAPTER 14. ADDITIONAL SILVER TOPICS 71
Now, our subarray contains a sum of 23, so we’ve gone too far. We now advance the
left pointer towards the right, to eliminate the leftmost elements from the subarray we are
considering.
6 3 6 8 2 5
X1 X2
P1 Q1 P2 Q2
Y1 Y2
If the two conditions hold and some of the signs are zero, then this means that the segments
intersect at their endpoints. If the problem does not count these as intersecting, then consider
zero to have the same sign as both positive and negative.
However, there is a special case. If the signs of all four areas are zero, then all four points
lie on a line. To check if they intersect in this case, we just check whether one point is
between the others. In particular, we check if P or Q is on XY or if X is on P Q. We don’t
need to check if Y is on P Q because if the segments do intersect, we will have two instances
of points on the other segments.
Here’s a full implementation:
struct Point
{
int x, y;
Point (int xst, int yst):
x(xst), y(yst)
{
}
};
where the ai s are either 0 or 1 and n = blog2 xc. There is also a bit at the beginning
designating whether the number is positive or negative, called the sign bit. In this section,
we work only with positive numbers for the sake of simplicity, so we will omit the sign bit in
binary representations.
For example:
17 = 24 + 20 = 100012
In a 32-bit integer, this is actually written as
000000000000000000000000000100012
but we’ll leave out the sign bit and other leading zeros as they’re not necessary for our
purposes.
Each digit in the binary representation, which is either 0 or 1, is called a bit.
Bitwise Operations
There are several binary operations on binary numbers called bitwise operations. These
operations are applied separately for each bit position. The common binary operations are
shown in table 14.1:
CHAPTER 14. ADDITIONAL SILVER TOPICS 75
The AND operation (&) returns 1 if and only if both bits are 1.
19 & 26
1 0 0 1 1 = 19
& 1 1 0 1 0 = 26
= 1 0 0 1 0 = 18
The OR operation (|) returns 1 if either bit is 1.
19 | 26
1 0 0 1 1 = 19
| 1 1 0 1 0 = 26
= 1 1 0 1 1 = 27
The XOR operation (∧) returns 1 if and only if exactly one of the bits is 1.
19 ∧ 26
1 0 0 1 1 = 19
& 1 1 0 1 0 = 26
= 0 1 0 0 1 = 9
Finally, the left shift operator x << k multiplies x by 2k . In particular, 1 << k is 2k , so
you can use this to check whether the kth bit of a number is 1 or 0. If x & (1 << k) is zero,
then the kth bit of x is 0, otherwise it is 1. Watch for overflow and use the long data type if
necessary.
1 << 5 = 1 · 25 = 32
7 << 2 = 7 · 22 = 28
Exercises
Calculate by converting the numbers to binary, applying the bit operations, and then
converting back to decimal numbers:
(a) 19 & 34 Answer: 2
(b) 14 | 29 Answer: 31
(c) 10 ∧ 19 Answer: 25
(d) 3 << 5 Answer: 96
CHAPTER 14. ADDITIONAL SILVER TOPICS 76
Generating Subsets
Occasionally in a problem we’ll want to iterate through every possible subset of a given
set, either to find a subset that satisfies some condition, or to find the number of subsets that
satisfy some condition. Also, some problems might ask you to find the number of partitions
of a set into 2 groups that satisfy a certain condition. In this case, we will iterate through all
possible subsets, and check each subset for validity (first adding the non-selected elements to
the second subset if necessary).
In a set of N elements, there are 2N possible subsets, because for each of the N elements,
there are two choices: either in the subset, or not in the subset. Subset problems usually
require a time complexity of O(N · 2N ), because each subset has an average of O(N ) elements.
Now, let’s look at how we can generate the subsets. We can represent subsets as binary
numbers from 0 to 2N − 1. If we zero-index our original array of elements, then the bit with
value 2k represents whether the element with index k is in the subset. If the 2k bit is 1, then
the element is included in the subset; if it’s 0, then it’s not included in the subset. Let’s look
at an example set of a, b, c.
Algorithm: The algorithm for generating all subsets of a given input array
Function generateSubsets
Input : An array arr, and its length n
for i ← 0 to 2n − 1 do
Declare list
for j = 0 to n-1 do
if the bit in the binary representation of i corresponding to 2j is 1 then
Add arr[j] to the list
end
end
Process the list
end
In the following code, our original set is represented by the array arr[] with length n.
int ans = 0;
for(int i = 0; i < (1<<n); i++){
CHAPTER 14. ADDITIONAL SILVER TOPICS 77
14.4 Ad-hoc
The silver division also often has ad hoc problems. They primarily rely on non-standard
algorithmic thinking and problem solving ability. You develop these skills by solving problems;
thus, we don’t have much content to teach you about ad hoc problems, but we provide a
selection of problems at the end of the chapter for your practice.
14.5 Problems
Two Pointers
1. CSES Problem Set Task 1640: Sum of Two Values
https://cses.fi/problemset/task/1640
Line Sweep
3. USACO US Open 2019 Silver Problem 2: Cow Steeplechase II
http://usaco.org/index.php?page=viewproblem2&cpid=943
CHAPTER 14. ADDITIONAL SILVER TOPICS 78
Subsets
4. (Subsets) CSES Problem Set Task 1623: Apple Division
https://cses.fi/problemset/task/1623
Ad hoc problems
5. USACO February 2016 Silver Problem 1: Circular Barn
http://usaco.org/index.php?page=viewproblem2&cpid=618
Problem Set
79
Chapter 15
Parting Shots
Set 1
1. https://codeforces.com/problemset/problem/1227/B
2. https://codeforces.com/problemset/problem/1196/B
3. https://codeforces.com/problemset/problem/1195/B
4. https://codeforces.com/problemset/problem/1294/B
5. https://codeforces.com/problemset/problem/1288/B
6. https://codeforces.com/problemset/problem/1293/A
7. https://codeforces.com/problemset/problem/1213/B
8. https://codeforces.com/problemset/problem/1207/B
9. https://codeforces.com/problemset/problem/1324/B
10. https://codeforces.com/problemset/problem/1327/A
Set 2
1. https://codeforces.com/problemset/problem/1182/B
2. https://codeforces.com/problemset/problem/1183/D
3. https://codeforces.com/problemset/problem/1183/C
4. https://codeforces.com/problemset/problem/1133/C
80
CHAPTER 15. PARTING SHOTS 81
5. https://codeforces.com/problemset/problem/1249/B2
6. https://codeforces.com/problemset/problem/1194/B
7. https://codeforces.com/problemset/problem/1271/C
8. https://codeforces.com/problemset/problem/1326/C
9. https://codeforces.com/problemset/problem/1294/C
10. https://codeforces.com/problemset/problem/1272/B
Set 3
1. https://codeforces.com/problemset/problem/1169/B
2. https://codeforces.com/problemset/problem/1102/D
3. https://codeforces.com/problemset/problem/978/F
4. https://codeforces.com/problemset/problem/1196/C
5. https://codeforces.com/problemset/problem/1154/D
6. https://codeforces.com/problemset/problem/1272/D
7. https://codeforces.com/problemset/problem/1304/C
8. https://codeforces.com/problemset/problem/1296/C
9. https://codeforces.com/contest/1263/problem/D
10. https://codeforces.com/contest/1339/problem/C
Set 4
1. https://codeforces.com/problemset/problem/1281/B
2. https://codeforces.com/problemset/problem/1196/D2
3. https://codeforces.com/problemset/problem/1165/D
4. https://codeforces.com/problemset/problem/1238/C
5. https://codeforces.com/problemset/problem/1234/D
6. https://codeforces.com/problemset/problem/1198/B
7. https://codeforces.com/problemset/problem/1198/A
8. https://codeforces.com/problemset/problem/1077/D
9. https://codeforces.com/problemset/problem/1303/C
10. https://codeforces.com/problemset/problem/1098/A
CHAPTER 15. PARTING SHOTS 82
Set 5
1. https://codeforces.com/problemset/problem/1185/D
2. https://codeforces.com/problemset/problem/1195/D2
3. https://codeforces.com/problemset/problem/1154/E
4. https://codeforces.com/contest/1195/problem/C
5. https://codeforces.com/problemset/problem/1196/E
6. https://codeforces.com/problemset/problem/1328/D
7. https://codeforces.com/problemset/problem/1253/D
8. https://codeforces.com/problemset/problem/1157/E
9. https://codeforces.com/problemset/problem/1185/C2
10. https://codeforces.com/problemset/problem/1209/D