Phase Transitions of Traveling Salesperson Problems solved with Linear Programming and Cutting Planes

In diesem Artikel wird ein Ensemble von Problemen des Handlungsreisenden (TSP) eingeführt, das abhängig von einem Parameter \(\sigma\) von einer trivial einfach zu lösenden Konfiguration, nämlich Städte, die äquidistant auf einem Kreis angeordnet sind, zum zufälligen euklidischen TSP in der Ebene interpoliert.

Einfach und schwierig zu lösende TSP Konfigurationen

Danach werden mittels linearer Programmierung einige Phasenübergänge festgestellt, ab welchen Werten von \(\sigma\) das Problem schwierig zu lösen wird. Zu zwei dieser Übergänge werden strukturelle Eigenschaften der optimalen Lösung gefunden, die sich an dieser Stelle ebenfalls charakteristisch ändern. Da die optimale Lösung nicht von der Lösungsmethode abhängt, sind diese Phasenübergänge also nicht nur von Bedeutung für das spezielle Lineare Programm bzw. den Algorithmus der zu dessen Lösung genutzt wurde, sondern fundamentale Eigenschaft dieses TSP Ensembles.

Im Detail haben wir die klassische Formulierung von Dantzig genutzt:

\begin{align*} \label{eq:objective} &\text{minimize} & \sum_i \sum_{j<i} c_{ij} x_{ij}\\ \label{eq:int} &\text{subject to} & x_{ij} &\in \{0,1\}\\ %\mathbb{Z}\\ \label{eq:inout} & & \sum_{j} x_{ij} &= 2& & \forall i \in V \\ \label{eq:sec} & & \sum_{i \in S, j \notin S} x_{ij} &\ge 2& & \forall S \varsubsetneq V, S \ne \varnothing \end{align*}

Hier ist \(c_{ij}\) die Distanzmatrix zwischen allen Paaren von Städten aus \(V\) und \(x_{ij}\) die gesuchte Adjazenzmatrix, also \(x_{ij} = 1\), wenn \(i\) und \(j\) aufeinanderfolgende Stationen der Tour sind und \(x_{ij} = 0\) sonst. Die erste Zeile minimiert also die Strecke der Tour. Um zu vermeiden, dass wir die triviale Lösung \(x_{ij}=0\), also „wenn wir zu Hause bleiben müssen wir am wenigsten Strecke zurücklegen“ finden, zwingt die dritte Zeile unseren Handlungsreisenden seine Tour so zu planen, dass in Summe zwei Striche an jede Stadt gezeichnet werden — genug, um hinein und wieder hinaus zu reisen. Allerdings, ist unser Handlungsreisender clever und würde versuchen uns auszutricksen, indem er halbe Striche einzeichnen würde, wie in einem anderen Blogeintrag visualisiert. Deshalb ist die Bedingung in der zweiten Zeile nötig, die die Einträge in der Adjazenzmatrix auf ganze Zahlen beschränkt. Dann bleibt nur noch das Problem, dass mehrere Routen, die nicht verbunden sind erlaubt wären, sodass wir sie durch die letzte Zeile verbieten: die Subtour Elimination Constraints. Der aufmerksame Leser mag schon erkannt haben, dass es für jede Untermenge von Städten so eine Constraint definiert, also exponentiell viele in der Anzahl der Städte. Die Lösung zu dieses Problem liegt darin, dass nur sehr wenige wirklich gebraucht werden, sodass man das Problem ohne diese Constraint löst, testet ob eine verletzt ist, was mittels der Berechnung eines minimum cut sehr schnell geht und dann eine einzelne Constraint, die diese Konfiguration verbietet hinzufügt. Diese Methode iterativ Constraints hinzuzufügen wird meist als Cutting Planes bezeichnet.

Also haben wir einen schnellen Algorithmus für das Problem des Handlungsreisenden gefunden? Nein, leider können wir den Millenium Preis noch nicht beanspruchen. Es gibt keinen bekannten Algorithmus, der dieses Problem unter Erfüllung der zweiten Zeilen, also Beschränkung auf ganzzahlige Lösungen lösen kann. Aber sobald wir diese Bedingung fallen lassen, können wir klassische Verfahren der linearen Programmierung nutzen, um dieses Problem effizient zu lösen. Dies wird auch Relaxation genannt. Die Länge der Strecke ist immer eine untere Schranke für die tatsächliche Lösung. Und wenn unsere Lösung per Zufall ganzzahlig ist, können wir uns sicher sein, die Optimale Lösung gefunden zu haben.

