Algorithmus zum Punkte verbinden

Könntest Du es mal erklären :D Mal so für Halbschlaue..
Ich kann es ja mal versuchen. Aber ich warne jetzt schon: das wird bestimmt länglich und etwas mathematisch-theoretisch…

Ein lineares Programm bzw. eine lineare Optimierungsaufgabe ist eine Aufgabe der Form:
maximiere c_1 * x_1 + c_2 * x_2 + … + c_n * x_n
unter den Nebenbedingungen:
a_1_1 * x_1 + a_1_2 * x_2 + … + a_1_n * x_n <= b_1
a_2_1 * x_1 + a_2_2 * x_2 + … + a_2_n * x_n <= b_2

a_m_1 * x_1 + a_m_2 * x_2 + … + a_m_n * x_n <= b_m

Oder nicht ganz so abstrakt formuliert: man hat n Variablen x_1, …, x_n und eine Zielfunktion c_1 * x_1 + c_2 * x_2 + … + c_n * x_n. Man sucht nun eine Belegung der Variablen mit konkreten Werten, sodass die Zielfunktion einen möglichst großen Wert annimmt. Die Werte, die die Variablen annehmen dürfen, unterliegen dabei aber bestimmten linearen Einschränkungen (den Nebenbedingungen).

Das klassische Beispiel für ein lineares Programm ist die Produktionsaufgabe: die Variablen x_1, …, x_n stehen für die Menge der zu produzierenden Güter im kommenden Monat (z.B. gibt x_1 an, wieviele Nägel die Fabrik herstellen soll, x_2 wieviele Konservendosen, x_3 wieviele Ölfässer…). Die Zielfunktion beschreibt den zu erwartenden Gewinn (die c_1, …, c_n geben also an, wieviel Gewinn man bei Produktion und Verkauf eines Nagels (c_1), einer Konservendose (c_2), eines Ölfasses (c_3) etc. macht). In den Nebenbedingungen stecken dann Einschränkungen, wie bspw. das im kommenden Monat zur Verfügung stehende Rohmaterial, die verfügbaren Maschinen, Arbeitskräfte usw. Lässt man nun einen LP-Solver auf dieses Problem los, erhält man am Ende eine Variablenbelegung (also die Mengen der zu produzierenden Güter pro Typ), mit der die Produktionskapazitäten optimal (im Sinne von: den maximalen Gewinn erzielend) ausgenutzt werden.

Was hat das nun mit Punkten und Dreiecken zu tun? :D Das ist der interessanteste, aber wohl auch komplizierteste Teil der Angelegenheit. Ich versuche trotzdem mal, den Schritt verständlich zu machen. Die Formulierung des Problems als LP beginnt damit, dass man sich zunächst seine Variablen passend wählt. In diesem Fall korrespondiert die Variable x_i_j zu der Kante, die die Punkte i und j verbindet. Insgesamt gibt es also n(n-1)/2 solcher Variablen (n Anzahl der Punkte). Die Variablen sollen außerdem binär sein, also nur die Werte 0 oder 1 annehmen können. Dabei bedeutet eine 1: verwende diese Kante in der Lösung. Entsprechend eine 0: verwende diese Kante nicht. Aus dieser Wahl ergibt sich ganz natürlich die Zielfunktion: als Vorfaktor für die Variable x_i_j wählen wir einfach die euklidische Distanz zwischen den Punkten i und j. Diese Zielfunktion wollen wir allerdings minimieren, sodass wir am Schluss die Lösung mit der kleinsten Umfangsumme erhalten.

Der wirklich knifflige Teil sind dann die Nebenbedingungen bzw.die Iteration: wie soll man die Variablen einschränken, damit man nur Dreiecke erhält? Ein erster Schritt wäre, zunächst einmal nur Polygone zu erzeugen (also zusammenhängende Kantenzüge, die zudem auch noch geschlossen sind). Dazu kann man die Beobachtung nutzen, dass jede Ecke eines Polygons zu genau zwei anderen Ecken verbunden ist. In unser lineares Programm übersetzt heißt das: für jeden Ecke (jeden Punkt) muss die Anzahl der zu dieser Ecke verbundenen Ecken genau zwei sein. Wann sind nun zwei Ecken i und j verbunden? Genau dann wenn die Variable x_i_j den Wert 1 annimmt. Für ein festes i muss also die Nebenbedingung
x_i_1 + x_i_2 + … + x_i_n = 2
gelten (für jede Ecke sind also genau zwei dazugehörige Kanten „aktiv“). Wenn wir das LP lösen, erhalten wir dann auch wie erwartet eine Menge von Polygonen (die sich auch nicht schneiden – sich schneidende Polygone widersprechen der Optimalität).

