From 68654e6ce5206684a8b7a00bad4cab32a26be4d4 Mon Sep 17 00:00:00 2001
From: Florian Unger <florian.unger@posteo.net>
Date: Thu, 14 Jan 2021 00:06:51 +0100
Subject: [PATCH] =?UTF-8?q?amortisierte=20Analyse=20von=20dynamische=20Arr?=
 =?UTF-8?q?ays=20=C3=BCberarbeitet?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 301_dynamische_Arrays.tex | 132 +++++++++++++++++++++++++-------------
 1 file changed, 87 insertions(+), 45 deletions(-)

diff --git a/301_dynamische_Arrays.tex b/301_dynamische_Arrays.tex
index a4e2586..1588c78 100644
--- a/301_dynamische_Arrays.tex
+++ b/301_dynamische_Arrays.tex
@@ -58,7 +58,7 @@ Danach tritt der erste Fall ein.
   \caption{\texttt{add}($\mathcal{DA}, x$)}
   \KwIn{A dynamic array $\mathcal{DA}$ and the element $x$ to be added} 
   \KwOut{$\mathcal{DA}$ with $x$ appended at the end}
-  \eIf{$n \leq n_\text{cap}$}{
+  \eIf{$n < n_\text{cap}$}{
     $\mathcal{A}[n] \leftarrow x$ \;
     \KwRet $(\mathcal{A}, n+1)$ \;
   }{
@@ -85,14 +85,14 @@ Viertel belegt ist. Die Verkleinerungsoperation braucht wieder $\mathcal{O}(n)$
   \caption{\texttt{delete}($\mathcal{DA}$)}
   \KwIn{A dynamic array $\mathcal{DA}$} 
   \KwOut{$\mathcal{DA}$ without the last element}
-  \eIf{$n \geq \frac{n_\text{cap}}{4}$}{
+  \eIf{$n > \frac{n_\text{cap}}{4}$}{
     \KwRet $(\mathcal{A}, n-1)$ \;
   }{
     $\mathcal{A}_\text{old} \leftarrow \mathcal{A}$ \;
     $\mathcal{A}$ = new Array of size $\frac{n_\text{cap}}{2}$ \;
     $\mathcal{A}[0,\cdots,n-1] \leftarrow \mathcal{A}_\text{old}[0,\cdots,n-1]$ \;
     $\texttt{free}(\mathcal{A}_\text{old})$ \;
-    \KwRet $\texttt{remove}((\mathcal{A}, n))$ \;
+    \KwRet $\texttt{delete}((\mathcal{A}, n))$ \;
   }
 \end{algorithm}
 
@@ -120,11 +120,23 @@ Damit kommen wir bei genauerer Betrachtung auf einen amortisierten Aufwand von $
 
 Im Unterschied zur average-case-Analyse haben wir bei einer amortisierten Analyse also \emph{garantiert}, dass $k$ subsequente Operationen nicht jedes
 mal im worst case landen.
+\begin{definition}[Amortisierte Laufzeit]
+  Seien $o_1, o_2, \dots$ endlich viele verschiedene Funktionen. Der \emph{amortisierte} Aufwand von Funktionenfolgen
+  $f_\bullet \in \{o_1, o_2, \dots\}^\mathbb{N}$ der Länge $n$ wird definiert als:
+  \[
+    T_{\text{amo}}(n) = \frac{\max(\{\sum_{i=1}^n f_i | f_\bullet \in \{o_1, o_2, \dots\}^\mathbb{N})}{n}.
+  \]
+\end{definition}
+
+Im Kontext der dynamische Arrays haben wir zwei Operationen, $o_1 = \texttt{add}(\mathcal{DA}, x)$ und
+$o_2 = \texttt{delete}(\mathcal{DA})$, wobei der hinzugefügte Wert $x$ keinerlei Auswirkungen auf die Laufzeit hat.
 
 \subsubsection{Aggregierte Analyse}
-Zuerst betrachten wir die Einzeloperationen $\texttt{add}$ und $\texttt{delete}$ separat.
+Zuerst betrachten wir die Einzeloperationen $\texttt{add}$ und $\texttt{delete}$ separat. Das entspricht also
+Funktionsfolgen $f_\bullet = (\texttt{add}, \texttt{add}, \dots)$ bzw $f_\bullet = (\texttt{delete}, \texttt{delete},
+\dots)$.
 
-Wir weisen der Zeile [1] bis [3] von $\texttt{add}$ 
+Der Einfachheit halber weisen wir den Zeilen [1] bis [3] von $\texttt{add}$ 
 den Aufwand $f(n) = 1$ und den Zeilen [4] bis [8] den Aufwand
 \[
   g(n) = 
@@ -163,65 +175,95 @@ Wir wollen nun untersuchen, was passiert, wenn wir zwischen Löschen und Einfüg
 
 In der aggregierten Analyse von \texttt{add} haben wir einfach $k$ Operationen von $\texttt{add}$ subsequent ausgeführt,
 den kumulativen Aufwand berechnet und dann durch $k$ geteilt, um den Aufwand pro Schritt zu berechnen. Das ging einfach
-und direkt, da es nur eine Operationenfolge gab (\texttt{add}, \texttt{add,} \dots, \texttt{add}).
+und direkt, da es nur eine Operationenfolge $f_\bullet$ gab: $(\texttt{add}, \texttt{add,} \dots, \texttt{add})$.
 Nun sind wir aber an gemischten Operationenfolgenden von \texttt{add} und \texttt{delete} interessiert. Für $k=3$ gibt
 es schon $8$ mögliche Operationenfolgen: (\texttt{add}, \texttt{add}, \texttt{add}), (\texttt{add}, \texttt{add},
 \texttt{delete}), \dots. Über all diese Möglichkeiten zu gehen und den maximalen durchschnittlichen Wert einer Sequenz
 zu bestimmen, wäre sehr mühselig.
 
-Wir gehen daher einen anderen Weg, die Accountingmethode. Hier wird der Datenstruktur selber ein ``Zeit''Konto $K$ zugeordnet,
+Wir gehen daher einen anderen Weg, die Accountingmethode. Hier wird der Datenstruktur selber ein "Zeitkonto" $K$ zugeordnet,
 in welche Operationen ein- und auszahlen. Schnelle Operationen wie ein \texttt{add} ohne Erweiterung von
 $\mathcal{A}$ zahlen dann mehr ein als sie kosten, wird dann aber eine Erweiterung von $\mathcal{A}$ fällig, wird sie
-aus dem Angespartem guthaben bezahlt.
+aus dem angespartem Guthaben bezahlt.
 
 Wir vereinfachen die Laufzeit von \texttt{add} und \texttt{delete} wie bei der aggregierten Analyse und haben damit folgende Tabelle für die Kosten und Einzahlungen:
 \begin{center}
   \begin{tabular}[c]{l | c | c}
-    Operation                              & Laufzeitkosten  & Einzahlung  \\
+    Laufzeit der $i$-ten Operation $f_i$        & Laufzeitkosten $a_i$  & Einzahlung $e_i$  \\
     \hline
-    \texttt{add} (ohne Erweiterung)        &   1             &   3         \\
-    \texttt{add} (mit Erweiterung)         &   n             &   3         \\
-    \texttt{delete} (ohne Erweiterung)     &   1             &   3         \\
-    \texttt{delete} (mit Erweiterung)      &   n             &   3         \\
+    \texttt{add} (ohne Umstrukturierung)        &   1                   &   3               \\
+    \texttt{add} (mit Umstrukturierung)         &   $n_i$               &   2               \\
+    \texttt{delete} (ohne Umstrukturierung)     &   1                   &   3               \\
+    \texttt{delete} (mit Umstrukturierung)      &   $n_i$               &   2               \\
   \end{tabular}
 \end{center}
 
+Wir betrachten dabei alles zu Zeitpunkten $i \in \mathbb{N}$. Dabei ist $f_i$ die Operation zum $i.$ Zeitpunkt, $n_i$ bezeichnet
+die Anzahl der Elemente im $\mathcal{DA}$ zum Zeitpunkt $i$ (vor dem Ausführen der Operation $f_i$), $a_i$ sind die dazu assoziierten (echten) Laufzeitkosten und $e_i$ die
+dafür eingezahlten Beträge. Der Kontostand schwankt auch mit der Zeit und wird daher indiziert.
+
 Dadurch, dass das Konto der Datenstruktur zugerechnet wird, und nicht nur einer speziellen Operation, können wir jetzt
 auch gemischte Operationsreihenfolgen analysieren. Solange der Kontostand nicht negativ wird, sind die Einzahlungen wohl
 eine ausreichende Abschätzung für die tatsächlichen Laufzeitkosten.
 
-Versuchen wir also, $K < 0$ zu erreichen. Wir starten direkt nach einer kostspieligen Operation, denn da ist der
-Kontostand minimal. Sowohl nach einer Erweiterung als auch nach einer Verschmälerung von $\mathcal{A}$ gilt:
-\begin{equation}
-  n = n_0 = \frac{1}{2} n_\text{cap},\    K_0 \geq 0.
-  \label{eq:post_resizing}
-\end{equation}
-
-Nun gibt es zwei Situationen, in denen wieder ein kostspielieger Kopiervorgang nötig wird, welcher das Konto belastet:
-\begin{itemize}
-  \item Wir erweitern $\mathcal{A}$, da $n = n_\text{cap}$ erreicht hat: Da wir die Größe von $\mathcal{A}$ gerade
-    angepasst wurde, gilt Gleichung (\ref{eq:post_resizing}). Vor einer Erweiterung müssen wir also mindestens $n_0$
-    einfache \texttt{add}-Operationen aufrufen. Mehr Aufrufe von \texttt{add} sind möglich, wenn zwischendurch noch \texttt{delete} aufgerufen wird. 
-    Das erhöht den Kontostand um $3n_0 - n_0$ (Einzahlung vs tatsächliche Kosten).
-    Der Kopiervorgang kostet dann $n_\text{cap} = 2n_0$, unser Kontostand ist danach also 
-    \[
-      K = K_0 + 3n_0 - n_0 - 2n_0 = K_0 \geq 0.
-    \]
-  \item Wir schrumpfen $\mathcal{A}$, da $n = \frac{1}{4} n_\text{cap}$ erreicht hat: Wieder gilt Gleichung
-    (\ref{eq:post_resizing}), da die
-    Größe von $\mathcal{A}$ gerade erst angepasst wurde. Vor einer Verkleinerung von $\mathcal{A}$ müssen also
-    mindestens $\frac{1}{2}n_0$ \texttt{delete}-Operationen durchgeführt werden, damit $n$ von $n_0 =
-    \frac{1}{2} n_\text{cap}$ auf $\frac{1}{4} n_\text{cap}$ fällt. Mehr Aufrufe sind möglich, falls zwischendurch auch
-    $\texttt{add}$ aufgerufen wird. Damit wird der Kontostand um mindestens $\frac{3}{2}n_0 - \frac{1}{2} n_0$ erhöht, bevor dann die kostspielige
-    Kopieraktion kommt, welche $\frac{1}{2}n_0$ kostet. Unser Kontostand beträgt danach also 
-    \[
-      K = K_0 + \frac{3}{2} n_0 - \frac{1}{2} n_0 - \frac{1}{2}n_0 \geq K_0 \geq 0.
-    \]
-\end{itemize}
-
-In beiden Fällen fällt der Kontostand $K$ nicht. Wenn wir die Analyse mit einem nichtnegativen Kontostand starten,
-bleibt dieser nichtnegativ. Damit ist $3 \in \mathcal{O}(1)$ eine gültige amortisierte Abschätzung auch für das
-gemischte Ausführen von $\texttt{add}$ und \texttt{delete}.
+Wir beobachten zuerst: Die eingezahlten Kosten vor einer Umstrukturierung decken die echten Kosten dieser
+Umstrukturierung:
+
+\begin{lemma}
+  Sei eine Operationenfolge $f_\bullet$ gegeben. Seien $i<j$ zwei Zeitpunkte, bei denen eine Umstrukturierung stattfindet. Dann ist
+  $K_i \leq K_j$.
+  \label{lemma:DA}
+\end{lemma}
+\begin{proof}
+  Wir beobachten zuerst: Direkt nach einer Umstrukturierung, als zu Beginn von Zeitpunkt $i+1$ gilt:
+  \[
+    n_{i+1} = \frac{n_\text{cap}}{2}.
+  \]
+  Nun gibt es zwei Fälle für die Umstrukturierung zum Zeitpunkt $j$:
+  
+  Fall 1: Die Umstrukturierung ist eine Erweiterung, es ist also $f_j$ gleich $\texttt{add}$.\\
+  Dann gibt es mindestens $n_{i+1}$ $\texttt{add}$-Operationen zwischen Zeitpunkt $i$ und $j$, da ansonsten $n$ nicht
+  genug gewachsen ist, um eine Erweiterung anzustoßen. Der Kontostand wächst daher bis zum Zeitpunkt $j-1$:
+  \[
+    K_{j-1} = K_i + \sum_{t=i+1}^{j-1} (e_t - a_t) \geq K_i + 3(n_{i+1}-1) - 1(n_{i-1}-1) = K_i + 2n_{i+1} - 2.
+  \]
+  Die Abschätzung $\geq$ rechtfertigt sich dadurch, dass zwischen $i$ und $j$ durchaus mehr als $n_{i+1}-1$ Zeitschritte liegen
+  können. Beispielsweise dann, wenn in der Operationenfolge auch mal ein $\texttt{delete}$ auftaucht, was dann ein
+  weiteres $\texttt{add}$ zum Ausgleich braucht.
+
+  Im Schritt $j$ wird nach Voraussetzung eine Umstrukturierung fällig. Daher betragen die Kosten $a_j = 1 
+  n_{\text{cap}} = 2 n_{i+1}$, während die Einzahlung $e_j = 2$ beträgt. Das entspricht aber genau der vorher
+  angesparten Summe:
+  \[
+    K_j = K_{j-1} + e_j - a_j \leq K_i.
+  \]
+
+  Fall 2, die Umstrukturierung in Form einer Verschmälerung verläuft analog und ist eine Übungsaufgabe.
+\end{proof}
+
+Damit können wir zeigen, dass das dynamische Array einen amortisierten Aufwand von $\mathcal{O}(1)$ hat:
+
+\begin{theorem}
+  Das dynamische Array hat einen asymptotischen Aufwand von $\mathcal{O}(1)$ für \texttt{\emph{add}}- und
+  \texttt{\emph{delete}}-Operationen, auch bei gemischter Reihenfolge.
+  \label{satz:DAAmo}
+\end{theorem}
+\begin{proof}
+  Sollten keinerlei Umstrukturierungen nötig sein, ist $a_i < e_i$ klar. Aber auch bei Umstruktierungen ist aus dem
+  vorherigen Lemma klar: Ist $K_0 = 0$ und damit nicht negativ, bleibt $K_i \leq 0$ für alle $i \in \mathbb{N}$.
+  Da die tatsächliche Laufzeit $T(f_i) = a_i$, haben wir also 
+  \[
+    T\left(\sum_{i=1}^n f_i \right) = \sum_{i=1}^n a_i \leq \sum_{i=1}^n e_i \leq 3n.
+  \]
+  Das gilt für alle möglichen Operationenfolgen, also auch für die zeitaufwendigste.
+
+  Nach Definition ist damit ist Laufzeit pro Operation 
+  \[
+    T_{\text{amo}}(n) = \frac{\max(\{\sum_{i=1}^n f_i | f_\bullet \in \{\texttt{add}, \texttt{delete}\}^\mathbb{N})}{n}
+      \leq \frac{3n}{n} = 3 \in \mathcal{O}(1).
+  \]
+\end{proof}
+
 
 \subsubsection{Aggregierte Analyse vs Accountingmethode}
 Warum nun den Extraaufwand für die Accountingmethode? Der Vorteil wird klar, wenn wir uns eine modifizierte Version von
-- 
GitLab