4. Listen
4.7 Kombinatorik
Dieses Kapitel ist ein kleiner Ausflug in die diskrete Mathematik, insbesondere die Mengenlehre. Einige Dinge werden wir aber tatsächlich später brauchen, zum Beispiel um ein Gitter aus Punkten graphisch darzustellen.
Grundbegriff der Mengenlehre ist die Menge, z.B.
und auch nicht dafür, wie oft ein Element genannt wurde, daher
Sie interessiert sich nur dafür, welche Elemente überhaupt enthalten sind. Daher gilt
Dies ist eine Menge aus vier (verschiedenen) Elementen.
Wir können aus zwei Mengen neue Mengen bauen, zum Beispiel
- die Vereinigung
, bestehend aus allen Elementen, die in oder in sind (oder in beiden); - die Schnittmenge
, bestehend aus den Elementen, die sowohl in als auch in sind; - die Differenzmenge
, bestehend aus den Elementen, die in sind, aber nicht in .
Wir haben spezielle Symbole
bedeutet, dass ein Element der Menge ist; bedeutet, dass eine Teilmenge von ist; dass also jedes Element , das in ist, auch in ist; als logische Formel also, dass gilt.
Manchmal drehen wir die Symbole um und schreiben
Definition 4.7 (Potenzmenge). Sei
Hier ein Beispiel:
Beachten Sie, dass
Theorem 4.8 Sei
Beweis.
Wir verwenden Induktion über
Für den Induktionsschritt haben wir eine Menge
Es gibt also genau
Die Potenzmenge
Für uns als Programmierer ist nun interessant, wie wir List
. Da spielen zwar Reihenfolge
eine Rolle, eine Liste repräsentiert eine Menge also nur unzureichend, aber für unsere
jetzigen Zwecke ist es ausreichend. Beachten Sie, dass der induktive Beweis oben
(insbesondere die Unterteilung in die Teilmengen, die
powerSet : List datatype -> List (List datatype)
powerSet list =
case list of
[] ->
[ [] ] -- das entspricht der Menge {{}}
a :: rest ->
let
thoseWithout_a : List (List datatype)
thoseWithout_a =
powerSet rest
thoseWith_a =
List.map ((::) a) thoseWithout_a
in
thoseWithout_a ++ thoseWith_a
Ein paar Erklärungen: in Zeile 12-14 berechnen wir List.map ((::) a) thoseWithout_a
zu verdauen, müssen Sie
vieles von dem hervorholen, das wir in den letzten Kapiteln gelernt haben:
- Das
::
ist ein Konstruktor für den Custom TypeList
. Wir schreiben ihn normalerweise in Infixform, so stellt also4 :: [2,6,7]
eine neue Liste[4, 2, 6, 7]
her. Wenn wir(::)
schreiben, so erhalten wir das gleiche in Präfixform, also(::) 4 [2, 6, 7]
. Der Konstruktor(::)
hat zwei Parameter als Ladegut: das Element (hier4
) und die Liste, der es vorangestellt werden soll (hier[2, 6, 7]
). -
Konstruktoren mit Ladegut verhalten sich syntaktisch wie Funktionen. Das
(::)
hat also Signatura -> List a -> List a
. - Im Ausdruck
(::) a
füttern wir(::)
mit nur einem Argument, hier vom Typdatatype
. Es wird also zu einer Funktion mit SignaturList datatype -> List datatype
. Was macht diese Funktion? Sie stellt Ihrem Argument (einer Liste) den Werta
voran und erzeugt so eine neue Liste. Wir verwenden hier also Currying. -
Im Ausdruck
List.map ((::) a) thoseWithout_a
wird nun die gecurryte Funktion((::) a)
("nimm das Argument und klebea
vorne dran) auf jedes Element vonthoseWithout_a
angewandt. Mengentheoretisch also: aus jedem wird die Menge gebildet.
Nun dürfen Sie es ausprobieren:
type Wer = Ich | Du | Er | Sie
powerSet [Ich, Du, Er, Sie]
[[],[Sie],[Er],[Er,Sie],[Du],[Du,Sie],[Du,Er],[Du,Er,Sie],[Ich],[Ich,Sie],[Ich,Er],[Ich,Er,Sie],[Ich,Du],[Ich,Du,Sie],[Ich,Du,Er],[Ich,Du,Er,Sie]]
Teilmengen bestimmter Größe
Sei
Um für eine Menge (in Elm: eine Liste)
hasLength : Int -> List a -> List a
hasLength k list = List.length list == k
List.filter (hasLength 2) (powerSet [Ich, Du, Er, Sie])
[[Er,Sie],[Du,Sie],[Du,Er],[Ich,Sie],[Ich,Er],[Ich,Du]]
Das geht, ist aber verschenderisch. Es werden erst alle
List.filter (hasLength 2) (powerSet (List.range 1 20))
liefert zwar das richtige Ergebnis (alle 190 Teilmengen der Größe 2), berechnet
aber erst einmal alle
Theorem 4.9 Es gilt
Beweis. Die ersten beiden Aussagen sind recht einfach.
Für
Falls
Wir können wieder diesem Beweis in Code umsetzen: wenn die Menge list
)
nichtleer ist, also die Form x :: rest
hat, dann können wir rekursiv
die Teilmengen von rest
mit Größe rest
mit Größe x
voranstellen, was mit List.map
ganz einfach geht.
Übungsaufgabe 4.7.1
Implementieren Sie eine Funktion
choose : Int -> List a -> List (List a)
,
die die Menge einer Teilmengen gegebener Größe berechnet.
Das Cartesische Produkt
Für zwei Mengen
also die Menge aller geordneten Paare, die sich bilden lassen. Das funktioniert auch wunderbar
mit unendlichen Mengen. So sind die Elemente von
Ich will ich Elm nun eine Funktion cartesianProduct
schreiben, die zwei Listen
nimmt und eine Liste von Paaren (aller möglicher Paare) zurückgibt. Zum Beispiel:
type Wochentag = Montag | Dienstag | Mittwoch | Donnerstag | Freitag | Samstag | Sonntag
wochentage = [Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag]
type Tageszeit = Vormittag | Nachmittag | Abend
tageszeiten = [Vormittag, Nachmittag, Abend]
cartesianProduct wochentage tageszeiten
[(Montag,Vormittag),(Montag,Nachmittag),(Montag,Abend),(Dienstag,Vormittag),(Dienstag,Nachmittag),(Dienstag,Abend),(Mittwoch,Vormittag),(Mittwoch,Nachmittag),(Mittwoch,Abend),(Donnerstag,Vormittag),(Donnerstag,Nachmittag),(Donnerstag,Abend),(Freitag,Vormittag),(Freitag,Nachmittag),(Freitag,Abend),(Samstag,Vormittag),(Samstag,Nachmittag),(Samstag,Abend),(Sonntag,Vormittag),(Sonntag,Nachmittag),(Sonntag,Abend)] : List (Wochentag, Tageszeit)
Wie gehe ich vor? Ich will die erste Liste durchgehen, und für jedes Element x
der
ersten Liste
folgendes tun: ich will es mit jedem Element y
der zweiten Liste zu einem Paar
(x, y)
kombinieren. Tasten wir uns langsam vor:
Tuple.pair Montag Vormittag
(Montag, Vormittag) : (Wochentag, Tagezeit)
Tuple.pair Montag
<function> : b -> ( Wochentag, b )
List.map (Tuple.pair Montag) tageszeiten
[(Montag,Vormittag),(Montag,Nachmittag),(Montag,Abend)] : List ( Wochentag, Tageszeit )
Der letzte Ausdruck paart den Wert Montag
mit jedem Element der Liste
[Vormittag, Nachmittag, Abend]
. Genau das wollen wir! Aber nicht nur
für Montag
, sondern für jedes Element der ersten Liste.
Wir brauchen nun tatsächlich eine Helferfunktion pairXwithAllInList:
pairXwithAllInList : List b -> a -> List (a,b)
pairXwithAllInList list x = List.map (Tuple.pair x) list
List.map (pairXwithAllInList tageszeiten) wochentage
[[(Montag,Vormittag),(Montag,Nachmittag),(Montag,Abend)],[(Dienstag,Vormittag),(Dienstag,Nachmittag),(Dienstag,Abend)],[(Mittwoch,Vormittag),(Mittwoch,Nachmittag),(Mittwoch,Abend)],[(Donnerstag,Vormittag),(Donnerstag,Nachmittag),(Donnerstag,Abend)],[(Freitag,Vormittag),(Freitag,Nachmittag),(Freitag,Abend)],[(Samstag,Vormittag),(Samstag,Nachmittag),(Samstag,Abend)],[(Sonntag,Vormittag),(Sonntag,Nachmittag),(Sonntag,Abend)]] : List (List ( Wochentag, Tageszeit ))
Fast. Aber nicht ganz. Analysieren wir den letzten Ausdruck
List.map (pairXwithAllInList tageszeiten) wochentage
. Das erste Argument von
List.map
,
also pairXwithAllInList tageszeiten
, ist ein unvollständiger Funktionsaufruf und
gibt
uns daher eine gecurryte Funktion. Sie wartet auf ein a
-Element, das sie dann mit
jedem Element in tageszeiten
zu einem (a, Tageszeit)
-Paar verkuppeln
kann. Und diese gecurryte Funktion wenden wir nun auf jedes Element in der Liste
wochentage
an. Leider gibt dies uns eine Liste von Listen zurück. Wir wollen diese
noch zu einer großen Gesamtliste verschmelzen. Zwei Listen verknüpft man mittels
List.append
oder ::
oder in Präfixschreibweise (::)
.
Um eine Liste von Listen zu einer großen zu vereinigen, können wir List.foldl
verwenden:
List.foldl List.append [] [ [1,2], [3,4], [5,6]]
[5,6,3,4,1,2] : List number
Knapp daneben, wenn uns die Reihenfolge wichtig ist. Das liegt nun daran, dass
List.foldl
das neue Element (zum Beispiel die Liste [3,4]
) nimmt und dann als erstes
Argument von List.append
nimmt, also List.append [3,4] [1,2]
. Genau
falschrum also.
Wir können jedoch einfach List.foldr
verwenden:
List.foldr List.append [] [ [1,2], [3,4], [5,6]]
[1,2,3,4,5,6] : List number
oder das vorgefertige List.concat
. Dann müssen wir gar nicht nachdenken:
List.concat [ [1,2], [3,4], [5,6]]
[1,2,3,4,5,6] : List number
Zurück zum cartesischen Produkt nun also: da uns
List.map (pairXwithAllInList tageszeiten) wochentage
eine Liste von
Listen von Paaren zurückgibt, müssen wir diese noch verschmelzen:
List.concat (List.map (pairXwithAllInList tageszeiten) wochentage)
[(Montag,Vormittag),(Montag,Nachmittag),(Montag,Abend),(Dienstag,Vormittag),(Dienstag,Nachmittag),(Dienstag,Abend),(Mittwoch,Vormittag),(Mittwoch,Nachmittag),(Mittwoch,Abend),(Donnerstag,Vormittag),(Donnerstag,Nachmittag),(Donnerstag,Abend),(Freitag,Vormittag),(Freitag,Nachmittag),(Freitag,Abend),(Samstag,Vormittag),(Samstag,Nachmittag),(Samstag,Abend),(Sonntag,Vormittag),(Sonntag,Nachmittag),(Sonntag,Abend)] : List ( Wochentag, Tageszeit )
Gut, das ist genau das, was wir wollen. Und nun zusammenfassend als Funktion:
pairXwithAllInList : List b -> a -> List ( a, b )
pairXwithAllInList list x =
List.map (Tuple.pair x) list
cartesianProduct : List a -> List b -> List ( a, b )
cartesianProduct listA listB =
List.concat (List.map (pairXwithAllInList listB) listA)
und der obligatorische Testaufruf:
cartesianProduct wochentage tageszeiten
In der Mathematik begegnen uns manchmal cartesische Produkte unbegrenzter Länge, also zum Beispiel
Das lässt sich in Elm nicht mehr mit Tupeln bewältigen, denn dann müsste
Ein (wieder etwas weit hergeholtes) Beispiel wäre die Konjugation deutscher Verben (oder schlimmer noch: lateinischer). Da gibt es nämlich Person, Numerus, Tempus und Modus:
personen =
[ "erste Person", "zweite Person", "dritte Person" ]
numeri =
[ "Singular", "Plural" ]
tempora =
[ "Praesens", "Praeteritum", "Perfekt" ]
modi =
[ "Indikativ", "Konjunktiv" ]
Das erlaubt uns jetzt schöne aussagen wie
"Ich müsste" ist der Konjunktiv Präteritum erste Person Singular des Verbs
"müssen". Wie bekommen wir alle möglichen Kombinationen? Ich kürze
jetzt das lange cartesianProduct
ab:
prod = cartesianProduct
prod modi (prod tempora (prod personen numeri))
[("Indikativ",("Praesens",("erste Person","Singular"))),("Indikativ",("Praesens",("erste Person","Plural"))),-- ... und so weiter
Ich will aber erstens den Ausdruck nicht so verschachtelt hinschreiben, zweitens will ich als
Ausgabetyp keine verschachtelten Tupel sondern einfach eine Listen von Listen, also
[["Indikativ", "Praesens" "erste Person", "Singular"], ["Indikativ", "Praesens", "erste Person", "Plural"], ...]
.
Ich will folgendes eintippen können:
cartesianProductManyLists [modi, tempora, personen, numeri]
Übungsaufgabe 4.7.2 Implementieren Sie eine Funktion
cartesianProductManyLists
, die aus [list_1, ..., list_k]
, also
einer List (List a)
eine Liste von Listen generiert, welche insgesamt
|list_1| * |list_2| * ... * |list_k|
Elemente hat, wovon jedes
wiederum eine Liste aus a
-Elementen ist.
Tip: ich habe hier ingesamt zwei Helferfunktionen gebraucht. Erstens
eien Variante von pairXwithAllInList
, zweitens
eine Variante von
cartesianProduct : List a -> List (List a) -> List (List a)
, die im
Prinzip
berechnet, wobei List a
ist und das zweite Argument, also
List (List a)
.
Dann schließlich eine Funktion, die die Listen cartesianProduct
kombiniert.
Permutationen
Mathematisch gesehen ist eine Permutation einer MengeÜbungsaufgabe 4.7.3
Schreiben Sie eine Funktion permutations : List a -> List (List a)
, die
alle Permutationen der Eingabeliste berechnet. Als Beispiel:
permutations [1,2,3]
[[1,2,3],[2,1,3],[2,3,1],[1,3,2],[3,1,2],[3,2,1]] : List (List number)
oder eben
List.map (String.join " ") (permutations ["ich", "dich", "fahre", "um"])
["ich dich fahre um","dich ich fahre um","dich fahre ich um","dich fahre um ich","ich fahre dich um","fahre ich dich um","fahre dich ich um","fahre dich um ich","ich fahre um dich","fahre ich um dich","fahre um ich dich","fahre um dich ich","ich dich um fahre","dich ich um fahre","dich um ich fahre","dich um fahre ich","ich um dich fahre","um ich dich fahre","um dich ich fahre","um dich fahre ich","ich um fahre dich","um ich fahre dich","um fahre ich dich","um fahre dich ich"] : List String