\( \newcommand{\data}{\textnormal{data}} \newcommand{\lower}{\textnormal{lower}} \newcommand{\upper}{\textnormal{upper}} \newcommand{\array}{\textnormal{array}} \newcommand{\depth}{\textnormal{depth}} \newcommand{\height}{\textnormal{height}} \newcommand{\BST}{\textnormal{BST}} \newcommand{\floor}[1]{\left\lfloor #1 \right\rfloor} \newcommand{\ceil}[1]{\left\lceil #1 \right\rceil} \newcommand{\lognpone}{\ceil{\log_2(n+1)}} \newcommand{\bitsize}{\textnormal{bitSize}} \newcommand{\mult}{\textnormal{mult}} \newcommand{\inneighbors}{\textnormal{inNeighbors}} \newcommand{\outneighbors}{\textnormal{outNeighbors}} \newcommand{\neighbors}{\textnormal{neighbors}} \newcommand{\indeg}{\textnormal{indeg}} \newcommand{\outdeg}{\textnormal{outdeg}} \newcommand{\scc}{\textnormal{scc}} \newcommand{\R}{\mathbb{R}} \newcommand{\val}{\textnormal{val}} \newcommand{\dist}{\textnormal{dist}} \newcommand{\rank}{\textnormal{rank}} \newcommand{\flows}{\textnormal{flows}} \newcommand{\Z}{\mathbb{Z}} \DeclareMathOperator*{\E}{\mathbb{E}} \newcommand{\scalar}[2]{\langle #1, #2\rangle} \)

Die Eingabe besteht aus einer Menge $X$ von Items $x$ aus einem geordneten Universum. Das Ziel ist es, diese Menge zu sortieren. Was heißt das im MPC-Kontext? Wir betrachten die Menge $X$ als sortiert, wenn jeder Item $x$ durch einen Item $(i,x)$ ersetzt worden ist, wobei $i = \rank(x,X)$ ist. Dann hätten wir $X$ in ein Array im Sinne von umgewandelt und somit in jedem vernünftigen Sinne sortiert. In Worten: am Ende des Algorithmus soll jedes Element seinen Rang kennen.

Unser Algorithmus ist eine MPC-Version von Quicksort. Die Idee ist es, nicht ein Pivot, sondern eine ganze Menge $Y$ von Pivots zu wählen. Die Pivots $Y$ zerteilen die Menge $X$ in mehrere Teilintervalle oder Unterprobleme. Über einen Baum ähnlich wie in senden wir dann alle Pivots zu einer zentralen Maschine, welche sie lokal sortiert (was als ein Schritt zählt) und als sortierte Menge wieder an alle Maschinen aussendet. Jede Maschine bestimmt dann (lokal, also in einem Schritt) für jedes ihrer Items, zu welchem Unterproblem es gehört. Nun lösen wir rekursiv jedes Unterproblem.

Eine Schwierigkeit besteht darin, dass wir die Maschinen irgendwie den Unterproblemen zuordnen müssen; also (1) bestimmen, an welchem Unterproblem $j$ die Maschine $i$ arbeiten soll und (2) erreichen müssen, dass alle Items, die zu Unterproblem $j$ gehören, auf den zuständigen Maschinen landen. Problem (1) wird einen weiteren "Baum-Broadcast" erfordern; Problem (2) lösen wir mit Randomisierung.

MPC-Quicksort

Als erstes wollen wir eine Menge $Y$ von $\sqrt{S} = N^{\epsilon/2}$ zufällig Pivots wählen, und zwar in einem Schritt, also ohne Kommunikation. Dies geht näherungsweise und mit Randomisierung: wenn wir jeden Item $x$ mit Wahrscheinlich $p$ markieren, dann gilt für die Menge $Y$ von markierten Items:

\begin{align*} \E[|Y|] & = \sum_{x \in X} \Pr[x \textnormal{ ist markiert}] = p \cdot N \ . \end{align*}

Wir wollen $\E[|Y|] = \frac{\sqrt{S}}{2}$ erreichen: die Menge $Y$ der Pivots soll auf keinen Fall mehr als $\sqrt{S}$ Elemente enthalten, und der Faktor $1/2$ dient als Sicherheitsabstand. Wir wählen also $p = \frac{\sqrt{S}}{2N}$.

Behauptung Sei $p = \frac{\sqrt{S}}{2N}$. Wenn wir jeden Item mit Wahrscheinlichkeit $p$ markieren, dann haben wir mit großer Wahrscheinlichkeit höchstens $\sqrt{S}$ Items markiert.

Warum die Behauptung gilt und was überhaupt "mit großer Wahrscheinlichkeit" heißt, das schauen wir uns weiter unten genauer an. Im Moment gehen wir einfach davon aus, dass $|Y| \leq \sqrt{S}$ gilt. Die Pivots $Y$ liegen über alle Maschinen verteilt. Wir wollen sie auf einer bestimmte Maschine, zum Beispiel Maschine 1, sammeln.

Übungsaufgabe Zeigen Sie, wie man in $2 \lambda$ Runden (merke: $\lambda = \frac{\log N}{\log S}$) erreichen kann, dass alle Items $Y$ auf Maschine $1$ liegen. Gehen Sie hierbei davon aus, dass jede Maschine anfangs noch mindestens $\sqrt{S}$ freien Speicherplatz hat.

Maschine $1$ hält nun (neben ihren "normalen" Eingabe-Items) die Menge $Y$. Sie sortiert sie lokal, bestimmt somit für jedes $y \in Y$ seinen Rang $j=\rank(y, Y)$ und bildet den "indizierten" Item $(j,y)$. Wir bezeichnen diese indizierte Menge $\{(\rank(y,Y), y) \ | \ y \in Y\}$ als $\hat{Y}$. Dann sendet Maschine $1$ die Menge $\hat{Y}$ an alle Maschinen; dies geht wiederum in $2 \lambda$ Runden, analog zu . Alle Maschinen kennen nun also die Pivots $y_1 \lt y_2 \lt \dots \lt y_k$ in aufsteigender Reihenfolge (mit $k := |Y|)$. Dies unterteilt $X$ in Teilintervalle / Unterprobleme. Formal: das Teilproblem $X_j$ ( für $0 \leq j \leq k$) ist die Menge \begin{align*} X_j := \{x \in X \ | \ \rank(x,Y) = j\} \ . \end{align*} Jede Maschine bestimmt nun $\rank(x,Y)$ für jeden ihrer Items $x$ und weiß nun, zu welchem Teilproblem $x$ gehört; ganz konkret ersetzt sie $x$ durch $\scalar{x}{\rank(x,Y)}$. Dies geht lokal, also in einer Runde. Jede Maschine $i$ kennt nun die Anzahl $N_{i,j}$ ihrer Items, die zu Teilproblem $j$ gehören. Wir wollen nun

\begin{align*} N_j := \sum_{i = 1}^P N_{i,j} = |X_j| \ . \end{align*}

bestimmen, also die Größe des Teilproblems $j$.

Übungsaufgabe Zeigen Sie, wie wir in $4 \lambda$ Runden erreichen können, dass jede Maschine alle Zahlen $N_0, \dots, N_k$ kennt.

Wir wollen nun die $P$ Maschinen den $k$ Unterproblemen zuteilen und dann $X_j$ auf die zuständigen Maschinen aufteilen. Hierfür brauchen wir mindestens $\ceil{|X_j| / S }$ Maschinen. Um etwas "Sicherheitsabstand" zu haben, setzen wir $P_j := \frac{2 |X_j|}{S}$. Jede Maschine kennt die Zahlen $P_j$ und weiß somit, welche Maschinen für Teilproblem $j$ zuständig sind: mit $t_j := P_1 + \cdots + P_{j-1}$ sind das die Maschinen mit Index in

\begin{align*} I_j := \{t_j + 1, t_j + 2, \dots, t_j + P_j\} \end{align*}

Beachten Sie, dass die Berechnung der Präfixsummen $P_1 + \cdots + P_{j-1}$ kein Präfixsummenproblem im MPC-Sinne darstellt, weil es lokal ausgeführt werden kann: jede Maschine kennt alle Zahlen $P_1, \dots, P_k$. Wir müssen nun alle Items, die zum Teilproblem $j$ gehöhren, auf die Maschinen mit Index in $I_j$ aufteilen. Damit dies in einer Runde gelingt, verwenden wir wiederum Randomisierung: Maschine $i$ schickt jeden ihrer Items $\scalar{x}{j}$ an eine Maschine $i'$, wobei sie $i'$ zufällig aus $I_j$ wählt.

Wieviele Items empfängt eine Maschine $i'$ in dieser Runde? Die Maschinen in Menge $I_j$ empfangen insgesamt $|X_j|$ Items; da die Items zufällig veretilt werden, erhält Maschine $i'$ im Erwartungswert $\frac{|X_j|}{P_j} = \frac{S}{2}$ viele Items.

Behauptung Mit hoher Wahrscheinlichkeit gibt es keine Maschine, die in diesem Schritt mehr als $S$ Items empfängt.

Wenn bisher kein "Unfall" geschehen ist (also keines der schlimmen Ereignisse von kleiner Wahrscheinlichkeit eingetreten ist), dann hält nun jede Maschine eine Menge von Items, die alle zu einem Teilproblem gehören, und jede Maschine weiß auch, an welchem Teilproblem sie arbeiten soll und welche anderen mit ihr. Wir können nun also rekursiv auf jedem Teilproblem weiterarbeiten.

Laufzeitanalyse

Die gerade beschriebene Phase von MPC-Quicksort benötigt $O(\lambda)$ Runden. Es bleibt zu untersuchen, wie viele Phasen der rekursive Aufruf erzeugt. Hierzu untersuchen wir, wie groß die Teilprobleme werden. Da wir $X$ in $k+1$ Teilprobleme $X_0, \dots, X_k$ unterteilen (mit $k = |Y|$ die Anzahl der Pivots), sollte jedes Teilproblem "im Durchschnitt" $\frac{N}{k+1}$ Elemente enthalten. Die Anzahl $k$ der Pivots ist wiederum im Erwartungswert $\tilde{k} := \frac{\sqrt{S}}{2}$, so dass wir $\frac{N}{k+1}$ wohl bei ungefährt $\frac{2N}{\sqrt{S}} = 2 N^{1 - \epsilon/2}$ erwarten.

Warnung. Es ist nicht korrekt, Erwartungswerte zu dividieren, also \begin{align*} \textcolor{red}{\E\left[ \frac{N}{1 + |Y|}\right] = \frac{N}{1 + \E[|Y|]}} \tag{im Allgemeinen falsch!} \end{align*} zu rechnen. Dennoch ist es legitim, quasi als Bierdeckelrechnung mal zu schauen, was dabei rauskommt, um zu sehen, ob man auf dem richtigen Weg ist.

Wir "erwarten" also, dass die Größe eines Teilproblems bei ungefähr $2 N^{1- \epsilon/2}$ liegt. Nach dieser Rechnung würde in jedem rekursiven Level die Anzahl $N$ der Elemente ungefähr durch $\tilde{k} = \frac{\sqrt{S}}{2}$ geteilt. Nach $\frac{\log N} {\log \tilde{k}} = O(1/\epsilon)$ Rekursionsleveln wären wir fertig; sogar noch etwas früher: sobald ein Teilproblem kleiner als $N^{\epsilon}$ ist, können wir es auf einer Maschine sortieren.

Behauptung Mit hoher Wahrscheinlichkeit gilt $N_j \leq \frac{4N}{\sqrt{S}}$ für jedes $j$.

Wahrscheinlichkeitsrechnung: Beweise der Behauptungen

Schneller sortieren in $O(\lambda)$ Runden

Ich beschreibe nun einen schnelleren Sortieralgorithmus, der auch auf Quicksort beruht. Er stammt aus dem Paper Sorting, Searching, and Simulation in the MapReduce Framework von Michael T. Goodrich, Nodari Sitchinava und Qin Zhang.