Dynamic Programming
Dynamic Programming
A RPAN B ANERJEE
2021
”Those who cannot remember the past are condemned to repeat it.”
— George Santayana
Prerequisites
It is recommended that the reader knows most concepts that appear on the Bronze and Silver
divisions of USACO. Namely, including but not limited to:
• Familiarity with competitive programming (i.e. what it is/how it works)
• Prefix sums
• Recursion
• Coordinate compression
• Custom comparators
• Binary search
• Two pointers
• Greedy algorithms
A Few Notes
There are problems at the end of each chapter, most of which are from USACO. They are
arranged in roughly ascending order of difficulty. It is recommended that you do a few problems
that are hard for you, but not to the extent that you make no progress after significant effort
(i.e., problems that are reasonably above your comfort zone). All code provided is compatible
with C++11. More advanced techniques, optimizations, and additions might be added here
later (early 2022). For now, this can be considered an introduction.
Contact
I am open to any corrections, suggestions, or questions. If you wish to contact me, you can
email arnbnrch@gmail.com.
3
Author’s Profile
Arpan Banerjee is currently a USACO Platinum competitor and a high school senior in NC,
USA.
Contents
I Dynamic Programming 7
1 Introduction 8
1.1 Overlapping Subproblems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.1 Fibonacci . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.2 Optimal Substructure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 Maximum Sum Subarray . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.4 DAG Formulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.5 General Strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2 Counting 13
2.1 Modular Arithmetic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1.1 Rules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.1.2 Modular Multiplicative Inverse . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1.3 With Fermat’s Little Theorem . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1.4 Binary Exponentiation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.1.5 With the Extended Euclidean Algorithm . . . . . . . . . . . . . . . . . . . 16
2.2 Binomial Coefficients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.1 With Pascal’s Identity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.2.2 With Modular Inverse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3 Knapsack 19
3.1 Fractional Knapsack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.2 Coin Change . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.3 When Greedy Fails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.4 0-1 Knapsack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.5 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4 Trees 23
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.2 Motivating Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3 Rerooting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.4 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5 Digits 30
5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
5.2 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
6 Bitmasking 37
6.1 Assignment Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.2 Representing Subsets With Bitmasks . . . . . . . . . . . . . . . . . . . . . . . . . . 37
6.3 Full Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
6.4 Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
7 Ranges 40
5
II Parting Shots 42
8 Problems 43
9 Resources 45
I
Dynamic Programming
7
1 Introduction
The essence of dynamic programming is breaking down a problem into subproblems and
attaining and combining the answers for them in such a way that the answer for the original
problem can be attained efficiently. The solutions to the subproblems are used to attain the
solutions to other subproblems through transitions. The reason for the improved efficiency of
dynamic programming (DP hereafter) is that redundant computations are avoided by keeping
states which contain only the necessary data, usually such that they can be visited exactly
once. In the scope of competitive programming, DP problems usually involve optimizing some
quantity (i.e. minimizing or maximizing something) or counting some quantity (i.e. the number
of ways to do something). Usually DP reduces the naive exponential solution to polynomial
time. There are two properties a problem must exhibit to be able to be solved with DP:
§1.1.1 Fibonacci
Let’s consider the Fibonacci series: 1, 1, 2, 3, 5, 8, . . . . We can define it recursively as:
(
f ( x − 1) + f ( x − 2) x ≥ 3
f (x) =
1 x≤2
f (5)
f (4) f (3)
f (2) f (1)
It can be easily noticed that f (3) is computed twice. It turns out that a lot of redundant
recomputations happen if f ( x ) is computed for some large x. Each f ( x ) calls two children.
8
Each of those children call two children, and so forth. The children are only called for x − 1 and
x − 2, so the values in the nodes decrease by a small constant (1 or 2). This means this approach
takes roughly O(2x ) time to find f ( x ). A tighter bound is O(Φ x ), but the main takeaway here is
that it’s exponential due to the same subproblems being computed multiple times.
However, if we use memoization, it will take O( x ) time at the expense of O( x ) memory. This
is because each state (subproblem) is visited at most once:
Remark 1.2.1. Note that greedy solutions also demonstrate optimal substructure, but what dif-
ferentiates a greedy approach from a DP one is that greedy does not use optimal solutions to
subproblems to make a choice, but rather it first makes a greedy choice which results in optimal
substructure.
Interestingly, this algorithm has a name: Kadane’s Algorithm. Since we are pulling from i − 1
when we are at i, this type of DP is sometimes called pull DP. A push DP approach, pushing
from index i to i + 1, is as follows:
Algorithm
Precondition : a and DP are 1-indexed
array DP ← 0
for i ← 0 to n − 1 do
DP[i + 1] ← max( DP[i ] + a[i + 1], a[i + 1])
end
return max ( DP)
Notice that since the DP only requires data from an adjacent index while the indices are
iterated upon, the space complexity can be reduced from O(n) to O(1) by using a few variables
instead of an array. Both of these approaches can be classified as bottom-up dynamic program-
ming since we are starting at the smallest subproblems and building up to the answer for the
entire array.
Every DP can be formulated as a DAG—a directed acyclic graph. The nodes of the DAG are the
subproblems and the edges are the transitions between the subproblems. Consequently, the
best path in the DAG is the path the DP takes. The reason its a directed acyclic graph is that if
there are cycles, some states would lead back to themselves after a series of transitions (edges)
with nowhere to stop at. Consider the following array:
-1 -2 3 1 -3 6 -1
The answer is highlighted in blue. Below is the DAG of the pull DP version with the optimal
path highlighted in red.
-1 -2 3 1 -3 6 -1
start
1 1 1 1 1 1 1
0 0 0 0 0 0 0
end
Nodes with 1 represent DP[i ] and nodes with 0 represent not including that element in the array.
Since DP[i ] is the max of DP[i − 1] + a[i ] and a[i ], the edges (transitions) are converted into
edges accordingly. From the 1 node, going to the previous 1 node corresponds to DP[i − 1] + a[i ],
and going to the previous 0 node corresponds to a[i ].
2. Determine the base cases: These are the initial state(s) that all other states depend on.
3. Make the transitions: The transitions enable you to transition from one state to another.
After all transitions are complete, the answer is usually attained trivially.
1 ≤ N, M ≤ 5000
Solution. To take care of the condition of having no more than M flowers of the same type
consecutively, the type of flower can be incorporated into the DP state: DP[i ][(0, 1)] =number
of configurations if
i : first i flowers placed
DP[0][0] = DP[0][1] = 1
Then, it can be filled from DP[i ][0] as follows:
The reasoning for this is that if flower i is placed at position p, you can add 1 . . . M flowers of
the opposite type in positions p + 1 . . . p + M while maintaining the constraint of not having
more than M contiguous flowers of the same type. By storing the number of configurations of
the prefix 1 . . . p with flower i placed at position p, we can easily update the next M positions of
the opposite flower. This transition can be repeated for all i between 0 and N and done similarly
from DP[i ][1]. This is colloquially called push DP since each state pushes into the next states.
Then the answer is DP[ N ][0] + DP[ N ][1]. Since we loop at most M times for at most 2N states,
this approach takes O( N M ) time.
2 Counting
Many problems ask for the number of ways some conditions can be fulfilled under some
constraints. Oftentimes, the problem asks for the answer to be outputted modulo a large number
(usually 1000000007 or 998244353), since the answer could be too large to fit into a 64 bit integer.
It is no coincidence that these two numbers are prime.
a≡r mod d
§2.1.1 Rules
Below are some provable identities:
The main takeaway is that you can do basic algebra under a modulus by modding individual
parts. Here is an easy problem to demonstrate this in practice:
Solution. Consider the possible states of each row. For each row r, we can keep track of the
number of ways to populate the 2 × r grid, and increase r until we reach 2 × N, giving the
required answer. Notice that at each row, we can either choose to join the left and right cells, or
leave them disconnected. This motivates us to keep two counts, one for the number of ways to
populate the 2 × r grid if we join the left and right cells of row r, and another one for if we keep
them separate. Then
13
join(r ) = join(r − 1) + sep(r − 1) + join(r − 1)
| {z } | {z }
Continue all configurations joined at r-1 Make new joined block
If both rows r − 1 and r are separate, there are four cases for the two cells in row r:
Then the answer is sep( N − 1) + join( N − 1) if we 0-index. The base cases are join[0] = 1
and sep[0] = 1. join and sep are initialized to 0 since nothing can be built initially. Below is an
implementation of this with the answer being taken modulo mod = 109 + 7.
does, according to the multiplication rule. There are two main ways to find the modular multi-
plicative inverse: Fermat’s Little Theorem and the Extended Euclidean Algorithm. Moreover,
there is an infinite number of modular multiplicative inverses of a under modulo p; they just
have to be part of the same equivalence class, but usually the smallest one is found.
Remark 2.1.3. This result only holds for a mod p 6= 0 since it’s impossible for 0 · a−1 to be congru-
ent to 1 mod p.
Problem 2.1.2
Prove that if a · a−1 ≡ 1 mod m, gcd ( a, m) must be 1.
a · a−1 = km + 1 =⇒ a · a−1 − km = 1
a−1 − km
gcd ( a, m) =1
gcd ( a, m)
Remark 2.1.4. This concept can also be extended to matrices. When applied to matrices, it is called
Matrix Exponentiation.
a · a −1 ≡ 1 mod m =⇒ a · a−1 − mk = 1
a · a−1 + mk = 1
ax + by = 1
This is called a linear Diophantine equation, and one solution can be found with the Ex-
tended Euclidean Algorithm. Note that we are trying to solve for ( x, y). The existence of
solutions is guaranteed by Bézout’s Lemma as well as the fact that this is a reduced version
of our original problem of finding a modular inverse which must exist. With the Extended
Euclidean Algorithm, we find x, y such that ax + by = gcd ( a, b). In our case, gcd ( a, b) = 1.
The answer, a−1 , is the value of x.
The algorithm is just repeatedly using this identity until a trivial state is reached:
The Extended Euclidean algorithm is a small extension to the Euclidean algorithm enabling
us to find a solution to ax + by = gcd ( a, b) = 1. Here is an example demonstrating the
extension:
Problem 2.1.3
Find the modular multiplicative inverse of 27 mod 100.
27x + 100y = 1
Let’s first use the Euclidean algorithm to find gcd (27, 100):
100 = 27 · 3 + 19 gcd (100, 27) = gcd (27, 19)
27 = 19 · 1 + 8 gcd (27, 19) = gcd (19, 8)
19 = 8 · 2 + 3 gcd (19, 8) = gcd (8, 3)
8 = 3·2+2 gcd (8, 3) = gcd (3, 2)
3 = 2·1+1 gcd (3, 2) = gcd (2, 1)
Notice that it’s guaranteed that the last residue is 1 because of the precondition gcd ( a, b) = 1,
or in this case, gcd (27, 100) = 1. Rearranging:
100 − 27 · 3 = 19
27 − 19 · 1 = 8
19 − 8 · 2 = 3
8−3·2 = 2
3−2·1 = 1
We can treat the bold numbers as variables. The first equation resembles our original Diophan-
tine equation with the exception of the right side, so let’s change that with some substitutions
to get 19 in terms of 1. Starting at the bottom, we can see that if we treat the bold numbers as
variables and substitute the bold numbers from the right side of the above equation into the left
side of the current equation and collect like terms, the intermediary bold numbers disappear,
leaving only 1 and the highest bold numbers, as desired. Doing this results in
27(−37) + 100(10) = 1
Most often, you will need to compute a binomial coefficient under a modulo m.
However, computing (nk) takes O(n2 ) time, which is infeasible if n is a large number.
n!
≡ n! · k!−1 · (n − k)!−1 mod m
k!(n − k )!
n! and k! can be found easily by precomputing factorials mod m.
§2.3 Problems
1. CF 1228E
The standard knapsack problems rarely (if ever) directly appear in contests. Instead, they
appear with variations and twists or in the guise of a different idea. Below are two of the most
traditional knapsack problems:
Sample Input
W=3
(w, v):
(1, 1)
(3, 2)
Sample Output
2.3333333 (take the first item and 2/3 of the second item)
Solution. We can greedily choose the items with largest v/w ratio until the entire item has been
taken. In that case, we choose the next highest v/w ratio and do the same procedure. This takes
O(n log n) time because of sorting the items.
Solution. Consider the following constraint: 1 ∈ C, and if the elements C are in non-decreasing
order c1 ≤ c2 · · · ≤ cn , then for all i, ci |ci+1 . In this case, we can do a similar greedy approach
as we did with fractional knapsack: deduct k by the maximum value in C until k is less than
it, in which case repeat the process until k = 0. A proof that this works is that for each larger
19
denomination, all smaller denominations divide it. Therefore, a mapping can always be made
from a larger denomination to a set of smaller ones, ensuring there is no better way to deduct
from k than by greedily choosing the largest elements in C. It turns out that this greedy approach
only works for canonical coin systems, of which the aforementioned constraint is a subset of.
However, if this was not the case, the greedy approach would not necessarily work. Consider
C = {1, 4, 5, 6} and k = 9, for instance.
It turns out that this is the widely accepted DP solution to this problem. Some variations
include:
2. All possible sums producible with a set of coins (modification of the subset sum problem)
Sample Input
W=3
(w, v):
(1, 1)
(3, 2)
Sample Output
2 (take item 2)
Solution. If we use the same greedy strategy as we did for fractional knapsack, we will take the
first item, but since there isn’t enough space for the second item, our answer is 1. This is clearly
not optimal, as we can take the second item instead. But how do we find the optimal subset of
items?
Remember, the goal is to find the maximum value of at most n items that we put into a knapsack
of weight capacity W. This motivates us to keep a DP to keep track of the maximum value for
each state, with a state being defined by (x: first x items, y: knapsack weight capacity y ≤ W).
Then if w = weights[i ] and v = values[i ], our transitions are
(
max(v, DP[i − 1][ j − w] + v, DP[i − 1][ j]) if j ≥ w,
DP[i ][ j] =
DP[i − 1][ j] otherwise.
and the full algorithm is:
This can be optimized further to O(n) space complexity rather than O(nW ) by realizing that
the DP only pulls from one row before; hence, rows 1 through i − 2 can simply be trashed when
going through row i:
1 // dp size is [2][W+1]
2 for (int i = 1; i <= n; i++){
3 int v = values[i], w = weights[i];
4 for (int j = 0; j < w; j++) dp[1][j] = dp[0][j]; // copy row 0 to row 1
5 for (int j = w; j <= W; j++){
6 dp[1][j] = max({ v, dp[0][j], dp[0][j - w] + v });
7 }
8 for (int j = 0; j <= W; j++){
9 dp[0][j] = dp[1][j];
10 dp[1][j] = 0;
11 }
12 }
13 cout<<dp[0][c]<<endl;
Another less standard way of doing it is using a one dimensional DP table with DP[i ] being
the maximum value sum at a weight of i. In the implementation below, values, weights, and DP
are 0-indexed:
1 for(int i=0; i<n; i++){
2 int v=values[i], w=weights[i];
3 for(int j=W; j>=0; j--){
4 if(j+w>W) continue;
5 if(dp[j]!=0 || j==0){
6 dp[j+w]=max(dp[j+w], dp[j]+v);
7 }
8 }
9 }
§3.5 Problems
1. USACO Gold Fruit Feast
5. CF 1132E
Remark. Consider why doing a knapsack for the minimum weight required for each talent sum
does not work for Talent Show. This solution passes all the test cases on USACO, but stresstesting
should make the mistake clear.
4 Trees
§4.1 Introduction
Conventionally, let n be the number of nodes in the tree. DP on trees is usually done to optimize
the naive solution—usually O(n2 )—to a smaller order, such as O(n). Most often:
• The tree is rooted (either by nature of the problem or done intentionally by the solver)
• The subproblems are built from the leaves to the root via a BFS/DFS traversal
Problem 4.1.1
Find the size of the subtrees for all n nodes in a tree in O(n) time.
Solution. Keep an array size to keep track of the sizes of each subtree. size[i] is the size of
the subtree rooted at i. Initialize size to 1 since that is the minimum possible size of a subtree.
Then after the DFS covers all subtrees of a node i, add the size values of i’s children to size[i].
Algorithm
array size[number of nodes] ← 1
Function DFS(i, parent):
foreach j ∈ adjacent[node] do
if j 6= parent then
DFS(j, node)
size[i ] ← size[i ] + size[ j]
end
end
End Function
There is also another way to do this. Keep a global variable time. Increment it whenever an
instance of the DFS function is called. Then for each node, its subtree size is time − entry time.
23
Algorithm With timestamps
time ← 0
array size[number of nodes]
Function DFS(i, parent):
entry time ← time
time ← time + 1
foreach j ∈ adjacent[node] do
if j 6= parent then
DFS(j, node)
end
end
size[i ] ← time − entry time
End Function
Problem 4.1.2
Find the diameter (longest path) in a tree in O(n) time.
Solution. This is very similar to the previous problem. Instead of keeping track of the subtree
size for all children of a node, keep track of the farthest distance from a leaf. The answer for a
node is simply the sum of the two maximum farthest distances of it’s children.
Problem 4.2.1
n bees numbered 1 . . . n are spreading word of a flower. Bee 1 is the one that first located
the flower and initially the only bee with the information. n − 1 pairs of bees are friends,
and bees only talk to friends. It is possible for all bees to hear the news. Each minute, a
bee with the news can transmit it to one friend. Compute the minimum time for all bees to
receive the news.
Sample Input
6
1 3
3 4
1 2
2 5
3 6
Sample Output
3
Solution. Without loss of generality, let’s assume the tree is rooted at 1. Below is an optimal way
the bees receive the news, from time 0 to time 3 inclusive.
1
2 3
5 6 4
2 3
5 6 4
2 3
5 6 4
2 3
5 6 4
Intuitively, it makes sense to send the message to the larger subtrees earlier than the smaller
ones. This is how the message spreads in the above diagram as well: it goes to 3 before 1. So,
we may think that larger subtrees always require more time to spread the message than smaller
subtrees. However, this is not always true. Consider the following two trees:
1
2 3
5 6 4
The second one is larger but takes less time for the message to fully spread. So, instead of
looking at subtree sizes, we can look at the answer for each subtree. We can prioritize subtrees
that take longer for the message to spread over those that take a shorter amount of time by
keeping track of the amount of time required to spread the message across each subtree using
DP. The algorithm below should make this clear.
Algorithm
ans ← 0
array times[number of nodes] ← 0
Function DFS(node, parent):
list children
foreach i ∈ adjacent[node] do
if i 6= parent then
DFS(i, node)
children.push back(times[i ])
end
end
sort(children, greatest to least)
for i = 0 to size(children)-1 do
times[node] = max (times[node], children[i ] + i + 1)
end
End Function
DFS(1, 1)
return times[1]
Problem 4.2.2 (CSES Tree Matching)
You are given a tree consisting of n nodes. A matching is a set of edges where each node is
an endpoint of at most one edge. What is the maximum number of edges in a matching?
For example, the left tree is an optimal matching, but the right one is not:
1 1
3 2 3 2
4 5 4 5
Solution. The most obvious thing to do here is have two states for each node i:
For each state, the maximum number of pairings in the subtree with root i can be stored. It can
also be noticed that in the frame of some subtree, a matching with the root of that subtree in a
pair always results in the optimal answer. The proof of this and the rest of the solution are left
as an exercise for the reader.
§4.3 Rerooting
Rerooting is a technique where information from the root is pulled (usually in sublinear time) to
an adjacent node in order to attain the desired information for the adjacent node. The adjacent
node is then treated as the new root, and the process continues until all the nodes in the tree
have acquired their desired quantity. From there, completing the problem becomes trivial.
Below is an example that nicely illustrates this concept.
Problem 4.3.1
There is a weighted tree with n nodes 1 . . . n and n − 1 edges. What is the expected value
of the sum of a randomly chosen path.
Solution. For each node i, let’s consider all the paths with one endpoint at i. Call the sum of the
weights of these paths sumi . Then the answer is
∑in=1 sumi
E[path sum] =
2
Furthermore, let’s start at a node that we call s. Then sums can be computed in O(n) time with
a BFS/DFS. Now, we need to extract sumi ∀i 6= s in linear time. This can also be done with a
BFS/DFS. Consider the following tree:
s
2 5
3 4 6
7 8
Without loss of generality, assume the traversal initially goes from node s to 5. Then we need to
find sum5 from sums .
2 5
3 4 6
7 8
Notice that compared to sums , the highlighted edge is added in all of the paths from 5 that end
in a node that is not in the subtree of 5. That edge is also removed in all of the paths from 5 to
another node in the subtree of 5. Let the subtree size of i be denoted by sizei . Then the transition
between adjacent nodes i → j can be written as follows:
Solution. There is also another way of doing this with linearity of expectation. Let’s call the
number of different paths where an edge i appears f i . The contribution of each edge to the
expected value is proportional to its f i . Then the answer is
∑in=1 f i
E[path sum] =
n−1
f i can easily be found by (pre)computing the subtree sizes for each node and doing a traversal
of the tree.
Remark 4.3.1. The ideas mentioned here can be extended to find the
• average length of a path
• sum of all path lengths
• number of paths that go through an edge, for all edges
• the sum of the distances from a node to all other nodes, for each node
for a weighted or unweighted tree in linear time.
2. CF 1187E
3. CF 1092F
§4.4 Problems
All of these except the last two should be done in subquadratic time:
3. Find the minimum and maximum distance from each node to a leaf
4. ICPC Brazil 2019-2020 Denouncing Mafia (not exactly DP, but very nice nonetheless)
6. Find the sum of XORs of all paths in a tree (hint: consider the contribution of each bit)
7. CF 1528E
8. AtCoder ABC207F
Remark 4.4.1. A non-DP way of computing the minimum distance from each node to a leaf is doing
a multisource BFS from the leaves. This gives the minimum distance to each node from the leaves,
which is equivalent to the original problem. The maximum distance to a leaf from each node can
also be found without DP by doing a BFS from each endpoint of a diameter. The answer for each
node is the maximum distance of the two BFSs for the node.
5 Digits
This topic is not extremely common in competitive programming; however, it is still good
to know. Digit DP can be used to solve problems that ask for how many integers in a range
have some property. The crux of it lies in the fact that the necessary information for each state
does not necessarily have to be an entire integer, but rather some information pertaining to
digits. Then since the full information of an integer is not in our state, it turns out that multiple
integers can be represented by a single state, which obviously reduces the time complexity. This
concept is best explained with an example:
§5.1 Introduction
Problem 5.1.1
There are two integers n, m where 0 ≤ n ≤ m ≤ 1018 . Find the number of integers between
n and m inclusive containing exactly k d digits. n, m, k, d are given in input.
Sample Input
(n, m, k, d) = (7, 111, 2, 1)
This translates to finding the number of integers in the range [7, 111] with exactly two 1
digits. There are three such integers: 11, 101, 110.
Solution. Notice that the answer ans[n, m] = ans[0, m] − ans[0, n − 1]. Now the problem is
reduced to solving for the range 0 to c for some c. The naive solution is to iterate through all
integers between 0 and c which is at worst O(1018 ). Here, the state is defined by an integer
[0, 1018 ]. Call an integer ≤ c with exactly k d digits valid. Naively, if we consider iterating
through the digits instead of the integers, the state is the current digit and all digits already
placed. This is also too expensive, so we are motivated to look for a state without the number
we are building, but rather with a small amount of data from it.
We can instead build the digits of the number left to right, with the state being defined
by the position from the left, the number of times k has been placed, and a boolean value
indicating whether the number built cannot possibly exceed k. For example, if the number
built is 122d4 d5 d6 and k is 123222, we know that regardless of what d4 , d5 , d6 are, the number
will always be less than k . . . the boolean value is true in this case, otherwise it is false. This
technique is very common in digit DP solutions.
Then we can let each state dp[pos][# times k has been placed before pos][bool] be
the number of valid integers ≤ c from that state, or in other words, the number of valid integers
that satisfy the conditions of that state, ignoring all previous states. So, the answer should be
dp[0][0][0].
To properly execute the transitions between the states, a DFS can be done. Let d0 d1 d2 be the
digits of 111 (c) and di be any digit not equal to di of c; if the boolean value is false, assume that
di < ci . Furthermore, let ? denote the digit we are currently choosing. Let’s see the recursion
tree for the aforementioned sample. The base cases are the nodes in the bottommost layer.
30
dfs(0, 0, 0)
?
dfs(1, 0, 1) dfs(1, 1, 0)
d0 ? d0 ?
A quick look at the tree reveals that some states are repeated. Even if the numbers formed so far
in the repeated nodes may be different, they have the same state, and are hence equivalent to
each other:
dfs(0, 0, 0)
?
dfs(1, 0, 1) dfs(1, 1, 0)
d0 ? d0 ?
To avoid recomputing them, we can use memoization. This can be thought of as the visited
array in a normal DFS. Memoizing the values of the DFS in a DP table would ensure that no
state is visited twice. This means the time (and space) complexity is simply the number of states
in the DP table: O(max num digits2 ). The code below should make this clear:
1 #include <bits/stdc++.h>
2 using namespace std;
3 #define int long long
4
9 void reset(){
10 for (int i = 0; i < max_num_digits; i++){
11 for (int j = 0; j < max_num_digits; j++){
12 dp[i][j][0] = -1;
13 dp[i][j][1] = -1;
14 }
15 }
16 }
17
44 signed main(){
45 reset();
46 cin >> n >> m >> k >> d;
47 int total = dfs(m);
48 reset();
49 cout << total - dfs(n - 1) << endl;
50 }
Remark 5.1.1. n and m can be made into strings so that large numbers that would normally overflow
64 bit integers can be used. The only drawback to this is that n − 1 would need to be calculated to
call f (n − 1). Alternatively, you can use a custom bignum object.
Remark 5.1.2. Note that for something like (n, m, k, d) = (0, 10, 1, 0), this code will output 9 since it
considers 01, 02, 03 . . . 09 to be valid. If valid numbers are not allowed to have leading zeros in their
digit = 0 count, some casework can be done to eliminate those numbers. One way is replacing int
xth digit=0 with int xth digit=(x==0 && d==0).
Here is a slightly easier problem that you can try for yourself:
Problem 5.1.3. For some a, b ≤ 1015 , find the sum of the digits of all integers in the range [ a, b]
(submit)
1 ≤ Q ≤ 104
Solution
Remark 5.1.4. There are other solutions to this problem (see the official solution), but here, we will
specifically focus on the digit DP approach.
jxk jyk
and in ternary are equivalent to shifting right by k. This means when converted
3k 3k
to ternary, the parity of all trits of x and y must be the same if there is a cow at ( x, y); call this
valid. Also note that moving across diagonally means adding the same amount to x and y.
This means we can focus specifically on x. From now on, assume x and y refer to their ternary
representations.
We want to count the number of x + ∆ and y + ∆ such that ∆ ≤ d and the parity of all trits of
x + ∆ and y + ∆ are the same. Since we are working in ternary, we are motivated to go from
the least to most significant trits. Of course, our state must include the position pos as one
of its attributes. Similar to the previous problem, a state stores the number of valid numbers
satisfying the constraints of only that state in particular, ignoring the previous states and only
focusing on the future. If we are at some state, we can ensure that the previous trits are all valid,
that is, the parity of all less-significant trits are the same.
We can also incorporate a flag into our definition of a state. The flag, call it leq, denotes
whether the suffix of the number x + ∆ we are building, from position pos to the end of x + ∆, is
less than or equal to the corresponding suffix of x + d. This necessitates some simple casework.
Call the trit candidate we are placing at pos ”q”:
1. leqnew = true when q < ( x + d) pos
2. leqnew = true when q = ( x + d) pos and leqold = true
3. Otherwise, leqnew = f alse
Remark 5.1.5. Consider why keeping leq for x is equivalent to keeping it for y or for both x and y.
There is one more factor we need to consider. Currently, our state is defined by leq and pos. But
how do we go from one state to the next? We add some ∆ to the trit at position pos of both x
and y that we are building. This might result in trits ≥ 3. So we need to keep track of carries on
both of the numbers we are building as well.
Remark 5.1.6. Consider why it’s never beneficial to increase the trit at some pos by 3 or greater.
If x < y, swap x, y. This will give the same answer because of symmetry. The reason this is
necessary is to know what the highest set trit is, i.e., where to stop. Our base case is defined
by the most significant trit in x. If leq is true and there are no carries, the value of the base is 1.
Otherwise, it is 0. The implementation should make everything more clear. Here are the utility
functions:
1 namespace util{
2 // returns dec->ternary conversion with 0's padded in front to make
3 // the length 40
4 vector<int> dec_to_ternary(int d){ // approx O(1000)
5 string ans = string(40, '0');
6 while (d != 0){
7 int i = 1, pow = 0;
8 while (i * 3 <= d) i *= 3, pow++;
9 if (i * 2 <= d) i *= 2, ans[40 - pow - 1] = '2';
10 else ans[40 - pow - 1] = '1';
11 d -= i;
12 }
13 vector<int> v;
14 for (char c : ans) v.push_back(c - '0');
15 return v;
16 }
17 int carry(int val, int& add){
18 if (val + add < 3){
19 int addcpy = add; add = 0;
20 return val + addcpy;
21 }
22 val = (val + add) % 3; add = 1;
23 return val;
24 }
25 void reset(){
26 for (int i = 0; i < 40; i++)
27 for (int j = 0; j < 3; j++)
28 for (int k = 0; k < 3; k++)
29 for (int l = 0; l < 2; l++)
30 dp[i][j][k][l] = -1;
31 }
32 }
33 using namespace util;
6 int dfs(int pos, int carryX, int carryY, bool leq){ // approx O(1000)
7 int cx = carryX, cy = carryY;
8 int& state = dp[pos][carryX][carryY][leq];
9 if (state != -1) return state; // memoize to ensure each state is done
10 // at most once
11 state = 0; // going to set it permanently now
12 if (pos == highest_trit - 1){ // base case
13 if(carryX==0) return(state = leq);
14 return state;
15 }
16 int xpos = carry(xvec[pos], carryX);
17 int ypos = carry(yvec[pos], carryY);
18 array<int, 2> carries = { carryX, carryY };
19 for (int add : {0, 1, 2}){ // setting trits at pos of the (x, y) we are
20 // building
21 carryX = carries[0]; carryY = carries[1];
22 int addX = add;
23 int newxpos = carry(xpos, addX); // new val of x[pos]
24 int addY = add;
25 int newypos = carry(ypos, addY); // new val of y[pos]
26 if (newxpos % 2 != newypos % 2) continue; // if not same parity,
27 // ignore and move on
28 carryX += addX, carryY += addY;
29 state += dfs(pos - 1, carryX, carryY,
30 (newxpos < xlim[pos] || (newxpos == xlim[pos] && leq)));
31 }
32 return state;
33 }
34
35 signed main(){
36 int t; cin >> t;
37 while (t--){
38 reset(); // fill dp with -1
39 int x, y, d; cin >> d >> x >> y;
40 if (x < y) swap(x, y); // symmetric, so same result. needed for
41 // highest_trit
42 xlim = dec_to_ternary(x + d); ylim = dec_to_ternary(y + d);
43 xvec = dec_to_ternary(x); yvec = dec_to_ternary(y);
44 for (int i = 0; i < xlim.size(); i++){
45 if (xlim[i]){
46 highest_trit = i; break;
47 }
48 }
49 cout << dfs(39, 0, 0, 1) << endl;
50 }
51 }
The implementation can be simplified slightly by using a prefix sum-styled DP as was done
with the previous problem. Assuming x ≥ y, this would ensure there will be no carries on the y
being built. If we denote f ( x, d) to be the answer for the original query of ( x, y, d) = ( x, 0, d),
then the answer for a query ( x, y, d) is f ( x − y, y + d) − f ( x − y, y − 1):
( x − y, 0) x
( x, y)
y ( x + d, y + d)
§5.2 Problems
1. CF 1560F2
2. CF 1036C
3. CodeChef DIGIMU
4. CF 1327E
5. Count the number of integers in the range [1, n] such that each digit 0 . . . 9 appears at least
once
6. CF 1073E
The naive solution is to try all n! assignments and pick the best one. This obviously takes
O(n!) time, which is not good. How can we do better? We can notice that for some subset of
size k of taxis that are filled with the first k people, we don’t care about how it’s filled, but rather
the minimum cost of it being filled. This makes sense when considering what we’re looking
for: the minimum cost of filling the size n subset. More specifically, we don’t care about how
the people are permuted in a taxi subset consisting of the first k people. This already hints at
reducing the time complexity from factorial to exponential.
000 = {}
001 = {0}
010 = {1}
011 = {0, 1}
100 = {2}
101 = {0, 2}
110 = {1, 2}
111 = {0, 1, 2}
37
§6.3 Full Solution
We can keep an n bit number where each bit denotes whether the corresponding taxi has been
taken or not. Our state is defined by the n bit number. If k of the n bits are 1, that means the first
k people have been assigned taxis corresponding to the k set bits.
Consider State k=0: 000 → State k=1: 100 → State k=2: 101. This represents per-
son 1 assigned taxi 1 and person 2 assigned taxi 3. However, we can also attain State k=2: 101
through State k=0: 000 → State k=1: 001 → State k=2: 101. This directly shows
how permutations are reduced to subsets.
Notice that you only need to iterate over the numbers 1 to n and everything happens auto-
matically:
7
111
6
110
5
101
4
100
3
011
2
010
1
001
This is because the arrows are all pointed in the same direction, enabling optimal substructure.
Proving this is always the case is left as an exercise for the reader.
§6.4 Problems
1. CSES Hamiltonian Flights
2. CF 16E
4. CF 11D
5. CF 895C
6. COCI Burza
7 Ranges
We can use DP on ranges to solve the following problem.
Solution. Let’s create a two dimensional DP with the state being defined by the minimum
number of strokes to copy a specific range. Then we can iterate over ranges by their size and
get the answer for a state by looking at states corresponding to subranges in the current range.
Notice that each range can be broken into two ranges, and it is never better to break a range
into more than two ranges. The implementation should make this clear.
1 #include <bits/stdc++.h>
2 using namespace std;
3
10 signed main(){
11 cin>>n;
12 for(int i=1; i<=n; i++) cin>>p[i];
13 for(int i=0; i<=n; i++){
14 for(int j=0; j<=n; j++){
15 if(i==j) dp[i][j]=1;
16 else dp[i][j]=sz;
17 }
18 if(i!=n) dp[i][i+1]=1+(p[i]!=p[i+1]);
19 }
20
40
24 for(int point=pos+1; point<n; point++){ // inside the gap
25 dp[pos][pos+gap]=min(dp[pos][pos+gap],
26 dp[pos][point]+dp[point][pos+gap]-1);
27 }
28 }
29 }
30 cout<<dp[1][n]<<endl;
31 }
42
8 Problems
It is equally important to know DP topics as it is to recognize when and how to apply them.
The best way to get better at DP problems is by solving them. Below, I leave you with a variety
of carefully chosen, high quality problems arranged in three sets of ten. The problems in each
of the sets are in approximate ascending order of difficulty. Most of these problems are from
Codeforces, but there are also a few nice problems from other sources.
Set 1
1. USACO Gold Hoof, Paper, Scissors
2. CF 118D
3. CF 101B
4. CF 1133E
7. CF 1551F
8. CF 1327F
10. CF 449D
Set 2
1. CF 1535C
2. CF 1538C
4. CF 69D
5. CF 1472G
6. CF 1067A
7. CF 1426F
10. CF 1188C
43
Set 3
1. CF 1534C
2. CF 1513C
3. CF 1525D
4. CF 118D
6. CF 1552F
7. CF 123C
9. CF 1541E1
10. CF 1540C2
9 Resources
Here, I leave some useful resources for you:
1. CP Algorithms
2. Codeforces DP Problems
3. USACO Guide
4. Codeforces DP Blogs
45