10 DP 2
10 DP 2
Bundle 10
Dynamic programming - 2
Editor
Salih Furkan Ceyhan
Reviewer
Caner Demirer
1
Contents
1 Bitmask DP 3
1.1 What is Bitmask? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Bitmask operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3 Iterating over Subsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 Task Assignment Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 DP on Rooted Trees 5
2.1 Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4 Digit DP 9
4.1 How to Work on Digits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4.2 Counting Problem Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2
1 Bitmask DP
Let’s say that we have a set of objects. How can we represent a subset of this set? One way is using a
map and mapping each object with a Boolean value indicating whether the object is picked. Another
way is if the objects can be indexed by integers, we can use a Boolean array. However, this can be
slow due to the operations of the map and array structures. If the size of the set is not too large (less
than 64), a bitmask is much more useful and convenient.
An integer is a sequence of bits. Thus, we can use integers to represent a small set of Boolean values.
We can do all the set operations using the bit operations. The bit operations are faster than the
map and array operations. Therefore, the bit operations takes less time. In some problems, the time
difference may be significant.
In bitmask the ith bit from the right represents the ith object. For example, let A = {1, 2, 3, 4, 5}, we
can represent B = {1, 2, 4} with the 11 (01011) bitmask.
3
1.3 Iterating over Subsets
There are N people and N tasks and each task is going to be allocated to a single person. We are
also given a matrix cost of size N × N , where cost[i][j] denotes how much a person is going to charge
for a task. Now we need to assign each task to a person in such a way that the total cost is minimum.
Note that each task is to be allocated to a single person, and each person will be allocated only one
task.
DP Approach: For every possible subset find new subsets can be generated from this subset and
update DP array. Here, we use bitmask to represent subsets and iterate over them.
Time complexity: O(2N ∗ N ).
Note: The Hungarian Algorithm solves this problem in O(N 3 ) time complexity.
4
2 DP on Rooted Trees
We define functions for nodes of the trees which we calculate recursively based on children of a node.
One of the states in our DP is usually a nodei , denoting that we are solving it for the sub-tree of
nodei .
2.1 Problem
Given a tree T of N (1-indexed) nodes, where each nodei has Ci coins attached to it. You have to
choose a subset of nodes such that no two adjacent nodes (nodes connected directly by an edge) are
chosen and the sum of coins attached to nodes in the chosen subset is maximized.
We define dp1(V ) and dp2(V ) as the optimal solution for when we are choosing nodes from sub-tree
of node V and if we include node V in our answer or not, respectively. Our final answer is the
maximum of two cases, max(dp1(V ), dp2(V )).
We can see that dp1(V ) = CV + ni=1 dp2(vi ) , where n is the number of children of node V and vi
P
is the ith child of the V . Similarly, dp2(V ) = ni=1 max(dp1(vi ), dp2(V )).
P
Complexity: O(N ).
Solution Code:
1 //pV is parent of V
2 void dfs(int V, int pV){
3 //base case:
4 //when dfs reaches a leaf it finds dp1 and dp2 and does not branch again.
5
6 //for storing sums of dp1 and max(dp1, dp2) for all children of V
7 int sum1=0, sum2=0;
8
9 //traverse over all children
10 for(auto v: adj[V]){
11 if(v == pV) continue;
12 dfs(v, V);
13 sum1 += dp2[v];
14 sum2 += max(dp1[v], dp2[v]);
15 }
16
17 dp1[V] = C[V] + sum1;
18 dp2[V] = sum2;
19 }
20 //Nodes are 1-indexed, therefore our answer stored in dp1[1] and dp2[1]
21 //for the answer we take max(dp1[1],dp2[1]) after calling dfs(1,0).
5
3 DP on Directed Acyclic Graphs
As we know, nodes of a directed acyclic graph (DAG) can be sorted topologically, and DP can be
implemented efficiently through this sorting.
First, we can find topological order with topological sort in O(N ) time complexity. Then we can find
the dp(V ) values in topological order, where the V is a node in the DAG and dp(V ) is the answer
for node V . Answer and implementation will differ from problem to problem.
Most of DP problems can be converted into a DAG. We are going to see why that is the case.
It is obvious that while doing DP and processing a state we evaluate a state by looking all of the
possible previous states; and to be able to this, all of the possible previous states must be processed
before the current state. From this point of view, we can see that some states depend on the other
states to be able to get processed, which gives us a DAG formation.
Note that some DP problems cannot be converted into a DAG and may require hyper-graphs. (Please
refer Advanced Dynamic Programming in Semiring and Hypergraph Frameworks for more details)
6
Problem:
There are N stones numbered 1,2,...,N . For each i (1 ≤ i ≤ N ), the height of ith stone is hi .
There is a frog who is initially on the stone 1. He will repeat the following action some number of
times to reach stone N :
If the frog is currently on stonei , it can jump to stonei+1 or stonei+2 . Here, the cost of a jump from
i to j is | hi − hj |. Find the minimum possible cost to reach stoneN .
Solution:
DP approach: We define dp[i] as the minimum cost of the first i index. In the end, our answer will
be dp[N ]. We can denote dp[i] in terms of dp[i − 1] and dp[i − 2]:
For N=5, we can see that in order to calculate dp[5] we need to calculate dp[4] and dp[3] first and
this rule applies to dp[4] and dp[3] as well.
Similarly,
dp[4] needs dp[3] and dp[2] to be calculated,
dp[3] needs dp[2] and dp[1] to be calculated,
dp[2] needs dp[1] to be calculated,
7
3.2 DP on Directed Acyclic Graph Problem
For a given DAG with N nodes and M weighted edges, find the longest path in the DAG.
Complexity: O(N + M )
Solution Code:
8
4 Digit DP
Problems that require the calculation of how many numbers there are between two values (say, A
and B) satisfying a particular property, can be solved using digit dynamic programming.
While constructing our numbers recursively (from the left), we need a way to check if our number is
still smaller than the given boundary number. For that, while branching, we keep a variable (named
”strict”) which limits our ability to select numbers that are bigger than the boundary number.
Let’s suppose that the boundary number is A. We start filling the number from the left (most
significant digit) and set strict to ”true”, meaning that we cannot take any number that is higher
than A’s corresponding digit. While branching, the values less than that digit are now going to be
not strict (strict = ”f alse”) because after that point we guarantee that the number is going to be
smaller than A. For the value equal to A’s corresponding digit, strictness will continue to be ”true”.
How many numbers x are there in the range A to B, where the digit d occurs exactly k times in x?
Constraints: A, B < 1060 , k < 60.
Iterate over all the numbers in the range [A, B] and count the number of digits equal to the d one
by one for every number. This complexity is too big, hence we need a better approach.
We can recursively fill digits of our number starting from the left. At any point of time we branch
to 3 possibilities, namely picking a number that is not d and smaller than the corresponding digit of
the boundary number, picking a number that is equal to the d and picking a number that is equal to
the corresponding digit of the boundary number. The depth of the recursive is equal to the number
of the digits in the decimal representation of the boundary number. Thus, we get a complexity of
O(3log10 N ). Still not enough.
We can represent a dp state by (current index, current strictness, the number of d0 s) which
denotes, the number of possible configurations of the remaining digits after picking the current digit.
We use dp[log10 N ][2][log10 N ] array and calculate every value maximum one time, therefore the worst
case is (log10 N ) ∗ 2 ∗ (log10 N ). Thus, we get O((log10 N )2 ) time complexity.
9
Solution Code:
1 #include <bits/stdc++.h>
2 using namespace std;
3 #define ll long long
4 ll A,B,d,k,dg;// dg: digit count
5 vector <ll> v;// digit vector
6 ll dp[25][2][25];
7 void setup(ll a)
8 {
9 memset(dp,0,sizeof dp);
10 v.clear();
11 ll tmp = a;
12 while(tmp)
13 {
14 v.push_back(tmp%10);
15 tmp/=10;
16 }
17 dg = (ll)v.size();
18 reverse(v.begin(), v.end());
19 }
20 ll rec(int idx, bool strict, int count)
21 {
22 if(dp[idx][strict][count]) return dp[idx][strict][count];
23 if(idx == dg or count > k) return (count == k);
24 ll sum = 0;
25 if(strict)
26 {
27 // all <v[idx] if d is included -1
28 sum += rec(idx+1, 0, count) * (v[idx] - (d<v[idx]));
29 // v[idx], if d==v[idx] send count+1
30 sum += rec(idx+1, 1, count + (v[idx]==d) );
31 if(d < v[idx])
32 sum += rec(idx+1, 0, count+1); // d
33 }
34 else
35 {
36 sum += rec(idx+1, 0, count) * (9); // other than d (10 - 1)
37 sum += rec(idx+1, 0, count+1); // d
38 }
39 return dp[idx][strict][count] = sum;
40 }
41 int main()
42 {
43 cin >> A >> B >> d >> k;
44 setup(B);
45 ll countB = rec(0,1,0);//countB is answer of [0..B]
46 setup(A-1);
47 ll countA = rec(0,1,0);//countA is answer of [0..A-1]
48 cout << fixed << countB - countA << endl;//difference gives us [A..B]
49 }
10
5 Walk Counting using Matrix Exponentiation
This method helps to count the number of walks with the desired length on a graph.
Let l be the desired length and let A and B be a node in graph G. If D is the adjacency matrix of
G, then Dl [A][B] is the number of walks from A to B with length l, where Dk denotes kth power of
the matrix D.[11]
5.1 Example
Directed Graph:
11
From the D3 we can see that there is 4 total walks with 3 length.
Let S be the set of walks and let w be a walk where w = {n1 , n2 , ..nk } and ni = ith node of the
walk. Then,
S = {{1, 3, 4, 3}, {3, 4, 3, 2}, {3, 4, 3, 4}, {4, 3.4.3}} and |S| = 4.
Using fast exponentiation on adjacency matrix we can find number of walks with length k in
O(N 3 log k) time, where N is the number of nodes in the graph.
Note that O(N 3 ) time complexity comes from matrix multiplication and we do log k multiplication
using fast exponentiation resulting in O(N 3 log k) time complexity.
12
6 Tree Child-Sibling Notation
In this method, we change the structure of the tree. In the standard tree, each parent node is con-
nected to all of its children. Here, instead of having each node store the pointers to all of its children,
a node will store a pointer to just one of its children. Apart from this, the node will also store a
pointer to its immediate right sibling.[1]
In this notation we can see that every node at most has 2 children (left(first-child), right(first-sibling)),
therefore this notation represents a binary tree.
13
6.1 Why You Would Use the LCRS Notation
The main reason for using the LCRS notation is to save memory. In the LCRS notation we do use
less memory than the standard notation.
Possible cases:
For example, the heap data structure. The main operations used on the Heap data structure
are:
• Join two trees together by making one tree a child of the other.
These two operations can be done efficiently on an LCRS structure. Therefore, using the LCRS
structure is convenient while working on a heap data structure. [6]
14
References
15