Als Ordnungsparameter des Phasenübergangs zwischen leichten und schweren Konfigurationen dient uns also die Wahrscheinlichkeit, dass mittels eines Simplex-Solvers eine ganzzahlige, und damit optimale, Lösung gefunden wird. Ohne die Subtour Elimination Constraints, fällt der Phasenübergang auf den Punkt, an dem sich die optimale Lösung erstmals von der Reihenfolge der Städte des ursprünglichen Kreises unterscheidet. Mit den Subtour Elimination Constraints, fällt der Phasenübergang auf den Punkt, wo die optimale Tour anfängt von einem Zickzack-Kurs auf große Meander zu wechseln. Dies wird durch die geometrische Gewundenheit, die Tortuosität,

\begin{align*} \tau = \frac{n-1}{L} \sum_{i=1}^{n} \left( \frac{L_i}{S_i}-1 \right). \end{align*}

ermittelt, die an diesem Punkt maximal wird. Hier wird die Tour in \(N\) Teilstücke mit gleichem Vorzeichen der Krümmung unterteilt und für jedes Teilstück das Verhältnis von direkter Ende-zu-Ende-Distanz \(S_i\) zu der Länge entlang der Tour \(L-i\) summiert.

Wir haben also kontinuierliche Phasenübergänge in der Schwierigkeit dieses Problems mittels linearer Programmierung detektiert und sie mit strukturellen Änderungen des Verhaltens in Verbindung gebracht.

A Fractal A Day

Vor einiger Zeit habe ich ein Programm geschrieben, das verschiedene Typen von Fraktalen generiert. Da viele Methoden Fraktale zu generieren relativ einfach zu parallelisieren sind und großen Bedarf an Rechenkraft haben, habe ich mich entschieden es in Rust zu implementieren. Bei Interesse kann das Programm von Github bezogen werden.

Da Fraktale nett anzuschauen sind, ist dieser Beitrag voller hochaufgelöster Bilder. Damit diese Seite dennoch flüssig geladen wird — auch bei langsamen Verbindungen, habe ich extra für diesen Eintrag in die Technik dieses Blogs eingegriffen. Außerdem gibt es @AFractalADay auf Twitter, der täglich ein zufälliges Fraktal tweetet.

Escape Time

Die erste Klasse von Fraktalen, die ich hier zeigen möchte, wird definiert durch das Konvergenzverhalten des wiederholten Anwendens einer Funktion. Was genau dieser Satz bedeutet, lässt sich am besten an einem Beispiel erklären.

Mandelbrot-Menge

Das vermutlich bekannteste Fraktal ist das Apfelmännchen, das die Mandelbrotmenge visualisiert. Das ist die Menge der komplexen Zahlen \(c = x + iy,\) die nicht konvergieren, wenn die Funktion \(f_c(z) = z^2 + c\) wiederholt angewendet wird. Also wenn die Folge

$$f_c(0), f_c(f_c(0)), f_c(f_c(f_c(0))), ...$$

gegen einen endlichen Wert strebt.

Wenn man jeden Punkt \(c\) auf der komplexen Ebene entsprechend des Konvergenzverhaltens bezüglich dieser Folge einfärbt — schwarz wenn es konvergiert, blau für langsame Divergenz, rot für schnelle Divergenz — erhält man ein solches Bild:

Zoom auf das Apfelmännchen

Dies ist ein Zoom auf den Rand des Apfelmännchens. Tatsächlich ist die Mandelbrotmenge kein Fraktal im eigentlichen Sinne, da seine fraktale Dimension 2 ist — der schwarze Bereich füllt eine Fläche.

Es einfach möglich dieses Fraktal zu rastern und dabei jeden Pixel parallel zu berechnen. Eine naive Implementierung könnte wie folgt aussehen.

// convenient iterators
#[macro_use] extern crate itertools;
use itertools::Itertools;

// parallelism
extern crate rayon;
use rayon::prelude::*;

// complex numbers
extern crate num;
use num::complex::Complex;

