-
Notifications
You must be signed in to change notification settings - Fork 6
/
permutationen.tex
196 lines (161 loc) · 12.3 KB
/
permutationen.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
Wir haben bereits mehrfach zufällige Permutationen benötigt;
in der Tat müssen so häufig Sequenzen von Elementen zufällig angeordnet werden, dass fast jede allgemeine Programmiersprache über sog. Shuffle-Funktionen verfügt.
\begin{algorithm}
\KwIn{Eingabearray $A[1 \ldots n]$}
\KwOut{Array~$A$}
\For{$n \ge i \ge 2$}{
$j \gets$ uniform aus $[1 \ldots i]$\;
Tausche $A[i]$ und $A[j]$
}
Gebe $A$ zurück
\caption{Fisher-Yates-Shuffle}
\label{alg:fisher-yates}
\end{algorithm}
Für kleine Eingaben $A[1 \ldots n]$ ist meist Fisher-Yates-Shuffle die beste Wahl.
Wie in \cref{alg:fisher-yates} dargestellt, partitioniert Fisher-Yates konzeptionell die Eingabe im Urnenpräfix und im Ergebnissuffix.
In jeder Iteration wird genau ein Element~$A[j]$ aus der Urne gewählt und dem Ergebnis an Position~$A[i]$ hinzugefügt.
Durch den Tausch von $A[i]$ und $A[j]$ stellen wir sicher, dass hierbei keine Elemente verloren gehen.
Dieser Algorithmus hat eine Laufzeit von $\Oh{n}$ und benötigt nur $\Oh{\log n}$ Bits zusätzlichen Speicher (u.\,a. für $i$ und $j$).
Zur Erinnerung eine Definition:
\begin{definition}
Wir bezeichnen einen Algorithmus als \emph{in-place}, wenn dieser auf einer Eingabe der Größe~$N$ nur $o(N)$ zusätzliche Speicherworte verwendet.
\end{definition}
Beim Fisher-Yates-Shuffle handelt es sich also um einen In-Place-Algorithmus.
\section{Paralleles Fisher-Yates}
Fisher-Yates ist auf den ersten Blick ein inhärent sequentielles Problem.
In jedem Schritt wird ein Element aus der Urne entnommen, was den Zustand der Urne verändert.
Daher ist das Ergebnis von Shun et al.~\cite{DBLP:conf/soda/ShunGBFG15} besonders spannend.
Sie zeigten, dass der Algorithmus fast ohne Modifikation in erwartet linearer Arbeit und $\Oh{\log n \log^* n}$ auf einer Priority-Write-CRCW implementiert werden kann.
Die Idee hierbei ist einfach.
Statt in jedem Schritt einen zufälligen Index zu ziehen, gehört das Array $H[1 .. n]$ zur Eingabe.
Dabei entspricht $H[i]$ dem Index, mit dem $A[i]$ getauscht werden soll; es übernimmt also die Rolle von $j$ in \cref{alg:fisher-yates}.
Das Vorberechnen der Indizes erlaubt es uns, schon Abhängigkeiten zu erkennen, bevor Swaps durchgeführt werden.
Weiter können wir die Swaps nebenläufig und out-of-order ausführen, wenn hierbei die erkannten Abhängigkeiten beachtet werden.
Hierzu nutzen die Autoren ihr \emph{Reserve-and-Commit}-Framework:
Wir suchen für jeden Index~$j$ in $A$ den ersten Swap, der dieses Element in der sequentiellen Ausführung verändern würde (\dh das größte $i$ mit $H[i] = j$).
Diesen Swap können wir ausführen und aus dem Spiel nehmen.
Sollte es weitere Swaps geben, die ebenfalls dieses Element verarbeiten möchten, werden diese verzögert.
In jedem Schritt wird also pro Abhängigkeitskette mindestens ein Swap verarbeitet.
Die Haupterkenntnis der Autoren ist, dass Fisher-Yates einen Abhängigkeitswald impliziert, der mit hoher Wahrscheinlichkeit eine Tiefe von $\Oh{\log n}$ hat.
In der Praxis ist der Algorithmus leider nicht besonders effizient:
Echte Maschinen sind in der Regel keine Priority-Write-CRCW -- dies muss mittels atomaren Compare-and-Swap-Operationen implementiert werden.
Diese Zugriffe sind überdies auch noch wahlfrei und verursachen für große Eingaben Cache-Misses.
Letztlich ist auch der Speicherbedarf suboptimal:
\begin{enumerate}
\item Die Eingabe $H$ benötigt mindestens $\Omega(n \log n)$ Bits.
\item Die Datenstruktur zum Finden der Kollisionen benötigt $\Omega(n \log P)$ Bits, wobei $P$ die Anzahl an Prozessoren bezeichnet.
\end{enumerate}
Der Algorithmus ist also nicht in-place und benötigt viele Prozessoren, um überhaupt einen Speed-up zu erzielen.
In einem weiteren Papier zeigten die Autoren, dass die Kollisionen mittels $o(n)$ Bits gefunden werden können.
Unter der Annahme, dass $H$ zur Eingabe gehört, handelt es sich also um einen In-Place-Algorithmus.
Im Vergleich zu Fisher-Yates wird jedoch weiterhin mindestens linear viel Speicher benötigt -- denn Fisher-Yates benötigt $H$ nicht.
\section{Paralleles Shuffling}
\textcolor{red}{Vorsicht:} In diesem Abschnitt verwenden wir nullindizierte Sequenzen $[x_0, \ldots, x_{n-1}]$ und Prozessoren $\set{p_0, \ldots, p_{P-1}}$.
Ein einfacher und deshalb besonders praktischer paralleler Algorithmus basiert auf~\cite{SANDERS2016489}.
Der Aufsatz diskutiert u.\,a. einen Algorithmus zum Shufflen auf verteilten Computern --
jeder Prozessor interagiert mit seiner Umwelt durch explizites Senden von Nachrichten.
%Auch wenn auf echten Maschinen hierdurch natürlich erhebliche Kosten entstehen, ignorieren wir in diesem Abschnitt die benötigte Kommunikation.
Der Algorithmus eignet sich mit geringen Modifikationen auch für parallele Maschinen mit gemeinsam genutztem Speicher und funktioniert auch im External-Memory-Modell.
Als Eingabe erhalten wir Elemente $X=[x_0, \ldots, x_{n-1}]$, die arbiträr auf $P$ Prozessoren partitioniert sind, \sd jeder Prozessor $\Theta(n/P)$ Elemente erhält.
Diese sollen in eine uniform zufällige Permutation überführt werden.
Die Ausgabereihenfolge ergibt sich dadurch, dass wir die lokalen Sequenzen der Prozessoren in aufsteigender Reihenfolge konkatenieren --
erst kommen alle Elemente, für die Prozessor~0 im Ergebnis zuständig ist, dann jene von Prozessor~1 usw. usf.
Hierzu führt Prozessor~$\ell$ das folgende Programm aus:
\begin{enumerate}
\item Sende jedes lokale Element~$x_i$ der Eingabe zu einem Prozessor, der unabhängig und uniform zufällig gewählt wurde.
\item Empfange alle Elemente $T = [t_0, \ldots, t_{k_\ell-1}]$.
\iffalse\item Berechne die Präfixsumme $\Delta_\ell = \sum_{i=0}^{\ell - 1} k_i$.
Der Wert $\Delta_\ell$ entspricht dann der Position des ersten Elements von Prozessor~$\ell$ in der Ausgabe.\fi
\item Permutiere $T$ lokal \zB mit Fisher-Yates.
\item Speichere $T$ als $\ell$-ten Teil der Ausgabe.
\end{enumerate}
Die Korrektheit des Algorithmus folgt aus dem folgendem Theorem.
\begin{theorem}[nach~\cite{SANDERS2016489}]
Jede mögliche Permutation wird mit Wahrscheinlichkeit $1/n!$ erzeugt.
Der Algorithmus erzeugt also eine uniforme Permutation.
\end{theorem}
\noindent
\textcolor{red}{Beweis wird nachgereicht.}
\iffalse
\begin{proof}
Sei $\pi$ eine vom Algorithmus erzeugte Permutation und $\pi'$ eine beliebige fixierte Permutation.
Dann gilt
\begin{align}
\prob{\pi = \pi'} & = \prob{\bigwedge\limits_{i=0}^{n-1} \pi(i) = \pi'(i)} \\
& = \prod_{i=0}^{n-1} \prob{\pi(i) = \pi'(i) \quad\middle|\quad \bigwedge\limits_{j=0}^{i-1} \pi(j) = \pi'(j) }
\end{align}
\noindent
Wir werden zeigen, dass für $0 \le i < n$ gilt
\begin{equation}
\prob{\pi(i) = \pi'(i) \quad\middle|\quad \bigwedge\limits_{j=0}^{i-1} \pi(j) = \pi'(j) } = \frac{1}{n - i}.
\end{equation}
\noindent
Aufgrund der folgenden Identität folgt dann das Theorem
\begin{equation}
\frac{1}{n!}
\quad = \quad \prod_{i=1}^{n} \frac{1}{i}
\quad = \quad \prod_{i=0}^{n-1}\frac{1}{n-i}.
\end{equation}
Sei $K = [k_0, \ldots, k_{P-1}]$ die Anzahl der Elemente, die den Prozessoren geschickt wurden, und $K'$ eine beliebige valide Anzahl (\dh insbesonder, dass $\sum_i k_i = n$ und $k_i > 0$ für alle $i$).
Wir zeigen nun, dass
\begin{align}
P_i & := \prod_{i=0}^{n-1} \prob{\pi(i) = \pi'(i) \quad\middle|\quad K = K' \land \bigwedge\limits_{j=0}^{i-1} \pi(j) = \pi'(j) } \\
& = \frac{1}{n - i}
\end{align}
Für ein fixiertes Element $x_i$ sei $\ell$ der Prozessor, der es in der Ausgabe enthält.
Es gilt $\pi(i) = \pi'(i)$ genau dann, wenn $x_i$ zunächst an Prozessor~$\ell$ geschickt wird und dort dann an den lokalen Rank~$\pi(i) - \Delta_\ell$ permutiert wird.
Aufgrund der bedingten Wahrscheinlichkeit nehmen wir an, dass bereits $i-1$ Element fixiert wurden und $n-i$ noch ihren Platz suchen.
Da $K = K'$ gilt weiter, dass $k'$ noch an Prozessor~$\ell$ geschickt werden müssen:
\begin{equation}
k' = \underbrace{\Delta_\ell + k_\ell}_\text{Ende von $\ell$'s Bereich} - \underbrace{\pi(i)}_\text{Ausgabeposition von $x_i$}
\end{equation}
Daher wird unter den verbleibenden $n-i$ Elementen $x_k$ mit Wahrscheinlichkeit $k' / (n-i)$ Prozessor~$\ell$ geschickt.
Die Wahrscheinlichkeit, dass es dann an die erste noch nicht fixierte Stelle geshuffelt wird, ist $1 / 'k$.
Somit folgt
\begin{equation}
P_i = \frac{k'}{n - i} \quad \cdot \quad \frac{1}{k'}.
\end{equation}
\noindent
Die Behauptung folgt.
\end{proof}
\fi
Am längsten benötigt der Algorithmus für das zufällige Versenden der Elemente und das lokale Permutieren der empfangen Elemente.
Die Laufzeit des ersten Schrittes ist linear in der größten Partition, die nach Voraussetzung auf $\Theta(n / P)$ beschränkt ist.
Das lokale Permutieren benötigt dann Zeit $\max_\ell k_\ell$, \dh linear in der Größe der größten empfangenen Partition.
Da die Prozessoren unabhängig uniform gewählt werden, handelt es sich also um ein klassisches Gedankenexperiment, bei dem $n$~Bälle auf $P$~Eimer verteilt werden sollen.
Man kann daher mit einem Chernoff-Argument zeigen, dass für $\beta > 0$ gilt:
\begin{equation}
\prob{\max_\ell k_i > \frac{n}{P} + \sqrt{2 \frac n p (\beta \ln n + \ln P) } + \Oh{\ln n}} < n^{-\beta}.
\end{equation}
Unter realistischen Annahmen ($n / P = \Omega(\ln n)$) folgt also eine Laufzeit von $\Oh{n / P}$ mit hoher Wahrscheinlichkeit.
\iffalse
\section{MergeShuffle}
In Analogie zu MergeSort können wir auch zufällige Permutation erzeugen;
der Algorithmus kann im Fork-Join-Modell analysiert werden, verursacht $\Oh{n \log n}$ Arbeit und hat einen Span von $\Oh{n}$.
Die Idee ist grob wie folgt:
wir teilen eine Eingabe von $n$ Elementen in zwei (etwa) gleich große Partition.
Dann permutieren wir beide Teile unabhängig und rekursive (das gelingt also trivial nebenläufig).
Schließlich \emph{mischen} wir die beiden Teile wieder.
Im Gegensatz zu MergeSort, verstehen wir aber unter \emph{Mischen} hier den Prozess,
zwei uniforme Permutationen in eine uniforme Permutation der Elemente beider Eingaben zu überführen.
Der Algorithmus an sich ist wenig praktisch, eine Generalisierung dieses Mischens ist aber für andere Algorithmen nützlich.
\begin{theorem}
Seien $A$ und $B$ zwei zufällig permutierte Sequenzen mit $n_A = |A|$ und $n_B = |B|$.
Dann erzeugt \cref{algo:mergeshuffle_merge} eine uniform permutierte Vereinigung der Elemente in $A$ und $B$ mit Größe $n = n_A + n_B$.
\end{theorem}
\begin{proof}[Beweisskizze]
Wir nehmen an, dass $B$ vollständig in der ersten Schleife konsumiert wird, und aus $A$ exakt $k$ Elemente verbleiben -- der andere Fall läuft analog.
Zu diesem Zeitpunkt wurden also $n-k$ Elemente in die Ausgabe übertragen.
Davon stammen $n_B$ aus $B$ und $n_A - k$ aus $A$.
Wir können uns den Prozess als zufällige Sequenz von $a$ und $b$, wobei jedes $a$ bewirkt, dass das nächste Elemente aus $A$ in die Ausgabe kopiert wird, und $b$ analog für $B$.
Die Sequenz endet mit einem $b$ -- sonst hätten wir nicht festgestellt, dass $B$ bereits erschöpft ist.
Da sowohl $A$ als auch $B$ bereits zufällig permutiert sind, ist es nicht notwendig jeweils ein zufälliges zu entnehmen -- das jeweils erste nicht gezogene hat diesselbe Verteilung.
Daher können wir zwei $a$s bzw. $b$s als ununterscheidbar annehmen und tatsächlich direkt über die zufällige $\set{a,b}^{n-k}$ Sequenz argumentieren.
Da jedes Zeichen $a$/$b$ unbiased und unabhängig gezogen wird, ist die $\set{a,b}$-Sequenz bedingt auf die Anzahl von $a$ und $b$ uniform.
Daraus folgt, dass die Sequenz der ersten $n - k$ Elemente eine zufällige Permutation ist.
Nun müssen wir noch die verbleibenden $k$ Elemente behandelt.
Die Beweisstruktur läuft analog zu Fisher Yates:
zu Beginn jeder Schleife ist die bereits erstellte Ausgabe per IV eine uniforme Permutation.
Durch Einfügen eines Elements an einer zufälligen Stelle, ist die neue --größere-- Permutation ebenfalls uniform.
\end{proof}
\fi