Two pointers method

This commit is contained in:
Antti H S Laaksonen 2017-01-03 01:49:59 +02:00
parent 1d56f95355
commit 7f443cd64e
1 changed files with 111 additions and 132 deletions

View File

@ -1,41 +1,42 @@
\chapter{Amortized analysis}
\index{tasoitettu analyysi@tasoitettu analyysi}
\index{amortized analysis}
Monen algoritmin aikavaativuuden pystyy laskemaan
suoraan katsomalla algoritmin rakennetta:
mitä silmukoita algoritmissa on ja miten monta
kertaa niitä suoritetaan.
Joskus kuitenkaan näin suoraviivainen analyysi ei
riitä antamaan todellista kuvaa algoritmin tehokkuudesta.
Often the time complexity of an algorithm
is easy to analyze by looking at the structure
of the algorithm:
what loops there are and how many times
they are performed.
However, sometimes a straightforward analysis
doesn't give a true picture of the efficiency of the algorithm.
\key{Tasoitettu analyysi} soveltuu sellaisten
algoritmien analyysiin, joiden osana on jokin operaatio,
jonka ajankäyttö vaihtelee.
Ideana on tarkastella yksittäisen operaation
sijasta kaikkia operaatioita algoritmin
aikana ja laskea niiden ajankäytölle yhteinen raja.
\key{Amortized analysis} can be used for analyzing
an algorithm that contains an operation whose
time complexity varies.
The idea is to consider all such operations during the
execution of the algorithm instead of a single operation,
and estimate the total time complexity of the operations.
\section{Kahden osoittimen tekniikka}
\section{Two pointers method}
\index{kahden osoittimen tekniikka}
\index{two pointers method}
\key{Kahden osoittimen tekniikka} on taulukon käsittelyssä
käytettävä menetelmä, jossa taulukkoa käydään läpi
kahden osoittimen avulla.
Molemmat osoittimet liikkuvat algoritmin aikana,
mutta rajoituksena on, että ne voivat liikkua vain
yhteen suuntaan, mikä takaa, että algoritmi toimii tehokkaasti.
In the \key{two pointers method},
two pointers iterate through the elements in an array.
Both pointers can move during the algorithm,
but the restriction is that each pointer can move
to only one direction.
This ensures that the algorithm works efficiently.
Tutustumme seuraavaksi kahden osoittimen tekniikkaan
kahden esimerkkitehtävän kautta.
We will next discuss two problems that can be solved
using the two pointers method.
\subsubsection{Alitaulukon summa}
\subsubsection{Subarray sum}
Annettuna on taulukko, jossa on $n$ positiivista kokonaislukua.
Tehtävänä on selvittää, onko taulukossa alitaulukkoa,
jossa lukujen summa on $x$.
Esimerkiksi taulukossa
Given an array that contains $n$ positive integers,
our task is to find out if there is a subarray
where the sum of the elements is $x$.
For example, the array
\begin{center}
\begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (8,1);
@ -60,7 +61,7 @@ Esimerkiksi taulukossa
\node at (7.5,1.4) {$8$};
\end{tikzpicture}
\end{center}
on alitaulukko, jossa lukujen summa on 8:
contains a subarray with sum 8:
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (2,0) rectangle (5,1);
@ -87,17 +88,18 @@ on alitaulukko, jossa lukujen summa on 8:
\end{tikzpicture}
\end{center}
Osoittautuu, että tämän tehtävän voi ratkaista
ajassa $O(n)$ kahden osoittimen tekniikalla.
Ideana on käydä taulukkoa läpi kahden osoittimen
avulla, jotka rajaavat välin taulukosta.
Joka vuorolla vasen osoitin liikkuu
yhden askeleen eteenpäin ja oikea osoitin
liikkuu niin kauan eteenpäin kuin summa on enintään $x$.
Jos välin summaksi tulee tarkalleen $x$, ratkaisu on löytynyt.
It turns out that the problem can be solved in
$O(n)$ time using the two pointers method.
The idea is to iterate through the array
using two pointers that define a range in the array.
On each turn, the left pointer moves one step
forward, and the right pointer moves forward
as long as the sum is at most $x$.
If the sum of the range becomes exactly $x$,
we have found a solution.
Tarkastellaan esimerkkinä algoritmin toimintaa
seuraavassa taulukossa, kun tavoitteena on muodostaa summa $x=8$:
As an example, we consider the following array
with target sum $x=8$:
\begin{center}
\begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (8,1);
@ -123,10 +125,10 @@ seuraavassa taulukossa, kun tavoitteena on muodostaa summa $x=8$:
\end{tikzpicture}
\end{center}
Aluksi osoittimet rajaavat taulukosta välin,
jonka summa on $1+3+2=6$.
Väli ei voi olla tätä suurempi,
koska seuraava luku 5 veisi summan yli $x$:n.
First, the pointers define a range with sum $1+3+2=6$.
The range can't be larger
because the next number 5 would make the sum
larger than $x$.
\begin{center}
\begin{tikzpicture}[scale=0.7]
@ -157,9 +159,9 @@ koska seuraava luku 5 veisi summan yli $x$:n.
\end{tikzpicture}
\end{center}
Seuraavaksi vasen osoitin siirtyy askeleen eteenpäin.
Oikea osoitin säilyy paikallaan, koska muuten
summa kasvaisi liian suureksi.
After this, the left pointer moves one step forward.
The right pointer doesn't move because otherwise
the sum would become too large.
\begin{center}
\begin{tikzpicture}[scale=0.7]
@ -190,10 +192,11 @@ summa kasvaisi liian suureksi.
\end{tikzpicture}
\end{center}
Vasen osoitin siirtyy taas askeleen eteenpäin
ja tällä kertaa oikea osoitin siirtyy kolme askelta
eteenpäin. Muodostuu summa $2+5+1=8$ eli taulukosta
on löytynyt väli, jonka lukujen summa on $x$.
Again, the left pointer moves one step forward,
and this time the right pointer moves three
steps forward.
The sum is $2+5+1=8$, so we have found a subarray
where the sum of the elements is $x$.
\begin{center}
\begin{tikzpicture}[scale=0.7]
@ -224,67 +227,42 @@ on löytynyt väli, jonka lukujen summa on $x$.
\end{tikzpicture}
\end{center}
% Algoritmin toteutus näyttää seuraavalta:
%
% \begin{lstlisting}
% int s = 0, b = 0;
% for (int a = 1; a <= n; a++) {
% while (b<n && s+t[b+1] <= x) {
% b++;
% s += t[b];
% }
% if (s == x) {
% // ratkaisu löytyi
% }
% s -= t[a];
% }
% \end{lstlisting}
%
% Muuttujat $a$ ja $b$ sisältävät vasemman ja oikean
% osoittimen kohdan.
% Muuttuja $s$ taas laskee lukujen summan välillä.
% Joka askeleella $a$ liikkuu askeleen eteenpäin
% ja $b$ liikkuu niin kauan kuin summa on enintään $x$.
The time complexity of the algorithm depends on
the number of steps the right pointer moves.
There is no upper bound how many steps the
pointer can move on a single turn.
However, the pointer moves \emph{a total of}
$O(n)$ steps during the algorithm
because it only moves forward.
Algoritmin aikavaativuus riippuu siitä,
kauanko oikean osoittimen liikkuminen vie aikaa.
Tämä vaihtelee, koska oikea osoitin voi liikkua
minkä tahansa matkan eteenpäin taulukossa.
Kuitenkin oikea osoitin liikkuu \textit{yhteensä}
$O(n)$ askelta algoritmin aikana, koska se voi
liikkua vain eteenpäin.
Since both the left and the right pointer
move $O(n)$ steps during the algorithm,
the time complexity is $O(n)$.
Koska sekä vasen että oikea osoitin liikkuvat
$O(n)$ askelta algoritmin aikana,
algoritmin aikavaativuus on $O(n)$.
\subsubsection{Sum of two numbers}
\subsubsection{Kahden luvun summa}
\index{2SUM problem}
\index{2SUM-ongelma}
Given an array of $n$ integers and an integer $x$,
our task is to find two numbers in array
whose sum is $x$ or report that there are no such numbers.
This problem is known as the \key{2SUM} problem,
and it can be solved efficiently using the
two pointers method.
Annettuna on taulukko, jossa on $n$ kokonaislukua,
sekä kokonaisluku $x$.
Tehtävänä on etsiä taulukosta kaksi lukua,
joiden summa on $x$, tai todeta,
että tämä ei ole mahdollista.
Tämä ongelma tunnetaan tunnetaan nimellä
\key{2SUM} ja se ratkeaa tehokkaasti
kahden osoittimen tekniikalla.
First, we sort the numbers in the array in
increasing order.
After this, we iterate through the array using
two pointers that begin at both ends of the array.
The left pointer begins from the first element
and moves one step forward on each turn.
The right pointer begins from the last element
and always moves backward until the sum of the range
defined by the pointers is at most $x$.
If the sum is exactly $x$, we have found a solution.
Taulukon luvut järjestetään ensin
pienimmästä suurimpaan, minkä jälkeen
taulukkoa aletaan käydä läpi kahdella osoittimella,
jotka lähtevät liikkelle taulukon molemmista päistä.
Vasen osoitin aloittaa taulukon alusta ja
liikkuu joka vaiheessa askeleen eteenpäin.
Oikea osoitin taas aloittaa taulukon lopusta
ja peruuttaa vuorollaan taaksepäin, kunnes osoitinten
määrittämän välin lukujen summa on enintään $x$.
Jos summa on tarkalleen $x$, ratkaisu on löytynyt.
Tarkastellaan algoritmin toimintaa
seuraavassa taulukossa, kun tavoitteena on muodostaa
summa $x=12$:
For example, consider the following array when
our task is to find two elements whose sum is $x=12$:
\begin{center}
\begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (8,1);
@ -310,9 +288,10 @@ summa $x=12$:
\end{tikzpicture}
\end{center}
Seuraavassa on algoritmin aloitustilanne.
Lukujen summa on $1+10=11$, joka on pienempi
kuin $x$:n arvo.
The initial positions of the pointers
are as follows.
The sum of the numbers is $1+10=11$
that is smaller than $x$.
\begin{center}
\begin{tikzpicture}[scale=0.7]
@ -344,9 +323,9 @@ kuin $x$:n arvo.
\end{tikzpicture}
\end{center}
Seuraavaksi vasen osoitin liikkuu askeleen eteenpäin.
Oikea osoitin peruuttaa kolme askelta, minkä jälkeen
summana on $4+7=11$.
Then the left pointer moves one step forward.
The right pointer moves three steps backward,
and the sum becomes $4+7=11$.
\begin{center}
\begin{tikzpicture}[scale=0.7]
@ -378,8 +357,9 @@ summana on $4+7=11$.
\end{tikzpicture}
\end{center}
Sitten vasen osoitin siirtyy jälleen askeleen eteenpäin.
Oikea osoitin pysyy paikallaan ja ratkaisu $5+7=12$ on löytynyt.
After this, the left pointer moves one step forward again.
The right pointer doesn't move, and the solution
$5+7=12$ has been found.
\begin{center}
\begin{tikzpicture}[scale=0.7]
@ -411,28 +391,27 @@ Oikea osoitin pysyy paikallaan ja ratkaisu $5+7=12$ on löytynyt.
\end{tikzpicture}
\end{center}
Algoritmin alussa taulukon järjestäminen vie
aikaa $O(n \log n)$.
Tämän jälkeen vasen osoitin liikkuu $O(n)$ askelta
eteenpäin ja oikea osoitin liikkuu $O(n)$ askelta
taaksepäin, mihin kuluu aikaa $O(n)$.
Algoritmin kokonaisaikavaativuus on siis $O(n \log n)$.
At the beginning of the algorithm,
the sorting takes $O(n \log n)$ time.
After this, the left pointer moves $O(n)$ steps
forward, and the right pointer moves $O(n)$ steps
backward. Thus, the total time complexity
of the algorithm is $O(n \log n)$.
Huomaa, että tehtävän voi ratkaista myös
toisella tavalla ajassa
$O(n \log n)$ binäärihaun avulla.
Tässä ratkaisussa jokaiselle taulukon luvulle
etsitään binäärihaulla toista lukua niin,
että lukujen summa olisi yhteensä $x$.
Binäärihaku suoritetaan $n$ kertaa ja
jokainen binäärihaku vie aikaa $O(\log n)$.
Note that it is possible to solve
in another way in $O(n \log n)$ time using binary search.
In this solution, we iterate through the array
and for each number, we try to find another
number such that the sum is $x$.
This can be done by performing $n$ binary searches,
and each search takes $O(\log n)$ time.
\index{3SUM-ongelma}
Hieman vaikeampi ongelma on \key{3SUM},
jossa taulukosta tuleekin etsiä kolme lukua,
joiden summa on $x$.
Tämä ongelma on mahdollista ratkaista ajassa $O(n^2)$.
Keksitkö, miten se tapahtuu?
A somewhat more difficult problem is
the \key{3SUM} problem where our task is
to find \emph{three} numbers whose sum is $x$.
This problem can be solved in $O(n^2)$ time.
Can you see how it is possible?
\section{Lähin pienempi edeltäjä}