2016-12-28 23:54:51 +01:00
|
|
|
\chapter{Dynamic programming}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
\index{dynamic programming}
|
2016-12-28 23:54:51 +01:00
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
\key{Dynamic programming}
|
|
|
|
is a technique that combines the correctness
|
|
|
|
of complete search and the efficiency
|
|
|
|
of greedy algorithms.
|
|
|
|
Dynamic programming can be used if the
|
|
|
|
problem can be divided into subproblems
|
|
|
|
that can be calculated independently.
|
2016-12-28 23:54:51 +01:00
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
There are two uses for dynamic programming:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\begin{itemize}
|
|
|
|
\item
|
2017-01-02 18:16:30 +01:00
|
|
|
\key{Findind an optimal solution}:
|
|
|
|
We want to find a solution that is
|
|
|
|
as large as possible or as small as possible.
|
2016-12-28 23:54:51 +01:00
|
|
|
\item
|
2017-01-02 18:16:30 +01:00
|
|
|
\key{Couting the number of solutions}:
|
|
|
|
We want to calculate the total number of
|
|
|
|
possible solutions.
|
2016-12-28 23:54:51 +01:00
|
|
|
\end{itemize}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
We will first see how dynamic programming can
|
|
|
|
be used for finding an optimal solution,
|
|
|
|
and then we will use the same idea for
|
|
|
|
counting the solutions.
|
|
|
|
|
|
|
|
Understanding dynamic programming is a milestone
|
|
|
|
in every competitive programmer's career.
|
|
|
|
While the basic idea of the technique is simple,
|
|
|
|
the challenge is how to apply it for different problems.
|
|
|
|
This chapter introduces a set of classic problems
|
|
|
|
that are a good starting point.
|
|
|
|
|
|
|
|
\section{Coin problem}
|
|
|
|
|
|
|
|
We first consider a problem that we
|
|
|
|
have already seen:
|
|
|
|
Given a set of coin values $\{c_1,c_2,\ldots,c_k\}$
|
|
|
|
and a sum of money $x$, our task is to
|
|
|
|
form the sum $x$ using as few coins as possible.
|
|
|
|
|
|
|
|
In Chapter 6.1, we solved the problem using a
|
|
|
|
greedy algorithm that always selects the largest
|
|
|
|
possible coin for the sum.
|
|
|
|
The greedy algorithm works, for example,
|
|
|
|
when the coins are the euro coins,
|
|
|
|
but in the general case the greedy algorithm
|
|
|
|
doesn't necessarily produce an optimal solution.
|
|
|
|
|
|
|
|
Now it's time to solve the problem efficiently
|
|
|
|
using dynamic programming, so that the algorithms
|
|
|
|
works for any coin set.
|
|
|
|
The dynamic programming
|
|
|
|
algorithm is based on a recursive function
|
|
|
|
that goes through all possibilities how to
|
|
|
|
select the coins, like a brute force algorithm.
|
|
|
|
However, the dynamic programming
|
|
|
|
algorithm is efficient because
|
|
|
|
it uses memoization to
|
|
|
|
calculate the answer for each subproblem only once.
|
|
|
|
|
|
|
|
\subsubsection{Recursive formulation}
|
|
|
|
|
|
|
|
The idea in dynamic programming is to
|
|
|
|
formulate the problem recursively so
|
|
|
|
that the answer for the problem can be
|
|
|
|
calculated from the answers for the smaller
|
|
|
|
subproblems.
|
|
|
|
In this case, a natural problem is as follows:
|
|
|
|
what is the smallest number of coins
|
|
|
|
required for constructing sum $x$?
|
|
|
|
|
|
|
|
Let $f(x)$ be a function that gives the answer
|
|
|
|
for the problem, i.e., $f(x)$ is the smallest
|
|
|
|
number of coins required for constructing sum $x$.
|
|
|
|
The values of the function depend on the
|
|
|
|
values of the coins.
|
|
|
|
For example, if the values are $\{1,3,4\}$,
|
|
|
|
the first values of the function are as follows:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\[
|
|
|
|
\begin{array}{lcl}
|
|
|
|
f(0) & = & 0 \\
|
|
|
|
f(1) & = & 1 \\
|
|
|
|
f(2) & = & 2 \\
|
|
|
|
f(3) & = & 1 \\
|
|
|
|
f(4) & = & 1 \\
|
|
|
|
f(5) & = & 2 \\
|
|
|
|
f(6) & = & 2 \\
|
|
|
|
f(7) & = & 2 \\
|
|
|
|
f(8) & = & 2 \\
|
|
|
|
f(9) & = & 3 \\
|
|
|
|
f(10) & = & 3 \\
|
|
|
|
\end{array}
|
|
|
|
\]
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
First, $f(0)=0$ because no coins are needed
|
|
|
|
for sum $0$.
|
|
|
|
Moreover, $f(3)=1$ because the sum $3$
|
|
|
|
can be formed using coin 3,
|
|
|
|
and $f(5)=2$ because the sum 5 can
|
|
|
|
be formed using coins 1 and 4.
|
|
|
|
|
|
|
|
The essential property in the function is
|
|
|
|
that the value $f(x)$ can be calculated
|
|
|
|
recursively from the smaller values of the function.
|
|
|
|
For example, if the coin set is $\{1,3,4\}$,
|
|
|
|
there are three ways to select the first coin
|
|
|
|
in a solution: we can choose coin 1, 3 or 4.
|
|
|
|
If coin 1 is chosen, the remaining task is to
|
|
|
|
form the sum $x-1$.
|
|
|
|
Similarly, if coin 3 or 4 is chosen,
|
|
|
|
we should form the sum $x-3$ or $x-4$.
|
|
|
|
|
|
|
|
Thus, the recursive formula is
|
|
|
|
\[f(x) = \min(f(x-1),f(x-3),f(x-4))+1\]
|
|
|
|
where the function $\min$ returns the smallest
|
|
|
|
of its parameters.
|
|
|
|
In the general case, for the coin set
|
|
|
|
$\{c_1,c_2,\ldots,c_k\}$,
|
|
|
|
the recursive formula is
|
2016-12-28 23:54:51 +01:00
|
|
|
\[f(x) = \min(f(x-c_1),f(x-c_2),\ldots,f(x-c_k))+1.\]
|
2017-01-02 18:16:30 +01:00
|
|
|
The base case for the function is
|
2016-12-28 23:54:51 +01:00
|
|
|
\[f(0)=0,\]
|
2017-01-02 18:16:30 +01:00
|
|
|
because no coins are needed for constructing
|
|
|
|
the sum 0.
|
|
|
|
In addition, it's a good idea to define
|
2016-12-28 23:54:51 +01:00
|
|
|
\[f(x)=\infty,\hspace{8px}\textrm{jos $x<0$}.\]
|
2017-01-02 18:16:30 +01:00
|
|
|
This means that an infinite number of coins
|
|
|
|
is needed to create a negative sum of money.
|
|
|
|
This prevents the situation that the recursive
|
|
|
|
function would form a solution where the
|
|
|
|
initial sum of money is negative.
|
2016-12-28 23:54:51 +01:00
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
Now it's possible to implement the function in C++
|
|
|
|
directly using the recursive definition:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\begin{lstlisting}
|
|
|
|
int f(int x) {
|
|
|
|
if (x == 0) return 0;
|
|
|
|
if (x < 0) return 1e9;
|
|
|
|
int u = 1e9;
|
|
|
|
for (int i = 1; i <= k; i++) {
|
|
|
|
u = min(u, f(x-c[i])+1);
|
|
|
|
}
|
|
|
|
return u;
|
|
|
|
}
|
|
|
|
\end{lstlisting}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
The code assumes that the available coins are
|
2016-12-28 23:54:51 +01:00
|
|
|
$\texttt{c}[1], \texttt{c}[2], \ldots, \texttt{c}[k]$,
|
2017-01-02 18:16:30 +01:00
|
|
|
and the value $10^9$ means infinity.
|
|
|
|
This function works but it is not efficient yet
|
|
|
|
because it goes through a large number
|
|
|
|
of ways to construct the sum.
|
|
|
|
However, the function becomes efficient by
|
|
|
|
using memoization.
|
|
|
|
|
|
|
|
\subsubsection{Memoization}
|
|
|
|
|
|
|
|
\index{memoization}
|
|
|
|
|
|
|
|
Dynamic programming allows to calculate the
|
|
|
|
value of a recursive function efficiently
|
|
|
|
using \key{memoization}.
|
|
|
|
This means that an auxiliary array is used
|
|
|
|
for storing the values of the function
|
|
|
|
for different parameters.
|
|
|
|
For each parameter, the value of the function
|
|
|
|
is calculated only once, and after this,
|
|
|
|
it can be directly retrieved from the array.
|
|
|
|
|
|
|
|
In this problem, we can use the array
|
2016-12-28 23:54:51 +01:00
|
|
|
\begin{lstlisting}
|
|
|
|
int d[N];
|
|
|
|
\end{lstlisting}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
where $\texttt{d}[x]$ will contain
|
|
|
|
the value $f(x)$.
|
|
|
|
The constant $N$ should be chosen so
|
|
|
|
that there is space for all needed
|
|
|
|
values of the function.
|
2016-12-28 23:54:51 +01:00
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
After this, the function can be efficiently
|
|
|
|
implemented as follows:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\begin{lstlisting}
|
|
|
|
int f(int x) {
|
|
|
|
if (x == 0) return 0;
|
|
|
|
if (x < 0) return 1e9;
|
|
|
|
if (d[x]) return d[x];
|
|
|
|
int u = 1e9;
|
|
|
|
for (int i = 1; i <= k; i++) {
|
|
|
|
u = min(u, f(x-c[i])+1);
|
|
|
|
}
|
|
|
|
d[x] = u;
|
|
|
|
return d[x];
|
|
|
|
}
|
|
|
|
\end{lstlisting}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
The function handles the base cases
|
|
|
|
$x=0$ and $x<0$ as previously.
|
|
|
|
Then the function checks if
|
|
|
|
$f(x)$ has already been calculated
|
|
|
|
and stored to $\texttt{d}[x]$.
|
|
|
|
If $f(x)$ can be found in the array,
|
|
|
|
the function directly returns it.
|
|
|
|
Otherwise the function calculates the value
|
|
|
|
recursively and stores it to $\texttt{d}[x]$.
|
|
|
|
|
|
|
|
Using memoization the function works
|
|
|
|
efficiently because it is needed to
|
|
|
|
recursively calculate
|
|
|
|
the answer for each $x$ only once.
|
|
|
|
After a value $f(x)$ has been stored to the array,
|
|
|
|
it can be directly retrieved whenever the
|
|
|
|
function will be called again with parameter $x$.
|
|
|
|
|
|
|
|
The time complexity of the resulting algorithm
|
|
|
|
is $O(xk)$ when the sum is $x$ and the number of
|
|
|
|
coins is $k$.
|
|
|
|
In practice, the algorithm is usable if
|
|
|
|
$x$ is so small that it is possible to allocate
|
|
|
|
an array for all possible function parameters.
|
|
|
|
|
|
|
|
Note that the array can also be constructed using
|
|
|
|
a loop that calculates all the values
|
|
|
|
instead of a recursive function:
|
2016-12-28 23:54:51 +01:00
|
|
|
\begin{lstlisting}
|
|
|
|
d[0] = 0;
|
|
|
|
for (int i = 1; i <= x; i++) {
|
|
|
|
int u = 1e9;
|
|
|
|
for (int j = 1; j <= k; j++) {
|
|
|
|
if (i-c[j] < 0) continue;
|
|
|
|
u = min(u, d[i-c[j]]+1);
|
|
|
|
}
|
|
|
|
d[i] = u;
|
|
|
|
}
|
|
|
|
\end{lstlisting}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
This implementation is shorter and somewhat
|
|
|
|
more efficient than recursion,
|
|
|
|
and experienced competitive programmers
|
|
|
|
often implement dynamic programming solutions
|
|
|
|
using loops.
|
|
|
|
Still, the underlying idea is the same as
|
|
|
|
in the recursive function.
|
|
|
|
|
|
|
|
\subsubsection{Constructing the solution}
|
|
|
|
|
|
|
|
Sometimes it is not enough to find out the value
|
|
|
|
of the optimal solution, but we should also give
|
|
|
|
an example how such a solution can be constructed.
|
|
|
|
In this 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 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:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\begin{lstlisting}
|
|
|
|
d[0] = 0;
|
|
|
|
for (int i = 1; i <= x; i++) {
|
|
|
|
d[i] = 1e9;
|
|
|
|
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}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
After this, we can print the coins needed
|
|
|
|
for the sum $x$ as follows:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\begin{lstlisting}
|
|
|
|
while (x > 0) {
|
|
|
|
cout << e[x] << "\n";
|
|
|
|
x -= e[x];
|
|
|
|
}
|
|
|
|
\end{lstlisting}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
\subsubsection{Counting the number of solutions}
|
2016-12-28 23:54:51 +01:00
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
Let us now consider a variation of the problem
|
|
|
|
that it's like the original problem but we should
|
|
|
|
count the total number of solutions instead
|
|
|
|
of finding the optimal solution.
|
|
|
|
For example, if the coins are $\{1,3,4\}$ and
|
|
|
|
the required sum is $5$,
|
|
|
|
there are a total of 6 solutions:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\begin{multicols}{2}
|
|
|
|
\begin{itemize}
|
|
|
|
\item $1+1+1+1+1$
|
|
|
|
\item $1+1+3$
|
|
|
|
\item $1+3+1$
|
|
|
|
\item $3+1+1$
|
|
|
|
\item $1+4$
|
|
|
|
\item $4+1$
|
|
|
|
\end{itemize}
|
|
|
|
\end{multicols}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
The number of the solutions can be calculated
|
|
|
|
using the same idea as finding the optimal solution.
|
|
|
|
The difference is that when finding the optimal solution,
|
|
|
|
we maximize or minimize something in the recursion,
|
|
|
|
but now we will sum together all possible alternatives to
|
|
|
|
construct a solution.
|
|
|
|
|
|
|
|
In this case, we can define a function $f(x)$
|
|
|
|
that returns the number of ways to construct
|
|
|
|
the sum $x$ using the coins.
|
|
|
|
For example, $f(5)=6$ when the coins are $\{1,3,4\}$.
|
|
|
|
The function $f(x)$ can be recursively calculated
|
|
|
|
using the formula
|
|
|
|
\[ f(x) = f(x-c_1)+f(x-c_2)+\cdots+f(x-c_k)\]
|
|
|
|
because to form the sum $x$ we should first
|
|
|
|
choose some coin $c_i$ and after this form the sum $x-c_i$.
|
|
|
|
The base cases are $f(0)=1$ because there is exactly
|
|
|
|
one way to form the sum 0 using an empty set of coins,
|
|
|
|
and $f(x)=0$, when $x<0$, because it's not possible
|
|
|
|
to form a negative sum of money.
|
|
|
|
|
|
|
|
In the above example the function becomes
|
2016-12-28 23:54:51 +01:00
|
|
|
\[ f(x) = f(x-1)+f(x-3)+f(x-4) \]
|
2017-01-02 18:16:30 +01:00
|
|
|
and the first values of the function are:
|
2016-12-28 23:54:51 +01:00
|
|
|
\[
|
|
|
|
\begin{array}{lcl}
|
|
|
|
f(0) & = & 1 \\
|
|
|
|
f(1) & = & 1 \\
|
|
|
|
f(2) & = & 1 \\
|
|
|
|
f(3) & = & 2 \\
|
|
|
|
f(4) & = & 4 \\
|
|
|
|
f(5) & = & 6 \\
|
|
|
|
f(6) & = & 9 \\
|
|
|
|
f(7) & = & 15 \\
|
|
|
|
f(8) & = & 25 \\
|
|
|
|
f(9) & = & 40 \\
|
|
|
|
\end{array}
|
|
|
|
\]
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
The following code calculates the value $f(x)$
|
|
|
|
using dynamic programming by filling the array
|
|
|
|
\texttt{d} for parameters $0 \ldots x$:
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\begin{lstlisting}
|
|
|
|
d[0] = 1;
|
|
|
|
for (int i = 1; i <= x; i++) {
|
|
|
|
for (int j = 1; j <= k; j++) {
|
|
|
|
if (i-c[j] < 0) continue;
|
|
|
|
d[i] += d[i-c[j]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
\end{lstlisting}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
Often the number of the solutions is so large
|
|
|
|
that it is not required to calculate the exact number
|
|
|
|
but it is enough to give the answer modulo $m$
|
|
|
|
where, for example, $m=10^9+7$.
|
|
|
|
This can be done by changing the code so that
|
|
|
|
all calculations will be done in modulo $m$.
|
|
|
|
In this case, it is enough to add the line
|
2016-12-28 23:54:51 +01:00
|
|
|
\begin{lstlisting}
|
2017-01-02 18:16:30 +01:00
|
|
|
d[i] %= m;
|
2016-12-28 23:54:51 +01:00
|
|
|
\end{lstlisting}
|
2017-01-02 18:16:30 +01:00
|
|
|
after the line
|
2016-12-28 23:54:51 +01:00
|
|
|
\begin{lstlisting}
|
2017-01-02 18:16:30 +01:00
|
|
|
d[i] += d[i-c[j]];
|
2016-12-28 23:54:51 +01:00
|
|
|
\end{lstlisting}
|
|
|
|
|
2017-01-02 18:16:30 +01:00
|
|
|
Now we have covered all basic
|
|
|
|
techniques related to
|
|
|
|
dynamic programming.
|
|
|
|
Since dynamic programming can be used
|
|
|
|
in many different situations,
|
|
|
|
we will now go through a set of problems
|
|
|
|
that show further examples how dynamic
|
|
|
|
programming can be used.
|
2016-12-28 23:54:51 +01:00
|
|
|
|
|
|
|
\section{Pisin nouseva alijono}
|
|
|
|
|
|
|
|
\index{pisin nouseva alijono@pisin nouseva alijono}
|
|
|
|
|
|
|
|
Annettuna on taulukko, jossa on $n$
|
|
|
|
kokonaislukua $x_1,x_2,\ldots,x_n$.
|
|
|
|
Tehtävänä on selvittää,
|
|
|
|
kuinka pitkä on taulukon
|
|
|
|
\key{pisin nouseva alijono}
|
|
|
|
eli vasemmalta oikealle kulkeva
|
|
|
|
ketju taulukon alkioita,
|
|
|
|
jotka on valittu niin,
|
|
|
|
että jokainen alkio on edellistä suurempi.
|
|
|
|
Esimerkiksi taulukossa
|
|
|
|
|
|
|
|
\begin{center}
|
|
|
|
\begin{tikzpicture}[scale=0.7]
|
|
|
|
\draw (0,0) grid (8,1);
|
|
|
|
\node at (0.5,0.5) {$6$};
|
|
|
|
\node at (1.5,0.5) {$2$};
|
|
|
|
\node at (2.5,0.5) {$5$};
|
|
|
|
\node at (3.5,0.5) {$1$};
|
|
|
|
\node at (4.5,0.5) {$7$};
|
|
|
|
\node at (5.5,0.5) {$4$};
|
|
|
|
\node at (6.5,0.5) {$8$};
|
|
|
|
\node at (7.5,0.5) {$3$};
|
|
|
|
|
|
|
|
\footnotesize
|
|
|
|
\node at (0.5,1.4) {$1$};
|
|
|
|
\node at (1.5,1.4) {$2$};
|
|
|
|
\node at (2.5,1.4) {$3$};
|
|
|
|
\node at (3.5,1.4) {$4$};
|
|
|
|
\node at (4.5,1.4) {$5$};
|
|
|
|
\node at (5.5,1.4) {$6$};
|
|
|
|
\node at (6.5,1.4) {$7$};
|
|
|
|
\node at (7.5,1.4) {$8$};
|
|
|
|
\end{tikzpicture}
|
|
|
|
\end{center}
|
|
|
|
pisin nouseva alijono sisältää 4 alkiota:
|
|
|
|
\begin{center}
|
|
|
|
\begin{tikzpicture}[scale=0.7]
|
|
|
|
\fill[color=lightgray] (1,0) rectangle (2,1);
|
|
|
|
\fill[color=lightgray] (2,0) rectangle (3,1);
|
|
|
|
\fill[color=lightgray] (4,0) rectangle (5,1);
|
|
|
|
\fill[color=lightgray] (6,0) rectangle (7,1);
|
|
|
|
\draw (0,0) grid (8,1);
|
|
|
|
\node at (0.5,0.5) {$6$};
|
|
|
|
\node at (1.5,0.5) {$2$};
|
|
|
|
\node at (2.5,0.5) {$5$};
|
|
|
|
\node at (3.5,0.5) {$1$};
|
|
|
|
\node at (4.5,0.5) {$7$};
|
|
|
|
\node at (5.5,0.5) {$4$};
|
|
|
|
\node at (6.5,0.5) {$8$};
|
|
|
|
\node at (7.5,0.5) {$3$};
|
|
|
|
|
|
|
|
\draw[thick,->] (1.5,-0.25) .. controls (1.75,-1.00) and (2.25,-1.00) .. (2.4,-0.25);
|
|
|
|
\draw[thick,->] (2.6,-0.25) .. controls (3.0,-1.00) and (4.0,-1.00) .. (4.4,-0.25);
|
|
|
|
\draw[thick,->] (4.6,-0.25) .. controls (5.0,-1.00) and (6.0,-1.00) .. (6.5,-0.25);
|
|
|
|
|
|
|
|
\footnotesize
|
|
|
|
\node at (0.5,1.4) {$1$};
|
|
|
|
\node at (1.5,1.4) {$2$};
|
|
|
|
\node at (2.5,1.4) {$3$};
|
|
|
|
\node at (3.5,1.4) {$4$};
|
|
|
|
\node at (4.5,1.4) {$5$};
|
|
|
|
\node at (5.5,1.4) {$6$};
|
|
|
|
\node at (6.5,1.4) {$7$};
|
|
|
|
\node at (7.5,1.4) {$8$};
|
|
|
|
\end{tikzpicture}
|
|
|
|
\end{center}
|
|
|
|
|
|
|
|
Merkitään $f(k)$ kohtaan $k$ päättyvän
|
|
|
|
pisimmän nousevan alijonon pituutta,
|
|
|
|
jolloin ratkaisu tehtävään on suurin
|
|
|
|
arvoista $f(1),f(2),\ldots,f(n)$.
|
|
|
|
Esimerkiksi yllä olevassa taulukossa
|
|
|
|
funktion arvot ovat seuraavat:
|
|
|
|
\[
|
|
|
|
\begin{array}{lcl}
|
|
|
|
f(1) & = & 1 \\
|
|
|
|
f(2) & = & 1 \\
|
|
|
|
f(3) & = & 2 \\
|
|
|
|
f(4) & = & 1 \\
|
|
|
|
f(5) & = & 3 \\
|
|
|
|
f(6) & = & 2 \\
|
|
|
|
f(7) & = & 4 \\
|
|
|
|
f(8) & = & 2 \\
|
|
|
|
\end{array}
|
|
|
|
\]
|
|
|
|
|
|
|
|
Arvon $f(k)$ laskemisessa on kaksi vaihtoehtoa,
|
|
|
|
millainen kohtaan $k$ päättyvä pisin nouseva alijono on:
|
|
|
|
\begin{enumerate}
|
|
|
|
\item Pisin nouseva alijono sisältää vain luvun $x_k$,
|
|
|
|
jolloin $f(k)=1$.
|
|
|
|
\item Valitaan jokin kohta $i$, jolle pätee $i<k$
|
|
|
|
ja $x_i<x_k$.
|
|
|
|
Pisin nouseva alijono saadaan liittämällä
|
|
|
|
kohtaan $i$ päättyvän pisimmän nousevan alijonon perään luku $x_k$.
|
|
|
|
Tällöin $f(k)=f(i)+1$.
|
|
|
|
\end{enumerate}
|
|
|
|
|
|
|
|
Tarkastellaan esimerkkinä arvon $f(7)$ laskemista.
|
|
|
|
Paras ratkaisu on ottaa pohjaksi kohtaan 5
|
|
|
|
päättyvä pisin nouseva alijono $[2,5,7]$
|
|
|
|
ja lisätä sen perään luku $x_7=8$.
|
|
|
|
Tuloksena on alijono $[2,5,7,8]$ ja $f(7)=f(5)+1=4$.
|
|
|
|
|
|
|
|
Suoraviivainen tapa toteuttaa algoritmi on
|
|
|
|
käydä kussakin kohdassa $k$ läpi kaikki kohdat
|
|
|
|
$i=1,2,\ldots,k-1$, joissa voi olla alijonon
|
|
|
|
edellinen luku.
|
|
|
|
Tällaisen algoritmin aikavaativuus on $O(n^2)$.
|
|
|
|
Yllättävää kyllä, algoritmin voi toteuttaa myös
|
|
|
|
ajassa $O(n \log n)$, mutta tämä on vaikeampaa.
|
|
|
|
|
|
|
|
\section{Reitinhaku ruudukossa}
|
|
|
|
|
|
|
|
Seuraava tehtävämme on etsiä reitti
|
|
|
|
$n \times n$ -ruudukon vasemmasta yläkulmasta
|
|
|
|
oikeaan alakulmaan.
|
|
|
|
Jokaisessa ruudussa on luku, ja reitti
|
|
|
|
tulee muodostaa niin, että reittiin kuuluvien
|
|
|
|
lukujen summa on mahdollisimman suuri.
|
|
|
|
Rajoituksena ruudukossa on mahdollista
|
|
|
|
liikkua vain oikealla ja alaspäin.
|
|
|
|
|
|
|
|
Seuraavassa ruudukossa paras reitti
|
|
|
|
on merkitty harmaalla taustalla:
|
|
|
|
\begin{center}
|
|
|
|
\begin{tikzpicture}[scale=.65]
|
|
|
|
\begin{scope}
|
|
|
|
\fill [color=lightgray] (0, 9) rectangle (1, 8);
|
|
|
|
\fill [color=lightgray] (0, 8) rectangle (1, 7);
|
|
|
|
\fill [color=lightgray] (1, 8) rectangle (2, 7);
|
|
|
|
\fill [color=lightgray] (1, 7) rectangle (2, 6);
|
|
|
|
\fill [color=lightgray] (2, 7) rectangle (3, 6);
|
|
|
|
\fill [color=lightgray] (3, 7) rectangle (4, 6);
|
|
|
|
\fill [color=lightgray] (4, 7) rectangle (5, 6);
|
|
|
|
\fill [color=lightgray] (4, 6) rectangle (5, 5);
|
|
|
|
\fill [color=lightgray] (4, 5) rectangle (5, 4);
|
|
|
|
\draw (0, 4) grid (5, 9);
|
|
|
|
\node at (0.5,8.5) {3};
|
|
|
|
\node at (1.5,8.5) {7};
|
|
|
|
\node at (2.5,8.5) {9};
|
|
|
|
\node at (3.5,8.5) {2};
|
|
|
|
\node at (4.5,8.5) {7};
|
|
|
|
\node at (0.5,7.5) {9};
|
|
|
|
\node at (1.5,7.5) {8};
|
|
|
|
\node at (2.5,7.5) {3};
|
|
|
|
\node at (3.5,7.5) {5};
|
|
|
|
\node at (4.5,7.5) {5};
|
|
|
|
\node at (0.5,6.5) {1};
|
|
|
|
\node at (1.5,6.5) {7};
|
|
|
|
\node at (2.5,6.5) {9};
|
|
|
|
\node at (3.5,6.5) {8};
|
|
|
|
\node at (4.5,6.5) {5};
|
|
|
|
\node at (0.5,5.5) {3};
|
|
|
|
\node at (1.5,5.5) {8};
|
|
|
|
\node at (2.5,5.5) {6};
|
|
|
|
\node at (3.5,5.5) {4};
|
|
|
|
\node at (4.5,5.5) {10};
|
|
|
|
\node at (0.5,4.5) {6};
|
|
|
|
\node at (1.5,4.5) {3};
|
|
|
|
\node at (2.5,4.5) {9};
|
|
|
|
\node at (3.5,4.5) {7};
|
|
|
|
\node at (4.5,4.5) {8};
|
|
|
|
\end{scope}
|
|
|
|
\end{tikzpicture}
|
|
|
|
\end{center}
|
|
|
|
Tällä reitillä lukujen summa on $3+9+8+7+9+8+5+10+8=67$,
|
|
|
|
joka on suurin mahdollinen summa vasemmasta yläkulmasta
|
|
|
|
oikeaan alakulmaan.
|
|
|
|
|
|
|
|
Hyvä lähestymistapa tehtävään on laskea
|
|
|
|
kuhunkin ruutuun $(y,x)$ suurin summa
|
|
|
|
reitillä vasemmasta yläkulmasta kyseiseen ruutuun.
|
|
|
|
Merkitään tätä suurinta summaa $f(y,x)$,
|
|
|
|
jolloin $f(n,n)$ on suurin summa
|
|
|
|
reitillä vasemmasta yläkulmasta oikeaan alakulmaan.
|
|
|
|
|
|
|
|
Rekursio syntyy havainnosta,
|
|
|
|
että ruutuun $(y,x)$ saapuvan reitin
|
|
|
|
täytyy tulla joko vasemmalta ruudusta $(y,x-1)$
|
|
|
|
tai ylhäältä ruudusta $(y-1,x)$:
|
|
|
|
\begin{center}
|
|
|
|
\begin{tikzpicture}[scale=.65]
|
|
|
|
\begin{scope}
|
|
|
|
\fill [color=lightgray] (3, 7) rectangle (4, 6);
|
|
|
|
\draw (0, 4) grid (5, 9);
|
|
|
|
|
|
|
|
\node at (2.5,6.5) {$\rightarrow$};
|
|
|
|
\node at (3.5,7.5) {$\downarrow$};
|
|
|
|
|
|
|
|
\end{scope}
|
|
|
|
\end{tikzpicture}
|
|
|
|
\end{center}
|
|
|
|
|
|
|
|
Kun $r(y,x)$
|
|
|
|
on ruudukon luku kohdassa $(y,x)$,
|
|
|
|
rekursion pohjatapaukset ovat seuraavat:
|
|
|
|
|
|
|
|
\[
|
|
|
|
\begin{array}{lcl}
|
|
|
|
f(1,1) & = & r(1,1) \\
|
|
|
|
f(1,x) & = & f(1,x-1)+r(1,x) \\
|
|
|
|
f(y,1) & = & f(y-1,1)+r(y,1)\\
|
|
|
|
\end{array}
|
|
|
|
\]
|
|
|
|
|
|
|
|
Yleisessä tapauksessa valittavana on
|
|
|
|
kaksi reittiä,
|
|
|
|
joista kannattaa valita se,
|
|
|
|
joka tuottaa suuremman summan:
|
|
|
|
\[ f(y,x) = \max(f(y,x-1),f(y-1,x))+r(y,x)\]
|
|
|
|
|
|
|
|
Ratkaisun aikavaativuus on $O(n^2)$, koska jokaisessa
|
|
|
|
ruudussa $f(y,x)$ saadaan laskettua vakioajassa
|
|
|
|
viereisten ruutujen arvoista.
|
|
|
|
|
|
|
|
\section{Repunpakkaus}
|
|
|
|
|
|
|
|
\index{repunpakkaus@repunpakkaus}
|
|
|
|
|
|
|
|
\key{Repunpakkaus} on klassinen ongelma,
|
|
|
|
jossa annettuna on $n$ tavaraa,
|
|
|
|
joiden painot ovat
|
|
|
|
$p_1,p_2,\ldots,p_n$ ja arvot ovat
|
|
|
|
$a_1,a_2,\ldots,a_n$.
|
|
|
|
Tehtävänä on valita reppuun pakattavat tavarat
|
|
|
|
niin, että tavaroiden
|
|
|
|
painojen summa on enintään $x$
|
|
|
|
ja tavaroiden arvojen summa on mahdollisimman suuri.
|
|
|
|
|
|
|
|
\begin{samepage}
|
|
|
|
Esimerkiksi jos tavarat ovat
|
|
|
|
\begin{center}
|
|
|
|
\begin{tabular}{rrr}
|
|
|
|
tavara & paino & arvo \\
|
|
|
|
\hline
|
|
|
|
A & 5 & 1 \\
|
|
|
|
B & 6 & 3 \\
|
|
|
|
C & 8 & 5 \\
|
|
|
|
D & 5 & 3 \\
|
|
|
|
\end{tabular}
|
|
|
|
\end{center}
|
|
|
|
\end{samepage}
|
|
|
|
ja suurin sallittu yhteispaino on 12,
|
|
|
|
niin paras ratkaisu on pakata reppuun tavarat $B$ ja $D$.
|
|
|
|
Niiden yhteispaino $6+5=11$ ei ylitä rajaa 12
|
|
|
|
ja arvojen summa
|
|
|
|
on $3+3=6$, mikä on paras mahdollinen tulos.
|
|
|
|
|
|
|
|
Tämä tehtävä on mahdollista ratkaista kahdella eri
|
|
|
|
tavalla dynaamisella ohjelmoinnilla
|
|
|
|
riippuen siitä, tarkastellaanko ongelmaa
|
|
|
|
maksimointina vai minimointina.
|
|
|
|
Käymme seuraavaksi läpi molemmat ratkaisut.
|
|
|
|
|
|
|
|
\subsubsection{Ratkaisu 1}
|
|
|
|
|
|
|
|
\textit{Maksimointi:} Merkitään $f(k,u)$
|
|
|
|
suurinta mahdollista tavaroiden yhteisarvoa,
|
|
|
|
kun reppuun pakataan jokin osajoukko
|
|
|
|
tavaroista $1 \ldots k$,
|
|
|
|
jossa tavaroiden yhteispaino on $u$.
|
|
|
|
Ratkaisu tehtävään on suurin arvo
|
|
|
|
$f(n,u)$, kun $0 \le u \le x$.
|
|
|
|
Rekursiivinen kaava funktion laskemiseksi on
|
|
|
|
\[f(k,u) = \max(f(k-1,u),f(k-1,u-p_k)+a_k),\]
|
|
|
|
koska kohdassa $k$ oleva tavara joko otetaan tai ei oteta
|
|
|
|
mukaan ratkaisuun.
|
|
|
|
Pohjatapauksina on $f(0,0)=0$ ja $f(0,u)=-\infty$,
|
|
|
|
kun $u \neq 0$. Tämän ratkaisun aikavaativuus on $O(nx)$.
|
|
|
|
|
|
|
|
Esimerkin tilanteessa optimiratkaisu on
|
|
|
|
$f(4,11)=6$, joka muodostuu seuraavan ketjun kautta:
|
|
|
|
\[f(4,11)=f(3,6)+3=f(2,6)+3=f(1,0)+3+3=f(0,0)+3+3=6.\]
|
|
|
|
|
|
|
|
\subsubsection{Ratkaisu 2}
|
|
|
|
|
|
|
|
\textit{Minimointi:} Merkitään $f(k,u)$
|
|
|
|
pienintä mahdollista tavaroiden yhteispainoa,
|
|
|
|
kun reppuun pakataan jokin osajoukko
|
|
|
|
tavaroista $1 \ldots k$,
|
|
|
|
jossa tavaroiden yhteisarvo on $u$.
|
|
|
|
Ratkaisu tehtävään on suurin arvo $u$,
|
|
|
|
jolle pätee $0 \le u \le s$ ja $f(n,u) \le x$,
|
|
|
|
missä $s=\sum_{i=1}^n a_i$.
|
|
|
|
Rekursiivinen kaava funktion laskemiseksi on
|
|
|
|
\[f(k,u) = \min(f(k-1,u),f(k-1,u-a_k)+p_k)\]
|
|
|
|
ratkaisua 1 vastaavasti.
|
|
|
|
Pohjatapauksina on $f(0,0)=0$ ja $f(0,u)=\infty$, kun $u \neq 0$.
|
|
|
|
Tämän ratkaisun aikavaativuus on $O(ns)$.
|
|
|
|
|
|
|
|
Esimerkin tilanteessa optimiratkaisu on
|
|
|
|
$f(4,6)=11$, joka muodostuu seuraavan ketjun kautta:
|
|
|
|
\[f(4,6)=f(3,3)+5=f(2,3)+5=f(1,0)+6+5=f(0,0)+6+5=11.\]
|
|
|
|
|
|
|
|
~\\
|
|
|
|
Kiinnostava seikka on, että eri asiat syötteessä
|
|
|
|
vaikuttavat ratkaisuiden tehokkuuteen.
|
|
|
|
Ratkaisussa 1 tavaroiden painot vaikuttavat tehokkuuteen
|
|
|
|
mutta arvoilla ei ole merkitystä.
|
|
|
|
Ratkaisussa 2 puolestaan tavaroiden arvot vaikuttavat
|
|
|
|
tehokkuuteen mutta painoilla ei ole merkitystä.
|
|
|
|
|
|
|
|
\section{Editointietäisyys}
|
|
|
|
|
|
|
|
\index{editointietxisyys@editointietäisyys}
|
|
|
|
\index{Levenšteinin etäisyys}
|
|
|
|
|
|
|
|
\key{Editointietäisyys} eli
|
|
|
|
\key{Levenšteinin etäisyys}
|
|
|
|
kuvaa, kuinka kaukana kaksi merkkijonoa ovat toisistaan.
|
|
|
|
Se on pienin määrä editointioperaatioita,
|
|
|
|
joilla ensimmäisen merkkijonon saa muutettua toiseksi.
|
|
|
|
Sallitut operaatiot ovat:
|
|
|
|
\begin{itemize}
|
|
|
|
\item merkin lisäys (esim. \texttt{ABC} $\rightarrow$ \texttt{ABCA})
|
|
|
|
\item merkin poisto (esim. \texttt{ABC} $\rightarrow$ \texttt{AC})
|
|
|
|
\item merkin muutos (esim. \texttt{ABC} $\rightarrow$ \texttt{ADC})
|
|
|
|
\end{itemize}
|
|
|
|
|
|
|
|
Esimerkiksi merkkijonojen \texttt{TALO} ja \texttt{PALLO}
|
|
|
|
editointietäisyys on 2, koska voimme tehdä ensin
|
|
|
|
operaation \texttt{TALO} $\rightarrow$ \texttt{TALLO}
|
|
|
|
(merkin lisäys) ja sen jälkeen operaation
|
|
|
|
\texttt{TALLO} $\rightarrow$ \texttt{PALLO}
|
|
|
|
(merkin muutos).
|
|
|
|
Tämä on pienin mahdollinen määrä operaatioita, koska
|
|
|
|
selvästikään yksi operaatio ei riitä.
|
|
|
|
|
|
|
|
Oletetaan, että annettuna on merkkijonot
|
|
|
|
\texttt{x} (pituus $n$ merkkiä) ja
|
|
|
|
\texttt{y} (pituus $m$ merkkiä),
|
|
|
|
ja haluamme laskea niiden editointietäisyyden.
|
|
|
|
Tämä onnistuu tehokkaasti dynaamisella
|
|
|
|
ohjelmoinnilla ajassa $O(nm)$.
|
|
|
|
Merkitään funktiolla $f(a,b)$
|
|
|
|
editointietäisyyttä \texttt{x}:n $a$
|
|
|
|
ensimmäisen merkin sekä
|
|
|
|
\texttt{y}:n $b$:n ensimmäisen merkin välillä.
|
|
|
|
Tätä funktiota käyttäen
|
|
|
|
merkkijonojen
|
|
|
|
\texttt{x} ja \texttt{y} editointietäisyys
|
|
|
|
on $f(n,m)$, ja funktio kertoo myös tarvittavat
|
|
|
|
editointioperaatiot.
|
|
|
|
|
|
|
|
Funktion pohjatapaukset ovat
|
|
|
|
\[
|
|
|
|
\begin{array}{lcl}
|
|
|
|
f(0,b) & = & b \\
|
|
|
|
f(a,0) & = & a \\
|
|
|
|
\end{array}
|
|
|
|
\]
|
|
|
|
ja yleisessä tapauksessa pätee kaava
|
|
|
|
\[ f(a,b) = \min(f(a,b-1)+1,f(a-1,b)+1,f(a-1,b-1)+c),\]
|
|
|
|
missä $c=0$, jos \texttt{x}:n merkki $a$
|
|
|
|
ja \texttt{y}:n merkki $b$ ovat samat,
|
|
|
|
ja muussa tapauksessa $c=1$.
|
|
|
|
Kaava käy läpi mahdollisuudet lyhentää merkkijonoja:
|
|
|
|
\begin{itemize}
|
|
|
|
\item $f(a,b-1)$ tarkoittaa, että $x$:ään lisätään merkki
|
|
|
|
\item $f(a-1,b)$ tarkoittaa, että $x$:stä poistetaan merkki
|
|
|
|
\item $f(a-1,b-1)$ tarkoittaa, että $x$:ssä ja $y$:ssä on
|
|
|
|
sama merkki ($c=0$) tai $x$:n merkki muutetaan $y$:n merkiksi ($c=1$)
|
|
|
|
\end{itemize}
|
|
|
|
Seuraava taulukko sisältää funktion $f$ arvot
|
|
|
|
esimerkin tapauksessa:
|
|
|
|
\begin{center}
|
|
|
|
\begin{tikzpicture}[scale=.65]
|
|
|
|
\begin{scope}
|
|
|
|
%\fill [color=lightgray] (5, -3) rectangle (6, -4);
|
|
|
|
\draw (1, -1) grid (7, -6);
|
|
|
|
|
|
|
|
\node at (0.5,-2.5) {\texttt{T}};
|
|
|
|
\node at (0.5,-3.5) {\texttt{A}};
|
|
|
|
\node at (0.5,-4.5) {\texttt{L}};
|
|
|
|
\node at (0.5,-5.5) {\texttt{O}};
|
|
|
|
|
|
|
|
\node at (2.5,-0.5) {\texttt{P}};
|
|
|
|
\node at (3.5,-0.5) {\texttt{A}};
|
|
|
|
\node at (4.5,-0.5) {\texttt{L}};
|
|
|
|
\node at (5.5,-0.5) {\texttt{L}};
|
|
|
|
\node at (6.5,-0.5) {\texttt{O}};
|
|
|
|
|
|
|
|
\node at (1.5,-1.5) {$0$};
|
|
|
|
\node at (1.5,-2.5) {$1$};
|
|
|
|
\node at (1.5,-3.5) {$2$};
|
|
|
|
\node at (1.5,-4.5) {$3$};
|
|
|
|
\node at (1.5,-5.5) {$4$};
|
|
|
|
\node at (2.5,-1.5) {$1$};
|
|
|
|
\node at (2.5,-2.5) {$1$};
|
|
|
|
\node at (2.5,-3.5) {$2$};
|
|
|
|
\node at (2.5,-4.5) {$3$};
|
|
|
|
\node at (2.5,-5.5) {$4$};
|
|
|
|
\node at (3.5,-1.5) {$2$};
|
|
|
|
\node at (3.5,-2.5) {$2$};
|
|
|
|
\node at (3.5,-3.5) {$1$};
|
|
|
|
\node at (3.5,-4.5) {$2$};
|
|
|
|
\node at (3.5,-5.5) {$3$};
|
|
|
|
\node at (4.5,-1.5) {$3$};
|
|
|
|
\node at (4.5,-2.5) {$3$};
|
|
|
|
\node at (4.5,-3.5) {$2$};
|
|
|
|
\node at (4.5,-4.5) {$1$};
|
|
|
|
\node at (4.5,-5.5) {$2$};
|
|
|
|
\node at (5.5,-1.5) {$4$};
|
|
|
|
\node at (5.5,-2.5) {$4$};
|
|
|
|
\node at (5.5,-3.5) {$3$};
|
|
|
|
\node at (5.5,-4.5) {$2$};
|
|
|
|
\node at (5.5,-5.5) {$2$};
|
|
|
|
\node at (6.5,-1.5) {$5$};
|
|
|
|
\node at (6.5,-2.5) {$5$};
|
|
|
|
\node at (6.5,-3.5) {$4$};
|
|
|
|
\node at (6.5,-4.5) {$3$};
|
|
|
|
\node at (6.5,-5.5) {$2$};
|
|
|
|
\end{scope}
|
|
|
|
\end{tikzpicture}
|
|
|
|
\end{center}
|
|
|
|
|
|
|
|
Taulukon oikean alanurkan ruutu
|
|
|
|
kertoo, että merkkijonojen \texttt{TALO}
|
|
|
|
ja \texttt{PALLO} editointietäisyys on 2.
|
|
|
|
Taulukosta pystyy myös
|
|
|
|
lukemaan, miten pienimmän editointietäisyyden
|
|
|
|
voi saavuttaa.
|
|
|
|
Tässä tapauksessa polku on seuraava:
|
|
|
|
|
|
|
|
\begin{center}
|
|
|
|
\begin{tikzpicture}[scale=.65]
|
|
|
|
\begin{scope}
|
|
|
|
\draw (1, -1) grid (7, -6);
|
|
|
|
|
|
|
|
\node at (0.5,-2.5) {\texttt{T}};
|
|
|
|
\node at (0.5,-3.5) {\texttt{A}};
|
|
|
|
\node at (0.5,-4.5) {\texttt{L}};
|
|
|
|
\node at (0.5,-5.5) {\texttt{O}};
|
|
|
|
|
|
|
|
\node at (2.5,-0.5) {\texttt{P}};
|
|
|
|
\node at (3.5,-0.5) {\texttt{A}};
|
|
|
|
\node at (4.5,-0.5) {\texttt{L}};
|
|
|
|
\node at (5.5,-0.5) {\texttt{L}};
|
|
|
|
\node at (6.5,-0.5) {\texttt{O}};
|
|
|
|
|
|
|
|
\node at (1.5,-1.5) {$0$};
|
|
|
|
\node at (1.5,-2.5) {$1$};
|
|
|
|
\node at (1.5,-3.5) {$2$};
|
|
|
|
\node at (1.5,-4.5) {$3$};
|
|
|
|
\node at (1.5,-5.5) {$4$};
|
|
|
|
\node at (2.5,-1.5) {$1$};
|
|
|
|
\node at (2.5,-2.5) {$1$};
|
|
|
|
\node at (2.5,-3.5) {$2$};
|
|
|
|
\node at (2.5,-4.5) {$3$};
|
|
|
|
\node at (2.5,-5.5) {$4$};
|
|
|
|
\node at (3.5,-1.5) {$2$};
|
|
|
|
\node at (3.5,-2.5) {$2$};
|
|
|
|
\node at (3.5,-3.5) {$1$};
|
|
|
|
\node at (3.5,-4.5) {$2$};
|
|
|
|
\node at (3.5,-5.5) {$3$};
|
|
|
|
\node at (4.5,-1.5) {$3$};
|
|
|
|
\node at (4.5,-2.5) {$3$};
|
|
|
|
\node at (4.5,-3.5) {$2$};
|
|
|
|
\node at (4.5,-4.5) {$1$};
|
|
|
|
\node at (4.5,-5.5) {$2$};
|
|
|
|
\node at (5.5,-1.5) {$4$};
|
|
|
|
\node at (5.5,-2.5) {$4$};
|
|
|
|
\node at (5.5,-3.5) {$3$};
|
|
|
|
\node at (5.5,-4.5) {$2$};
|
|
|
|
\node at (5.5,-5.5) {$2$};
|
|
|
|
\node at (6.5,-1.5) {$5$};
|
|
|
|
\node at (6.5,-2.5) {$5$};
|
|
|
|
\node at (6.5,-3.5) {$4$};
|
|
|
|
\node at (6.5,-4.5) {$3$};
|
|
|
|
\node at (6.5,-5.5) {$2$};
|
|
|
|
|
|
|
|
\path[draw=red,thick,-,line width=2pt] (6.5,-5.5) -- (5.5,-4.5);
|
|
|
|
\path[draw=red,thick,-,line width=2pt] (5.5,-4.5) -- (4.5,-4.5);
|
|
|
|
\path[draw=red,thick,->,line width=2pt] (4.5,-4.5) -- (1.5,-1.5);
|
|
|
|
\end{scope}
|
|
|
|
\end{tikzpicture}
|
|
|
|
\end{center}
|
|
|
|
|
|
|
|
Merkkijonojen \texttt{PALLO} ja \texttt{TALO} viimeinen merkki on sama,
|
|
|
|
joten niiden editointietäisyys on sama kuin
|
|
|
|
merkkijonojen \texttt{PALL} ja \texttt{TAL}.
|
|
|
|
Nyt voidaan poistaa viimeinen \texttt{L} merkkijonosta \texttt{PAL},
|
|
|
|
mistä tulee yksi operaatio.
|
|
|
|
Editointietäisyys on siis yhden suurempi
|
|
|
|
kuin merkkijonoilla \texttt{PAL} ja \texttt{TAL}, jne.
|
|
|
|
|
|
|
|
\section{Laatoitukset}
|
|
|
|
|
|
|
|
Joskus dynaamisen ohjelmoinnin tila on monimutkaisempi kuin
|
|
|
|
kiinteä yhdistelmä lukuja.
|
|
|
|
Tarkastelemme lopuksi tehtävää, jossa
|
|
|
|
laskettavana on, monellako tavalla
|
|
|
|
kokoa $1 \times 2$ ja $2 \times 1$ olevilla laatoilla
|
|
|
|
voi täyttää $n \times m$ -kokoisen ruudukon.
|
|
|
|
Esimerkiksi ruudukolle kokoa $4 \times 7$
|
|
|
|
yksi mahdollinen ratkaisu on
|
|
|
|
\begin{center}
|
|
|
|
\begin{tikzpicture}[scale=.65]
|
|
|
|
\draw (0,0) grid (7,4);
|
|
|
|
\draw[fill=gray] (0+0.2,0+0.2) rectangle (2-0.2,1-0.2);
|
|
|
|
\draw[fill=gray] (2+0.2,0+0.2) rectangle (4-0.2,1-0.2);
|
|
|
|
\draw[fill=gray] (4+0.2,0+0.2) rectangle (6-0.2,1-0.2);
|
|
|
|
\draw[fill=gray] (0+0.2,1+0.2) rectangle (2-0.2,2-0.2);
|
|
|
|
\draw[fill=gray] (2+0.2,1+0.2) rectangle (4-0.2,2-0.2);
|
|
|
|
\draw[fill=gray] (1+0.2,2+0.2) rectangle (3-0.2,3-0.2);
|
|
|
|
\draw[fill=gray] (1+0.2,3+0.2) rectangle (3-0.2,4-0.2);
|
|
|
|
\draw[fill=gray] (4+0.2,3+0.2) rectangle (6-0.2,4-0.2);
|
|
|
|
|
|
|
|
\draw[fill=gray] (0+0.2,2+0.2) rectangle (1-0.2,4-0.2);
|
|
|
|
\draw[fill=gray] (3+0.2,2+0.2) rectangle (4-0.2,4-0.2);
|
|
|
|
\draw[fill=gray] (6+0.2,2+0.2) rectangle (7-0.2,4-0.2);
|
|
|
|
\draw[fill=gray] (4+0.2,1+0.2) rectangle (5-0.2,3-0.2);
|
|
|
|
\draw[fill=gray] (5+0.2,1+0.2) rectangle (6-0.2,3-0.2);
|
|
|
|
\draw[fill=gray] (6+0.2,0+0.2) rectangle (7-0.2,2-0.2);
|
|
|
|
|
|
|
|
\end{tikzpicture}
|
|
|
|
\end{center}
|
|
|
|
ja ratkaisujen yhteismäärä on 781.
|
|
|
|
|
|
|
|
Tehtävän voi ratkaista dynaamisella ohjelmoinnilla
|
|
|
|
käymällä ruudukkoa läpi rivi riviltä.
|
|
|
|
Jokainen ratkaisun rivi pelkistyy merkkijonoksi,
|
|
|
|
jossa on $m$ merkkiä joukosta $\{\sqcap, \sqcup, \sqsubset, \sqsupset \}$.
|
|
|
|
Esimerkiksi yllä olevassa ratkaisussa on 4 riviä,
|
|
|
|
jotka vastaavat merkkijonoja
|
|
|
|
\begin{itemize}
|
|
|
|
\item
|
|
|
|
$\sqcap \sqsubset \sqsupset \sqcap \sqsubset \sqsupset \sqcap$,
|
|
|
|
\item
|
|
|
|
$\sqcup \sqsubset \sqsupset \sqcup \sqcap \sqcap \sqcup$,
|
|
|
|
\item
|
|
|
|
$\sqsubset \sqsupset \sqsubset \sqsupset \sqcup \sqcup \sqcap$ ja
|
|
|
|
\item
|
|
|
|
$\sqsubset \sqsupset \sqsubset \sqsupset \sqsubset \sqsupset \sqcup$.
|
|
|
|
\end{itemize}
|
|
|
|
|
|
|
|
Tehtävään sopiva rekursiivinen funktio on $f(k,x)$,
|
|
|
|
joka laskee, montako tapaa on muodostaa ratkaisu
|
|
|
|
ruudukon riveille $1 \ldots k$ niin,
|
|
|
|
että riviä $k$ vastaa merkkijono $x$.
|
|
|
|
Dynaaminen ohjelmointi on mahdollista,
|
|
|
|
koska jokaisen rivin sisältöä
|
|
|
|
rajoittaa vain edellisen rivin sisältö.
|
|
|
|
|
|
|
|
Riveistä muodostuva ratkaisu on kelvollinen,
|
|
|
|
jos rivillä 1 ei ole merkkiä $\sqcup$,
|
|
|
|
rivillä $n$ ei ole merkkiä $\sqcap$
|
|
|
|
ja kaikki peräkkäiset rivit ovat \emph{yhteensopivat}.
|
|
|
|
Esimerkiksi rivit
|
|
|
|
$\sqcup \sqsubset \sqsupset \sqcup \sqcap \sqcap \sqcup$ ja
|
|
|
|
$\sqsubset \sqsupset \sqsubset \sqsupset \sqcup \sqcup \sqcap$
|
|
|
|
ovat yhteensopivat,
|
|
|
|
kun taas rivit
|
|
|
|
$\sqcap \sqsubset \sqsupset \sqcap \sqsubset \sqsupset \sqcap$ ja
|
|
|
|
$\sqsubset \sqsupset \sqsubset \sqsupset \sqsubset \sqsupset \sqcup$
|
|
|
|
eivät ole yhteensopivat.
|
|
|
|
|
|
|
|
Koska rivillä on $m$ merkkiä ja jokaiselle merkille on 4
|
|
|
|
vaihtoehtoa, erilaisia rivejä on korkeintaan $4^m$.
|
|
|
|
Niinpä ratkaisun aikavaativuus on $O(n 4^{2m})$,
|
|
|
|
koska joka rivillä käydään läpi $O(4^m)$
|
|
|
|
vaihtoehtoa rivin sisällölle
|
|
|
|
ja jokaista vaihtoehtoa kohden on $O(4^m)$
|
|
|
|
vaihtoehtoa edellisen rivin sisällölle.
|
|
|
|
Käytännössä ruudukko kannattaa kääntää niin
|
|
|
|
päin, että pienempi sivun pituus on $m$:n roolissa,
|
|
|
|
koska $m$:n suuruus on ratkaiseva ajankäytön kannalta.
|
|
|
|
|
|
|
|
Ratkaisua on mahdollista tehostaa parantamalla rivien esitystapaa merkkijonoina.
|
|
|
|
Osoittautuu, että ainoa seuraavalla rivillä tarvittava tieto on,
|
|
|
|
missä kohdissa riviltä lähtee laattoja alaspäin.
|
|
|
|
Niinpä rivin voikin tallentaa käyttäen vain merkkejä
|
|
|
|
$\sqcap$ ja $\Box$, missä $\Box$ kokoaa yhteen vanhat merkit
|
|
|
|
$\sqcup$, $\sqsubset$ ja $\sqsupset$.
|
|
|
|
Tällöin erilaisia rivejä on vain $2^m$
|
|
|
|
ja aikavaativuudeksi tulee $O(n 2^{2m})$.
|
|
|
|
|
|
|
|
Mainittakoon lopuksi, että laatoitusten määrän laskemiseen
|
|
|
|
on myös yllättävä suora kaava
|
|
|
|
\[ \prod_{a=1}^{\lceil n/2 \rceil} \prod_{b=1}^{\lceil m/2 \rceil} 4 \cdot (\cos^2 \frac{\pi a}{n + 1} + \cos^2 \frac{\pi b}{m+1}).\]
|
|
|
|
Tämä kaava on sinänsä hyvin tehokas,
|
|
|
|
koska se laskee laatoitusten määrän ajassa $O(nm)$,
|
|
|
|
mutta käytännön ongelma kaavan käyttämisessä
|
|
|
|
on, kuinka tallentaa välitulokset riittävän tarkkoina lukuina.
|
|
|
|
|
|
|
|
|