fn raster(resolution: (u32, u32)) -> Vec<u64> {
    let (x, y) = resolution;

    // generate the points, we want to raster
    let pixels: Vec<(u32, u32)> = iproduct!(0..y, 0..x).collect();

    // start a parallel iterator on the points ...
    pixels.par_iter()
          .map(|&(j, i)| {
              // ... mapping every point ...
              let z = map_to_cplx_plane(i, j);
              // ... to the number of iterations needed to diverge
              time_to_diverge(z)
          })
          .collect()
}

fn map_to_cplx_plane(x: u32, y u32) -> Complex<f64> {
    // TODO: here we need to get the offset and scale somehow
    let x = (x-x_offset) as f64 * x_scale;
    let y = (y-y_offset) as f64 * y_scale;
    Complex<f64> {re: x, im: y}
}

fn time_to_diverge(mut state: Complex<f64>) -> u64 {
    // threshold is 2^2, since we compare to the square of the norm
    // as soon as the norm is >= 2 it is sure to diverge
    let threshold = 4.;

    // abort after 1000 iterations
    let max_count = 1000;

    let c = state;

    let mut ctr = 0u64;
    while {
        state = state * state + c;
        ctr += 1;

        state.norm_sqr() < threshold && ctr < max_count
    } {}
    ctr
}

Julia-Mengen

Nahe verwandt sind die Julia-Mengen. Hier benutzt man die gleiche Funktion \(f_c\), allerdings färbt man jeden Punkt \(z\) entsprechend seines Konvergenzverhaltens bei einem festen Parameter \(c\).

Ein Julia-Fraktal

Tatsächlich ist jede beliebige Funktion \(f\) erlaubt und nicht nur die oben erwähnte quadratische. Mit unkonventioneller Zuordnung von Farben zu Divergenzzeiten ergibt sich mit \(f(z) = (-2.6-i) \cosh(z)\) dieses Bild:

Ein weiteres Julia-Fraktal

Newton-Fraktal

Das Newton-Verfahren zur Findung von Nullstellen startet an einem beliebigen Punkt auf einer Kurve, und berechnet die Nullstelle der Tangente an diesem Punkt. Mit der Tangente dieses Punktes wird genauso verfahren. Dabei sollten sich die so erhaltenen Punkte immer dichter einer Nullstelle nähern. Bei einer komplexen Funktion können wir dies für jeden Startpunkt iterieren. Jeder Punkt wird gegen eine Nullstelle konvergieren, der wir eine Farbe zuordnen und den Punkt mit dieser Farbe einfärben. Wenn wir die Sättigung davon abhängig machen, wie schnell die Konvergenz ist, sieht das Ergebnis für \(f(x) = z^4 + 5^{z+i} + 15\) so aus.

Newton Fraktal für f(x) = z^4 + 5^{z+i} + 15

Chaos Game

Eine große Klasse von Fraktalen lässt sich mit dem Chaos Game erzeugen. Man benutzt dazu mindestens zwei Abbildungen \(f_1(z)\) und \(f_2(z)\), die jeweils einen Punkt \(z\) auf einen anderen Punkt abbilden. Man wählt einen Punkt zum Starten, bildet ihn mit einer Zufälligen der beiden Abbildungen ab, zeichnet den resultierenden Punkt ein und wiederholt dies sehr oft.

Dieser Algorithmus ist inherent sequenziell, allerdings kann man parallel an vielen verschiedenen Punkten starten und die Ergebnisse dieser unabhängigen Markovketten in einem Bild zusammenführen.

In Rust könnte der entsprechende Codeschnipsel so aussehen:

extern crate num_cpus;
use std::thread;
use std::sync::mpsc::channel;

let cpus = num_cpus::get();

// create a transmitter, receiver pair
let (tx, rx) = channel();
for _ in 0..cpus {
    // clone a transmitter for each thread
    let tx = tx.clone();

    // generator yielding the points from the chaos game
    // using a random seed
    let sampler = get_sampler();

    // we need some histogram implementation
    let mut hist = Histogram::new();

    thread::spawn(move || {
        // feed the samples into the histogram
        hist.feed(sampler.take(iterations_per_task));
        // send the finished histogram to the receiver
        tx.send(hist).unwrap();
    });
}

// collect all parallel computed histograms into main_hist
let mut main_hist = Histogram::new();
for _ in 0..cpus {
    let h = rx.recv().unwrap();
    main_hist.merge(&h);
}

Sierpinski-Dreieck und Bernsley-Farn

