Fixes and clean code

This commit is contained in:
Antti H S Laaksonen 2017-05-18 22:00:29 +03:00
parent 22749765ee
commit cdca81e407
1 changed files with 110 additions and 115 deletions

View File

@ -30,8 +30,9 @@ counting the solutions.
Understanding dynamic programming is a milestone Understanding dynamic programming is a milestone
in every competitive programmer's career. in every competitive programmer's career.
While the basic idea of the technique is simple, While the basic idea is simple,
the challenge is how to apply it to different problems. the challenge is how to apply
dynamic programming to different problems.
This chapter introduces a set of classic problems This chapter introduces a set of classic problems
that are a good starting point. that are a good starting point.
@ -73,11 +74,11 @@ subproblems.
In the coin problem, a natural recursive In the coin problem, a natural recursive
problem is as follows: problem is as follows:
what is the smallest number of coins what is the smallest number of coins
required for constructing a sum $x$? required to form a sum $x$?
Let $f(x)$ be a function that gives the answer Let $f(x)$ be a function that gives the answer
to the problem, i.e., $f(x)$ is the smallest to the problem, i.e., $f(x)$ is the smallest
number of coins required for constructing a sum $x$. number of coins required to form a sum $x$.
The values of the function depend on the The values of the function depend on the
values of the coins. values of the coins.
For example, if the coin values are $\{1,3,4\}$, For example, if the coin values are $\{1,3,4\}$,
@ -101,27 +102,27 @@ f(10) & = & 3 \\
First, $f(0)=0$ because no coins are needed First, $f(0)=0$ because no coins are needed
for the sum $0$. for the sum $0$.
Moreover, $f(3)=1$ because the sum $3$ Then, for example, $f(3)=1$ because the sum $3$
can be formed using coin 3, can be formed using coin 3,
and $f(5)=2$ because the sum 5 can and $f(5)=2$ because the sum 5 can
be formed using coins 1 and 4. be formed using coins 1 and 4.
The essential property in the function is The essential property of $f$ is
that each value of $f(x)$ can be calculated that its values can be
recursively from smaller values of the function. recursively calculated from its smaller values.
For example, if the coin set is $\{1,3,4\}$, For example, if the coin set is $\{1,3,4\}$,
there are three ways to select the first coin there are three ways how we can choose the
in a solution: we can choose coin 1, 3 or 4. first coin in a solution.
If coin 1 is chosen, the remaining task is to If we choose coin 1, the remaining task
form the sum $x-1$. is to form the sum $x-1$.
Similarly, if coin 3 or 4 is chosen, Similarly, after choosing coins 3 and 4,
we should form the sum $x-3$ or $x-4$. the remaining sums are $x-3$ and $x-4$.
Thus, the recursive formula is Thus, the recursive formula is
\[f(x) = \min(f(x-1),f(x-3),f(x-4))+1\] \[f(x) = \min(f(x-1),f(x-3),f(x-4))+1\]
where the function $\min$ gives the smallest where the function $\min$ gives the smallest
of its parameters. of its parameters.
In the general case, for the coin set In the general case, for a coin set
$\{c_1,c_2,\ldots,c_k\}$, $\{c_1,c_2,\ldots,c_k\}$,
the recursive formula is the recursive formula is
\[f(x) = \min(f(x-c_1),f(x-c_2),\ldots,f(x-c_k))+1.\] \[f(x) = \min(f(x-c_1),f(x-c_2),\ldots,f(x-c_k))+1.\]
@ -130,9 +131,9 @@ The base case for the function is
because no coins are needed for constructing because no coins are needed for constructing
the sum 0. the sum 0.
In addition, it is convenient to define In addition, it is convenient to define
\[f(x)=\infty\hspace{8px}\textrm{if $x<0$}.\] \[f(x)=\infty\hspace{8px}\textrm{if $x<0$},\]
This means that an infinite number of coins which means that to get a negative sum of money,
is needed for forming a negative sum of money. an infinite number of coins is needed.
This prevents the function from constructing This prevents the function from constructing
a solution where the initial sum of money is negative. a solution where the initial sum of money is negative.
@ -144,16 +145,16 @@ we can directly implement a solution in C++:
int f(int x) { int f(int x) {
if (x < 0) return INF; if (x < 0) return INF;
if (x == 0) return 0; if (x == 0) return 0;
int u = INF; int best = INF;
for (int i = 1; i <= k; i++) { for (auto c : coins) {
u = min(u, f(x-c[i])+1); best = min(best, f(x-c)+1);
} }
return u; return best;
} }
\end{lstlisting} \end{lstlisting}
The code assumes that the available coins are The code assumes that the available coins are
stored in an array $\texttt{c}$, stored in an array $\texttt{coins}$,
and the constant \texttt{INF} denotes infinity. and the constant \texttt{INF} denotes infinity.
This function works but it is not efficient yet, This function works but it is not efficient yet,
because it goes through a large number because it goes through a large number
@ -161,7 +162,7 @@ of ways to construct the sum.
However, the function can be made efficient by However, the function can be made efficient by
using memoization. using memoization.
\subsubsection{Memoization} \subsubsection{Using memoization}
\index{memoization} \index{memoization}
@ -175,16 +176,18 @@ For each parameter, the value of the function
is calculated recursively only once, and after this, is calculated recursively only once, and after this,
the value can be directly retrieved from the array. the value can be directly retrieved from the array.
In this problem, we can use an array In this problem, we use arrays
\begin{lstlisting} \begin{lstlisting}
int d[N]; bool ready[N];
int value[N];
\end{lstlisting} \end{lstlisting}
where $\texttt{d}[x]$ will contain where $\texttt{ready}[x]$ indicates
the value of $f(x)$. whether the value of $f(x)$ has been calculated,
The constant $N$ has to be chosen so and if it is, $\texttt{value}[x]$
that all required values of the function fit contains this value.
in the array. The constant $N$ has been chosen so
that all required values fit in the arrays.
After this, the function can be efficiently After this, the function can be efficiently
implemented as follows: implemented as follows:
@ -193,109 +196,103 @@ implemented as follows:
int f(int x) { int f(int x) {
if (x < 0) return INF; if (x < 0) return INF;
if (x == 0) return 0; if (x == 0) return 0;
if (d[x]) return d[x]; if (ready[x]) return value[x];
int u = INF; int best = INF;
for (int i = 1; i <= k; i++) { for (auto c : coins) {
u = min(u, f(x-c[i])+1); best = min(best, f(x-c)+1);
} }
d[x] = u; ready[x] = true;
return d[x]; value[x] = best;
return best;
} }
\end{lstlisting} \end{lstlisting}
The function handles the base cases The function handles the base cases
$x<0$ and $x=0$ as previously. $x<0$ and $x=0$ as previously.
Then the function checks if Then the function checks from
$\texttt{ready}[x]$ if
$f(x)$ has already been stored $f(x)$ has already been stored
in $\texttt{d}[x]$. in $\texttt{value}[x]$,
If the value of $f(x)$ is found in the array, and if it is, the function directly returns it.
the function directly returns it.
Otherwise the function calculates the value Otherwise the function calculates the value
recursively and stores it in $\texttt{d}[x]$. recursively and stores it in $\texttt{value}[x]$.
Using memoization the function works Using memoization the function works
efficiently, because the answer for each parameter $x$ efficiently, because the answer for each parameter $x$
is calculated recursively only once. is calculated recursively only once.
After a value of $f(x)$ has been stored in the array, After a value of $f(x)$ has been stored in $\texttt{value}[x]$,
it can be efficiently retrieved whenever the it can be efficiently retrieved whenever the
function will be called again with the parameter $x$. function will be called again with the parameter $x$.
The time complexity of the resulting algorithm The resulting algorithm works in $O(xk)$ time,
is $O(xk)$ where the sum is $x$ and the number of where the sum is $x$ and the number of coins is $k$.
coins is $k$.
In practice, the algorithm can be used if In practice, the algorithm can be used if
$x$ is so small that it is possible to allocate $x$ is so small that it is possible to allocate
an array for all possible function parameters. an array for all possible function parameters.
Note that the array can also be constructed using Note that we can also construct the array \texttt{value}
a loop that calculates all the values \emph{iteratively} using
instead of a recursive function: a loop that simply calculates all the values
of $f$ for parameters $0 \ldots x$:
\begin{lstlisting} \begin{lstlisting}
d[0] = 0; value[0] = 0;
for (int i = 1; i <= x; i++) { for (int i = 1; i <= x; i++) {
int u = INF; value[i] = INF;
for (int j = 1; j <= k; j++) { for (auto c : coins) {
if (i-c[j] < 0) continue; if (i-c >= 0) {
u = min(u, d[i-c[j]]+1); value[i] = min(value[i], value[i-c]+1);
}
d[i] = u;
}
\end{lstlisting}
This implementation is shorter and somewhat
more efficient than recursion,
and experienced competitive programmers
often prefer dynamic programming solutions
that are implemented using loops.
Still, the underlying idea is the same as
in the recursive function.
\subsubsection{Constructing a solution}
Sometimes we are asked both to find the value
of an optimal solution and also to give
an example how such a solution can be constructed.
In the coin problem, this means that the algorithm
should show how to select the coins that produce
the sum $x$ using as few coins as possible.
We can construct the solution by adding another
array to the code. The new array indicates for
each sum of money the first coin that should be
chosen in an optimal solution.
In the following code, the array \texttt{e}
is used for this:
\begin{lstlisting}
d[0] = 0;
for (int i = 1; i <= x; i++) {
d[i] = INF;
for (int j = 1; j <= k; j++) {
if (i-c[j] < 0) continue;
int u = d[i-c[j]]+1;
if (u < d[i]) {
d[i] = u;
e[i] = c[j];
} }
} }
} }
\end{lstlisting} \end{lstlisting}
After this, we can print the coins needed Since the iterative solution is shorter and a bit
for the sum $x$ as follows: more efficient than recursion,
competitive programmers often prefer this solution.
\subsubsection{Constructing an example solution}
Sometimes we are asked both to find the value
of an optimal solution and to give
an example how such a solution can be constructed.
In the coin problem, for example,
we might be asked both the minimum number of coins
and an example how to choose the coins.
We can do this by using an array \texttt{first}
that indicates for
each sum of money the first coin
in an optimal solution:
\begin{lstlisting}
value[0] = 0;
for (int i = 1; i <= x; i++) {
value[i] = INF;
for (auto c : coins) {
if (i-c < 0) continue;
int v = value[i-c]+1;
if (v < value[i]) {
value[i] = v;
first[i] = c;
}
}
}
\end{lstlisting}
After this, we can print the coins that
form the sum $x$ as follows:
\begin{lstlisting} \begin{lstlisting}
while (x > 0) { while (x > 0) {
cout << e[x] << "\n"; cout << first[x] << "\n";
x -= e[x]; x -= first[x];
} }
\end{lstlisting} \end{lstlisting}
\subsubsection{Counting the number of solutions} \subsubsection{Counting the number of solutions}
Let us now consider a variant of the problem Let us now consider a variant of the coin problem
that is otherwise like the original problem, that is otherwise like the original problem,
but we should count the total number of solutions instead but we are asked to count the total number of solutions instead
of finding the optimal solution. of finding the optimal solution.
For example, if the coins are $\{1,3,4\}$ and For example, if the coins are $\{1,3,4\}$ and
the target sum is $5$, the target sum is $5$,
@ -312,21 +309,19 @@ there are a total of 6 solutions:
\end{itemize} \end{itemize}
\end{multicols} \end{multicols}
The number of the solutions can be calculated The number of solutions can be calculated
using the same idea as finding the optimal solution. using the same idea as finding the optimal solution.
The difference is that when finding the optimal solution, The difference is that when finding the optimal solution,
we maximize or minimize something in the recursion, we maximize or minimize something in the recursion,
but now we will calculate sums of numbers of solutions. but now we calculate sums of numbers of solutions.
To solve the problem, we can define a function $f(x)$ To solve the problem, we define a function $f(x)$
that gives the number of ways to construct that gives the number of ways to construct
a sum $x$ using the coins. a sum $x$ using the coins.
For example, $f(5)=6$ when the coins are $\{1,3,4\}$. For example, $f(5)=6$ when the coins are $\{1,3,4\}$.
The value of $f(x)$ can be calculated recursively The value of $f(x)$ can be calculated recursively
using the formula using the formula
\[ f(x) = f(x-c_1)+f(x-c_2)+\cdots+f(x-c_{k}),\] \[ f(x) = f(x-c_1)+f(x-c_2)+\cdots+f(x-c_{k}).\]
because to form the sum $x$, we have to first
choose some coin $c_i$ and then form the sum $x-c_i$.
The base cases are $f(0)=1$, because there is exactly The base cases are $f(0)=1$, because there is exactly
one way to form the sum 0 using an empty set of coins, one way to form the sum 0 using an empty set of coins,
and $f(x)=0$, when $x<0$, because it is not possible and $f(x)=0$, when $x<0$, because it is not possible
@ -352,14 +347,15 @@ f(9) & = & 40 \\
The following code calculates the value of $f(x)$ The following code calculates the value of $f(x)$
using dynamic programming by filling the array using dynamic programming by filling the array
\texttt{d} for parameters $0 \ldots x$: \texttt{count} for parameters $0 \ldots x$:
\begin{lstlisting} \begin{lstlisting}
d[0] = 1; count[0] = 1;
for (int i = 1; i <= x; i++) { for (int i = 1; i <= x; i++) {
for (int j = 1; j <= k; j++) { for (auto c : coins) {
if (i-c[j] < 0) continue; if (i-c >= 0) {
d[i] += d[i-c[j]]; count[i] += count[i-c];
}
} }
} }
\end{lstlisting} \end{lstlisting}
@ -372,20 +368,19 @@ This can be done by changing the code so that
all calculations are done modulo $m$. all calculations are done modulo $m$.
In the above code, it suffices to add the line In the above code, it suffices to add the line
\begin{lstlisting} \begin{lstlisting}
d[i] %= m; count[i] %= m;
\end{lstlisting} \end{lstlisting}
after the line after the line
\begin{lstlisting} \begin{lstlisting}
d[i] += d[i-c[j]]; count[i] += count[i-c];
\end{lstlisting} \end{lstlisting}
Now we have discussed all basic Now we have discussed all basic
techniques related to ideas of dynamic programming.
dynamic programming.
Since dynamic programming can be used Since dynamic programming can be used
in many different situations, in many different situations,
we will now go through a set of problems we will now go through a set of problems
that show further examples about that show further examples about the
possibilities of dynamic programming. possibilities of dynamic programming.
\section{Longest increasing subsequence} \section{Longest increasing subsequence}