2.4 Graham's Scan
Geschichte
- 1972 von Ronald L. Graham entwickelt.
- 1979 von A. M. Andrew verbessert.
Die Folien zeigen die Variante nach Andrew (Monotone Chain): Sortierung nach -Koordinate, getrennte obere und untere Hülle. Grahams Original sortiert stattdessen nach dem Winkel um einen Startpunkt. Andrews Version kommt ohne Winkel aus und behandelt Sonderfälle einfacher.
Grundidee: inkrementell, von links nach rechts
Graham's Scan ist ein inkrementeller Algorithmus:
- Die Punkte werden einzeln betrachtet und nacheinander zur Lösung hinzugefügt.
- Das passiert von links nach rechts.
- Dafür werden die Punkte nach ihrer -Koordinate sortiert.
Problem: Die Ecken der Hülle sollen in Randreihenfolge entstehen. Läuft man von links nach rechts, kommen aber oben und unten liegende Ecken durcheinander.
Lösung (Folie 14): Berechne zuerst die obere Hülle, dann die untere Hülle.
- Getrennt wird an den Punkten mit minimaler und maximaler -Koordinate, und nach dem Sortieren. Beide gehören sicher zur Hülle, denn ganz links und ganz rechts kann nichts mehr kommen.
- Obere Hülle: der Randteil von über oben nach .
- Untere Hülle: der Randteil von über unten zurück nach .
Im Bild unten sind und der linkeste und der rechteste Punkt. Dort zerfällt die Hülle in die obere Hülle (violett) und die untere Hülle (blau). Setz ruhig eigene Punkte.
Die Schlüsselbeobachtung: nur Rechtskurven
Läuft man die obere Hülle von links nach rechts ab, biegt man an jeder Ecke nach rechts ab, im Uhrzeigersinn um das Innere herum.
Daraus folgt die Regel: Bilden die letzten drei Punkte der aktuellen Kette keine Rechtskurve, dann liegt unterhalb der Strecke (oder auf ihr). kann dann keine Ecke der oberen Hülle sein und wird gelöscht. Danach prüft man erneut die nun letzten drei Punkte, denn auch der Punkt vor könnte jetzt überflüssig sein.
Für die untere Hülle gilt dasselbe, gespiegelt: Von rechts nach links abgelaufen, sind auch dort nur Rechtskurven erlaubt. Deshalb ist der Code für beide Hälften identisch, nur die Laufrichtung ist umgekehrt.
Der Rechtskurven-Test (Folie 17)
Drei Punkte haben eine Rechtskurve, wenn links der Strecke liegt.
Mit dem Orientierungstest:
(Beide Formulierungen sind gleichwertig: rechts von ⇔ links von .)
Algorithmus (Folie 15)
1 Polygon GrahamScan(Points P) {
2 Sortiere Punkte nach x-Koordinate (p1,...,pn);
3 Lupper.append(p1); Lupper.append(p2);
4 for (i = 3; i <= n; i++) {
5 Lupper.append(pi);
6 while (Lupper enthält mehr als zwei Punkte und
7 die letzten drei Punkte a,b,c machen eine Linkskurve)
8 Lupper.delete(b);
9 }
10 Llower.append(pn); Llower.append(pn-1);
11 for (i = n-2; i >= 1; i--) {
12 Llower.append(pi);
13 while (Llower enthält mehr als zwei Punkte und
14 die letzten drei Punkte a,b,c machen eine Linkskurve)
15 Llower.delete(b);
16 }
17 Llower.delete(ersten Punkt); Llower.delete(letzten Punkt);
18 return Lupper.append(Llower);
19 }
Zeile für Zeile:
| Zeile | Bedeutung |
|---|---|
| 2 | Sortieren – sorgt dafür, dass jeder neue Punkt der bisher rechteste ist |
| 3 | Die obere Kette startet mit den zwei linkesten Punkten |
| 5 | Neuer Punkt wird immer angehängt, er ist der rechteste bisher und damit sicher Ecke der Hülle der bisherigen Punkte |
| 6–8 | Solange die letzten drei eine Linkskurve bilden: mittleren Punkt entfernen. Die Liste verhält sich wie ein Stack |
| 10–16 | dasselbe für die untere Hülle, von rechts nach links |
| 17 | Der erste Punkt von ist , der letzte . Beide stehen schon in und werden entfernt, damit sie nicht doppelt vorkommen |
| 18 | Obere + untere Hülle = alle Ecken im Uhrzeigersinn, beginnend bei |
Interaktiv durchspielen: Setze eigene Punkte und gehe den Algorithmus Schritt für Schritt durch. Violett ist die obere Kette, blau die untere, das Dreieck zeigt den Test der letzten drei Punkte. ⏩ springt zum nächsten neuen Punkt.
Durchgerechnetes Beispiel
Punkte (bereits lexikographisch sortiert): , , , , .
Obere Hülle (links → rechts):
| Schritt | Liste nach Anhängen | Test der letzten drei | Aktion | Liste danach |
|---|---|---|---|---|
| Start | A B | – | – | A B |
| +C | A B C | orient(A,B,C) : Rechtskurve | – | A B C |
| +D | A B C D | orient(B,C,D) : Linkskurve | C löschen | A B D |
| A B D | orient(A,B,D) : Rechtskurve | – | A B D | |
| +E | A B D E | orient(B,D,E) : Rechtskurve | – | A B D E |
Untere Hülle (rechts → links, Reihenfolge E, D, C, B, A):
| Schritt | Liste | Test | Aktion | danach |
|---|---|---|---|---|
| Start | E D | – | – | E D |
| +C | E D C | orient(E,D,C) : Linkskurve | D löschen | E C |
| +B | E C B | orient(E,C,B) : Rechtskurve | – | E C B |
| +A | E C B A | orient(C,B,A) : Linkskurve | B löschen | E C A |
| E C A | orient(E,C,A) : Linkskurve | C löschen | E A |
Zeile 17: E und A entfernen, ist leer.
Ergebnis: A → B → D → E (im Uhrzeigersinn). C liegt innen. Die untere Kante schließt das Polygon. ✔
Dasselbe Beispiel zum Nachklicken (um verschoben, die orient-Werte bleiben gleich):
Laufzeit (Folie 16)
Die konvexe Hülle von Punkten in der Ebene kann in berechnet werden.
| Teil | Aufwand |
|---|---|
| Lexikographisches Sortieren | |
Obere Hülle, for-Schleife (Zeile 4) | Durchläufe |
Obere Hülle, while-Schleife (Zeile 6) | insgesamt höchstens Löschungen |
| ⇒ obere Hülle | |
| Untere Hülle | analog |
| Gesamt |
Warum ist die while-Schleife insgesamt nur ?
Auf den ersten Blick sieht es nach aus: eine while-Schleife in einer for-Schleife, und die while-Schleife kann in einem Schritt viele Punkte löschen.
Amortisierte Argumentation: Jeder Punkt wird genau einmal angehängt und höchstens einmal gelöscht, denn gelöschte Punkte kommen nie zurück. Jeder while-Durchlauf, dessen Test zutrifft, löscht einen Punkt. Also gibt es über den gesamten Algorithmus höchstens erfolgreiche Tests. Dazu kommt pro for-Durchlauf ein Test, der fehlschlägt und die Schleife beendet. Zusammen sind das höchstens Tests, also .
Das Sortieren dominiert. Sind die Punkte schon sortiert, läuft der Scan in .
Mit der unteren Schranke folgt: Graham's Scan ist worst-case-optimal.
Korrektheit (Folie 16)
Beweis durch Induktion über die Anzahl der verarbeiteten Punkte.
Invariante: Nach Verarbeitung von enthält genau die Ecken der oberen Hülle von , von links nach rechts.
- Anfang (): ist trivialerweise die obere Hülle zweier Punkte.
- Schritt : ist der rechteste Punkt und daher sicher eine Ecke der neuen oberen Hülle. Ecken der alten Hülle, die jetzt unter der neuen Kante zu liegen, sind keine Ecken mehr. Das sind genau die Punkte am Ende der Liste, an denen beim Anhängen von eine Linkskurve entsteht. Die
while-Schleife entfernt sie von hinten. Sobald eine Rechtskurve entsteht, ist die restliche Kette nach Induktionsvoraussetzung schon konvex und bleibt unverändert.
Untere Hülle analog.
Sonderfälle (Folie 18)
„Der Algorithmus ist nicht korrekt!“
1. Mehrere Punkte mit gleicher -Koordinate
Dann ist die Ordnung „nach sortiert“ nicht eindeutig. Liegen z.B. zwei Punkte senkrecht übereinander ganz links, ist nicht klar, welcher ist, und der Scan kann je nach Reihenfolge falsch abbiegen.
Lösung: lexikographisch sortieren. Bei gleicher -Koordinate entscheidet die -Koordinate:
Das entspricht einer infinitesimal kleinen Drehung des Koordinatensystems: Jetzt haben alle Punkte verschiedene „-Werte“.
2. Drei Punkte im Rechtskurven-Test liegen auf einer Geraden
Ist , macht der Weg weder eine Links- noch eine Rechtskurve. Die Folie sagt: „Keine Rechtskurve!“ Also wird gelöscht. Der Punkt liegt auf der Kante und ist keine echte Ecke.
Daraus folgt: Die Löschbedingung ist nicht „Linkskurve“ (), sondern „keine Rechtskurve“, also .
Weitere Fälle (Ergänzung)
| Fall | Verhalten |
|---|---|
| Es gibt kein Polygon. Die Punkte direkt zurückgeben | |
| Doppelte Punkte | Die Problemstellung verlangt paarweise verschiedene Punkte. In der Praxis Duplikate nach dem Sortieren entfernen |
| Alle Punkte kollinear | Obere Hülle wird , untere . Nach Zeile 17 bleibt , eine Strecke. Funktioniert ohne Sonderbehandlung |
In C++ (Ergänzung)
Umsetzung von Folie 15 mit den Sonderfällen
struct PointF { double x, y; };
// > 0: Linkskurve, < 0: Rechtskurve, == 0: kollinear
double orient(const PointF& a, const PointF& b, const PointF& c)
{
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
bool lexLess(const PointF& a, const PointF& b)
{
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
bool samePoint(const PointF& a, const PointF& b)
{
return a.x == b.x && a.y == b.y;
}
std::vector<PointF> grahamScan(std::vector<PointF> P)
{
std::sort(P.begin(), P.end(), lexLess); // Zeile 2, lexikographisch
P.erase(std::unique(P.begin(), P.end(), samePoint), P.end()); // Duplikate entfernen
const int n = int(P.size());
if (n < 3) return P;
std::vector<PointF> upper{P[0], P[1]}; // Zeile 3
for (int i = 2; i < n; ++i) { // Zeile 4
upper.push_back(P[i]);
while (upper.size() > 2 && // "keine Rechtskurve"
orient(upper[upper.size() - 3], upper[upper.size() - 2], upper.back()) >= 0)
upper.erase(upper.end() - 2); // b löschen
}
std::vector<PointF> lower{P[n - 1], P[n - 2]}; // Zeile 10
for (int i = n - 3; i >= 0; --i) { // Zeile 11
lower.push_back(P[i]);
while (lower.size() > 2 &&
orient(lower[lower.size() - 3], lower[lower.size() - 2], lower.back()) >= 0)
lower.erase(lower.end() - 2);
}
upper.insert(upper.end(), lower.begin() + 1, lower.end() - 1); // Zeilen 17–18
return upper; // im Uhrzeigersinn
}