Mit dieser Methode kann man alte Bekannte wie das Sierpinski-Dreieck erzeugen.

Sierpinski-Dreieck

Dazu benötigt man die drei affinen Transformationen, die man alle mit gleicher Wahrscheinlichkeit auswählt:

$$\begin{align} f_1(\vec z) &=\begin{pmatrix} -1/4 & \sqrt 3 / 4 \\ -\sqrt 3 / 4 & -1/4 \end{pmatrix} \cdot \begin{pmatrix} z_x \\ z_y \end{pmatrix} + \begin{pmatrix} -1/4\\ \sqrt 3 / 4 \end{pmatrix}\\ f_2(\vec z) &=\begin{pmatrix} 1/2 & 0 \\ 0 & 1/2 \end{pmatrix} \cdot \begin{pmatrix} z_x \\ z_y \end{pmatrix} + \begin{pmatrix} 1/4\\ \sqrt 3 / 4 \end{pmatrix}\\ f_3(\vec z) &=\begin{pmatrix} -1/4 & -\sqrt 3 / 4 \\ \sqrt 3 / 4 & 1/4 \end{pmatrix} \cdot \begin{pmatrix} z_x \\ z_y \end{pmatrix} + \begin{pmatrix} 1\\ 0 \end{pmatrix} \end{align}$$

Ein anderes berühmtes Beispiel ist der Bernsley-Farn. Um ihn zu erzeugen, benutzt man die folgenden vier affinen Abbildungen, die man mit den Wahrscheinlichkeiten

$$p_1 = 0.01, p_2 = 0.85, p_3 = 0.07, p_4 = 0.07$$

verwendet:

$$\begin{align} f_1(z) &=\begin{pmatrix} 0.16\\ 0 \end{pmatrix}\\ f_2(z) &=\begin{pmatrix} 0.85 & 0.04 \\ 0 & -0.04 \end{pmatrix} \cdot \begin{pmatrix} z_x \\ z_y \end{pmatrix} + \begin{pmatrix} 0.85\\ 1.6 \end{pmatrix}\\ f_3(z) &=\begin{pmatrix} 0.2 & -0.26 \\ 0 & 0.23 \end{pmatrix} \cdot \begin{pmatrix} z_x \\ z_y \end{pmatrix} + \begin{pmatrix} 0.22\\ 1.6 \end{pmatrix}\\ f_4(z) &=\begin{pmatrix} -0.15 & 0.28 \\ 0 & 0.26 \end{pmatrix} \cdot \begin{pmatrix} z_x \\ z_y \end{pmatrix} + \begin{pmatrix} 0.24\\ 0.44 \end{pmatrix}\\ \end{align}$$

Als Ergebnis erhält man diesen Farn.

Bernsley-Farn

Fractal Flame

Fractal Flame ist der Name einer Klasse von Zufallsfraktalen, die nach dem gleichen Muster wie oben aus einer Reihe affiner Transformationen \(A_i\) bestehen. Zusätzlich können die affinen Transformationen mit einer nichtlinearen Variation \(V_j\) erweitert werden, sodass \(f_i(\vec z) = V_j(A_i(\vec z))\) (oder Linearkombinationen dieser Variationen). Zur Visualisierung werden die Punkte nicht direkt gezeichnet, sondern in ein Histogramm eingetragen, aus dem die Farbintensitäten typischerweise logarithmisch berechnet werden.

Fractal Flame, 'Horseshoe' Variation

Hier wird jedem \(f_i\) ein Farbton zugeordnet. Die Farbe eines Punktes ist eine Mischung dieser Farben, die widerspiegelt, wie oft eine Abbildung genutzt wurde, um an diesen Punkt zu gelangen.

Interessanterweise sind diese Systeme anscheinend sehr anfällig für schlechte Zufallszahlen, was sich in „Löchern“ in den ansonsten glatten Flächen bemerkbar macht.

Möbius Flame

Diese Fraktale sind nahezu identisch zu den Fractal Flames, nur dass anstatt von affinen Transformationen Möbius Transformationen auf der komplexen Ebene genutzt werden.

$$f_i(z) = \frac{a_i z + b_i}{c_i z + d_i}$$

Möbius Flame

Wie findet man „gute“ Parameter?