Ab da setzt dann die Iteration ein. Wir müssen die Lösung des LPs betrachten, also in diesem Fall die entstandenen Polygone. Wenn ausschließlich Dreiecke entstanden sind, dann sind wir bereits fertig. Andernfalls müssen wir zusätzliche Nebenbedingungen einführen, die bei der nächsten Iteration das Entstehen genau dieser Polygone (die keine Dreiecke sind) verhindert. Man fügt dem LP also zusätzliche Nebenbedingungen hinzu und lässt es wieder vom Solver lösen. Die Lösung testet man wieder auf Nicht-Dreiecke, fügt zusätzliche Nebenbedingungen hinzu um diese zu vermeiden, löst das erweiterte Problem wieder… usw. bis man bei einer Lösung angekommen ist, die nur Dreiecke enthält. Im Beispiel haben bereits 6 Iterationen ausgereicht, um die Lösung zu erhalten.

Jetzt könnte man sich natürlich fragen, warum man nicht einfach von vorneherein Nebenbedingungen hinzufügt, die das Entstehen von Polygonen mit mehr als drei Ecken verbieten bzw. das Entstehen von Dreiecken erzwingen. Dann müsste man das LP doch nur einmal lösen. Das Problem, in das man dabei rennt: man müsste dazu sehr sehr viele Nebenbedingungen hinzufügen. Das macht den Solver langsam (im schlimmsten Fall benötigt der Solver exponentielle Laufzeit in der Anzahl der Nebenbedingungen). Bei 6 Punkten mag es noch ganz gut funktionieren, bei 9 Punkten hat mein Rechner aber bereits über 10 Minuten für die Lösung benötigt. Bei allen 18 Punkten habe ich nach fast zwei Stunden abgebrochen. Vermutlich wäre die Lösung auch nach zwei Tagen noch nicht vorgelegen. Das iterative (man könnte auch sagen: adaptive) Verfahren nimmt dagegen nur einen Bruchteil dieser Nebenbedingungen mit auf und liefert trotzdem dasselbe Ergebnis. Abgesehen davon muss man das Problem in jeder Iteration nicht unbedingt komplett von Neuem lösen: der Simplex-Algorithmus – de facto der Standard-Ansatz zum Lösen linearer Programme – kann das bereits gefundene Optimum für einen „Warmstart“ beim Lösen des erweiterten linearen Programms verwenden. Der Nachteil, dass man den Solver mehrfach anschmeißen muss, verschwindet damit nahezu.

Ich hoffe das war jetzt ausführlich und verständlich genug. Wenn noch Fragen offen sind, nur zu :)

Wofür ich den Algorithmus brauche?

Es ist eine Programmieraufgabe, die ich bekommen habe, gegebene Punkte zu Dreiecken zu verbinden, ohne dass sie sich überschneiden. Die Grundlagen habe ich schon alle (also das Programm, dass die Punkte aus der Datei liest und verarbeitet, ein Bild bereitstellt und die Punkte drauf markiert und auch Dreiecke zeichnen kann), nur mein Algorithmus will nicht.
Und wo bekommt man solche Programmieraufgaben gestellt? ;) Die Frage war eher nach dem tieferen Sinn bzw. dem Verwendungszweck dieses Algorithmus'. Wenn es natürlich rein akademischer Natur ist, dann hat sich die Frage damit beantwortet.

Es war einfach meine erste Idee, das mit den kürzesten Verbindungen zu machen, aber das scheint ja wohl nicht zu gehen. Daher suche ich jetzt eine bessere Möglichkeit.
Eine einfache und effiziente Möglichkeit wird es hier wohl nicht geben – wenn du darauf bestehst, dass die Umfangsumme minimal sein soll. Die Vermutung liegt nahe, dass es sich hierbei um ein NP-schweres Problem handelt. Wenn du diese Forderung fallen lässt und nur noch daran interessiert bist, die Punkte zu überschneidungsfreien Dreiecken zu verbinden, dann könnte die Sache anders aussehen.

Ein spontaner Einfall hierzu: Divide & Conquer. Finde eine Trenngerade, die die Punktemenge in zwei Punktemengen mit jeweils durch drei teilbarer Punkteanzahl trennt. Führe dies rekursiv durch, bis nur noch Punktemengen mit jeweils drei Punkten übrig sind, für die das Problem dann trivial ist. Gegebenenfalls Backtracking verwenden, falls man in eine Sackgasse läuft. Vorteil dieser Methode: leicht parallelisierbar. Nachteil: das Auffinden der Trenngerade könnte nicht ganz trivial sein… möglicherweise gibt es aber dazu schon passende Algorithmen.