Offenbar hat dieser Typ von Fraktal sehr viele freie Parameter. Um hübsche Resultate zu generieren, müssen sie angepasst werden. Tatsächlich gibt es mit electric sheep (ich hoffe stark, dass es eine Blade Runner Referenz ist) ein Crowdsourcing-Projekt, das mithilfe von evolutionären Algorithmen und dem Feedback von Menschen besonders ansehnliche Fraktale erzeugt.

Für mein Programm habe ich eine simplere Methode genutzt. Damit man ein Fraktal gut sehen kann, sollte seine fraktale Dimension größer als 1 sein. Abschätzbar ist es relativ einfach über die Korrelations-Dimension. Dazu misst man die paarweisen Abstände von Punkten und misst den Exponenten ihrer kumulativen Verteilungsfunktion.

Kombiniert mit einigen Heuristiken, die zu langgestreckte Fraktale verhindert, sind die Ergebnisse meist ansprechend

Weitere Fraktale

Es gibt natürlich viel mehr Typen von Fraktalen. Auch wenn @AFractalADay sie bisher nicht zeichnen kann, habe ich einige Bilder angefertigt, die ich hier auch gerne zeigen möchte.

Diffusionsbegrenztes Wachstum

Diffusionsbegrenztes Wachstum bildet das Wachstum von Kristallen in stark verdünnten Lösungen ab. Man startet mit einem Seed und lässt dann einzelne Teilchen diffundieren, bis sie auf dem Nachbarfeld eines Seeds landen, wo sie dann bleiben und Teil des Seeds werden. Dieser Prozess bildet verästelte Strukturen aus.

Diffusionsbegrenztes Wachstum

Random Walks

Einige Arten von Random Walks haben eine fraktale Dimension zwischen 1 und 2, was sie zu ansehnlichen Fraktalen machen sollte. Der Smart Kinetic Self Avoiding Walk, der in meinem rsnake die Strategie des Autopiloten ist, hat eine fraktale Dimension von \(\frac{7}{4}\). 100000 Schritte sehen so aus:

Smart Kinetic Self Avoiding Walk, 100000 Schritte

Vicsek

Das Vicsek-Modell wurde 1995 vorgeschlagen, um das Schwarmverhalten von Vögeln oder Fischen zu modellieren. Die Idee ist, dass jedes Individuum seine Bewegungsrichtung an der seiner Nachbarn anpasst. Wenn jedes Individuum genügend Nachbarn hat und die Störeinflüsse nicht zu groß sind, bilden sich Schwärme. Videos von solchen Schwärmen werden auf allen größeren Konferenzen der Statistischen Physik gezeigt — und jetzt auch hier.

Auf GitHub findet sich das Programm, das ich für obiges Video geschrieben habe. Es ist in Rust geschrieben und zeigt die Simulation per Piston auf dem Bildschirm.

Ich habe sehr großen Gefallen an Rust gefunden — gerade für ein Projekt wie dieses scheint es ideal geeignet. Es ist so schnell wie C, aber man muss sich keinerlei Gedanken um den Speicher machen und einige andere Fehlerklassen, die der Compiler direkt verhindert. Rayon macht Parallelisierung so einfach wie OpenMP — mit dem Vorteil, dass der Compiler einen Fehler ausgibt, falls es eine Variable gibt, aus der parallel gelesen und geschrieben wird.

Als Beispiel, warum ich Rust als sehr leserlich und elegant empfinde, möchte ich folgendes (unvollständige) Beispiel ansehen.

pub enum Proximity {
    Neighbors(usize),
    Radius(f64)
}

pub struct Vicsek {
    proximity: Proximity,
}

impl Vicsek {
    fn update(bird: &mut Bird) {
        match self.proximity {
            Proximity::Neighbors(n) => self.update_direction_neighbors(bird, n, noise),
            Proximity::Radius(r) => self.update_direction_disk(bird, r, noise),
        }
    }
}

Die Methode update() passt die Richtung an, in die ihr Argument im nächsten Zeitschritt fliegen soll. In meiner Simulation gibt es zwei Möglichkeiten: entweder orientiert man sich an seinen n nächsten Nachbarn oder an allen Vögeln innerhalb eines Radius von r. Der Datentyp Proximity kann eines von beiden beinhalten — welches vorhanden ist, kann elegant per Pattern-Matching ermittelt werden.

Brauche ich länger, um Rust zu schreiben als C oder C++? Vermutlich, aber ich verbringe weniger Zeit mit dem Debuggen. Netto also mehr Spaß.