Grüße, Matthias
 
Das Problem lässt mich grade nicht los… deshalb verzeiht mir bitte den Doppelpost und das Selbstzitat ;)

Ein spontaner Einfall hierzu: Divide & Conquer. Finde eine Trenngerade, die die Punktemenge in zwei Punktemengen mit jeweils durch drei teilbarer Punkteanzahl trennt. Führe dies rekursiv durch, bis nur noch Punktemengen mit jeweils drei Punkten übrig sind, für die das Problem dann trivial ist.
Verwendet man als Trenngeraden stets Parallelen zu den Koordinatenachsen, lässt sich das Problem sehr schnell und einfach lösen. Sortiere die Punkte zunächst nach einer Achse und teile dann die Menge der Punkte anhand der Sortierung in zwei „Hälften“. Die Anzahl der Punkte in jedem Teil der Partition soll dabei durch drei teilbar bleiben. Führe für die beiden Teile der Partition wieder den Divide-&-Conquer-Schritt aus.

Für die Wahl der Achse, anhand der sortiert und partitioniert werden soll, gibt es verschiedene Möglichkeiten. Am einfachsten ist es, immer abwechselnd x- und y-Achse zu verwenden. Schönere Ergebnisse kriegt man allerdings, wenn die Punktemenge immer entlang der längsten Ausbreitung aufgeteilt wird (d.h. wenn die Ausmaße entlang der x-Achse größer als entlang der y-Achse sind, dann teile anhand der x-Koordinate auf, andernfalls anhand der y-Koordinate).

Ich habe zu diesem Algorithmus mal einen Prototypen in Ruby implementiert (hier mit dem Ansatz „alternierende Achsen“):
Ruby:
# Divide & Conquer
def dac(points, axis=0)
  n = points.size

  if n == 3
    # Dreieck ausgeben
    p points
    return
  end

  # Punkte entlang einer Achse sortieren
  points = points.sort_by {|point| point[axis]}
  # Partition bilden und D&C-Schritt rekursiv durchführen
  size = (n/6)*3
  dac(points[0, size], 1-axis)
  dac(points[size, n - size], 1-axis)
end

# Punkte in ein "float[n][2]"-Array einlesen
points = File.readlines("daten60.txt")
points.map! {|line| line.split.map {|a| a.to_f} }

# Divide & Conquer durchführen
dac(points)

Den Beispieldatensatz daten60.txt und die Ergebnisse der verschiedenen Lösungsansätze findet ihr im Anhang. Das adaptive ILP-Verfahren hat für die Optimallösung übrigens auf meiner 1,6GHz-CPU in 8 Iterationen knapp 50 Sekunden gebraucht. Der Divide-&-Conquer-Algorithmus spuckte sein Ergebnis quasi sofort aus.

Grüße, Matthias

PS: Da die Dateinamen zu den Bildern nicht angezeigt werden, hier die Beschreibung v.l.n.r.: Adaptives ILP-Verfahren, Divide & Conquer mit alternierenden Achsen, Divide & Conquer mit „längster Achse“
PPS: Eigentlich beschämend, dass ich nicht gleich auf die Idee gekommen bin ;)
 

Anhänge

  • daten60.txt
    2,1 KB · Aufrufe: 36
  • solution-adaptive-ilp.gif
    solution-adaptive-ilp.gif
    17,2 KB · Aufrufe: 38
  • solution-divide-and-conquer-alternating-axes.gif
    solution-divide-and-conquer-alternating-axes.gif
    19,5 KB · Aufrufe: 44
  • solution-divide-and-conquer-longest-axis.gif
    solution-divide-and-conquer-longest-axis.gif
    17,6 KB · Aufrufe: 49
Wow, erst man vielen Dank für die ausführliche Hilfe! Ich glaube, ich habe sogar das meiste davon verstanden (dank einem Semester Lineare Algebra I an der TUHH das ich nebenbei gemacht habe und ein paar Büchern über die Komplexivität von Problemen), aber das heißt noch lange nicht, dass ich es implementieren könnte. Aber ich finde den Ansatz, es mit solchen linearen Funktionen zu machen. Man könnte das ganze bestimmt auch in eine Matrix schreiben und dann Lösen, oder? Aber ich glaube, das wird zu kompliziert für mein Programm :p

Den Divide & Conquer Ansatz finde ich auch sehr gut, ich werde mich mal dran machen, das auszuprobieren. Klingt effizient und plausibel, melde mich dann mit Ergebnissen zurück.
 
Soo, hier kommt gleich das Feedback. Ich habe mal versucht, deine Ruby-Funktion in C# umzuwandeln, aber habe keine so einfache Methode gefunden, das Array (oder in meinem Fall eine Liste) zu splitten. Daher habe ich meine eigene geschrieben, die es aber irgendwie nicht so wirklich tut.

Hier mal mein Code:
C#:
    class DivideAndConquerAlgorithm : TriangleAlgorithm
    {
        public override void DrawTriangles()
        {
            divideAndConquer(coordinates, 0);
        }

        private void divideAndConquer(List<Coordinate> coordinates, int axis)
        {
            int n = coordinates.Count;

            if (n == 3)
            {
                drawTriangle(coordinates);
                return;
            }

            if (axis == 0)
                coordinates.Sort(delegate(Coordinate c1, Coordinate c2) { return c1.X.CompareTo(c2.X); });
            if (axis == 1)
                coordinates.Sort(delegate(Coordinate c1, Coordinate c2) { return c1.Y.CompareTo(c2.Y); });

            int size = (n / 6) * 3;

            divideAndConquer(splitList<Coordinate>(coordinates, 0, size), 1 - axis);
            divideAndConquer(splitList<Coordinate>(coordinates, size, n - size), 1 - axis);
        }

        private List<T> splitList<T>(List<T> list, int begin, int end)
        {
            List<T> result = new List<T>();

            for (int i = begin; i < end; i++)
                result.Add(list[i]);

            return result;
        }
    }
Anmerkung zum Code:
Die Variable "coordinates", die alle Punkte als "Coordinate"-Objekte enthält, sowie die "drawTriangle"-Funktion werden aus der Klasse "TriangleAlgorithm" vererbt.

Ich bekomme sofort nach dem Start eine StackOverflowException bei der Zeile "List<T> result = new List<T>();". Ich verstehe das nicht, warum hier ein Stack Overflow? Müsste doch entweder bedeuten, dass die Liste voll ist (ist sie aber nicht, wird ja grade erst initialisiert) oder aber dass der RAM voll ist, was auch nicht sein kann (die Auslastung geht ca. 10MB hoch, aber es sind immer noch 1300MB frei!

Was mache ich falsch?

EDIT:
Problem behoben, es gab eine Endlosschleife, da meine divideAndConquer-Funktion eine Liste der Länge 0 als Input bekommen hat, das ist ungleich 3, daher hat er immer weiter und weiter gemacht. Das Problem war diese Zeile:
C#:
            divideAndConquer(splitList<Coordinate>(coordinates, size, n - size), 1 - axis);
Das "n-size" muss nur "n" heißen, er soll ja von "size" bis "n" splitten. Das gleiche Problem hätte bei dir auch auftreten müssen!
Puh, das war grad eine Stunde lang Schritt für Schritt durch das Programm gehen und alle Variablen analysieren, Breakpoints setzen, Ablaufdiagramme erstellen, ... :p

EDIT2:
Was mir grade zu "n-size" einfällt: Wenn du das als Länge angesehen hast, geht das bei dir natürlich prima. Ich habe es als Ende des Intervalls angesehen!

Im Anhang mein Ergebnis mit den Daten, die du bei dir angehängt hast.
 

Anhänge

  • dreiecke02.jpg
    dreiecke02.jpg
    47,1 KB · Aufrufe: 44
Zuletzt bearbeitet:
Ich habe mal versucht, deine Ruby-Funktion in C# umzuwandeln, aber habe keine so einfache Methode gefunden, das Array (oder in meinem Fall eine Liste) zu splitten.
Ich bin kein .NET-Experte, aber möglicherweise hilft hier die Methode GetRange weiter.

EDIT2:
Was mir grade zu "n-size" einfällt: Wenn du das als Länge angesehen hast, geht das bei dir natürlich prima. Ich habe es als Ende des Intervalls angesehen!
Ja, die Methode ist in Ruby so definiert, dass sie einen Startindex und eine Länge übergeben bekommt.

Im Anhang mein Ergebnis mit den Daten, die du bei dir angehängt hast.
Na das sieht doch prima aus! Schön, dass es funktioniert.

Grüße, Matthias
 
Die Divide&Conquer-Methode. Von der Idee so einfach und umgesetzt auch noch schnell und in meinen Augen sehr sauber. WOW.

mfg chmee
 

Neue Beiträge

Zurück