Folien 3: Ausgewählte Algorithmen
Transcription
Folien 3: Ausgewählte Algorithmen
Grundlagen der Informatik Prof. Dr. Stefan Enderle NTA Isny 3 Ausgewählte Algorithmen 3.1 Sortieren • Die Aufgabe „Sortieren einer Menge von Elementen“ kommt häufig vor. • Formal: – Gegeben: n (verschiedene) Objekte a1,a2,..an, auf denen eine totale Ordnung < definiert ist. – Gesucht ist eine Permutation p:{1,...,n} → {1,...,n} mit der Eigenschaft ap(1) < ap(2) < ... < ap(n) Sortieren • Vereinfachung / Verallgemeinerung: – Die total Ordnung < kann auf beliebigen Datentypen definiert sein. – Wir verwenden in den Beispielen jedoch jeweils Natürliche oder Realzahlen. 3.1.1 Direktes Auswählen • Gegeben ist ein Array a mit n Werten, also a[ i ] mit i=1..n. • Sortieren durch direktes Auswählen – startet mit der ersten Position und wählt dann aus allen Elementen das Kleinste für diese Position aus. – Danach wird die zweite Position gewählt und aus den restlichen Elementen wieder das Kleinste ausgewählt, usw. Direktes Auswählen - Algorithmus • Geg: a[ i ] mit i=1..n. • Tue für alle Positionen i = 1..n-1 : – Sei k=i – Prüfe für alle j = i+1..n : • Ist a[ k ] > a[ j ], dann sei k := j – vertausche a[ i ] und a[ k ] „Vertauschen“ von Zahlen • Bemerkung: – Ein Vertauschen zweier Zahlen a und b mittels a := b b := a wäre natürlich naiv, da der Wert von a nach der ersten Zuweisung verloren wäre. – Korrekt: temp := a a := b b := temp 3.1.2 Bubble-Sort • Gegeben ist ein Array a mit n Werten, also a[ i ] mit i=1..n. • Bubble-Sort – geht das komplette Array durch und ordnet jeweils zwei benachbarte Elemente. – Falls sich noch Änderungen ergaben, startet der Vorgang erneut Bubble-Sort - Algorithmus • Geg: a[ i ] mit i=1..n. • Widerhole – Setze vertauscht := false – Tue für alle Positionen i = 1..n-1 : • Wenn a[ i ] > a[ i+1 ] – vertausche a[ i ] und a[ i+1 ] – setze vertauscht := true • bis vertauscht = false 3.1.3 Formale Analyse • Direktes Auswählen und Bubble-Sort sind „naive“ Sortierverfahren, da sie nicht die bestmögliche Anzahl an Schleifendurchläufen, Vergleichen und Vertauschungen erreichen. • Beispiel: Direktes Auswählen: – besteht aus zwei Schleifen und dem „Inneren“ mit angenommenem konstantem Aufwand 1. – Anzahl Durchläufe: <formel> – Also im wesentlichen quadratischer Aufwand Formale Analyse • Beispiel: Bubble-Sort: – Im günstigsten Fall (wenn alle Elemente bereits sortiert sind) linearer Aufwand, da die Schleife nur 1x durchlaufen wird. – Im Mittel aber auch quadratischer Aufwand Formale Analyse • Wie schnell kann man überhaupt sortieren? • Wir nehmen als Basisoperation die Vergleiche beim Sortieren. • Jeder Vergleich ruft eine Verzweigung im Programmablauf hervor. • Man kann also jedem Sortier-Algorithmus für eine Eingabegröße n einen Entscheidungsbaum zuordnen. Formale Analyse • Beispiel: Verzweigungsbaum für 3 Elemente • Runde Kästchen = Vergleiche • Eckige Kästchen = Resultat der Sortierung Formale Analyse • Für n Elemente gibt es n! mögliche Permutationen. • Einem korrekten Sortieralgorithmus muss also ein Entscheidungsbaum zugeordnet sein, der mindestens n! Blätter hat. (Im Beispiel 3! = 6 Blätter.) • Die Höhe des Baumes ist die Anzahl der maximal notwendigen Vergleiche. • Frage also: Wie hoch ist ein Binärbaum mindestens, wenn er n! Blätter hat? Formale Analyse • Satz: Ein Binärbaum mit k Blättern hat mindestens die Höhe log2 k . • D.h. jeder Sortier-Algorithmus hat im ungünstigsten Fall mindestens die Komplexität log2 (n!) • Als „normale“ Funktion: – Stirlingsche Formel: (formel) – Also folgt: (formel) Formale Analyse • Fazit: Ein Sortieralgorithmus hat mindestens eine Komplexität von n*log n. 3.1.4 Quicksort • Gegeben ist ein Array a mit n Werten, also a[ i ] mit i=1..n. • Quicksort – sucht sich ein Pivot-Element p aus – Dann wird so vertauscht, dass alle a[i] < p links und alle a[i]>p rechts von p liegen. (bild) – Nun wird Quicksort rekursiv auf dem linken und rechten Teil separat aufgerufen. Quicksort - Algorithmus • Geg: a[ i ] mit i=1..n. • quicksort(links, rechts) falls links < rechts dann teiler := split(links, rechts) quicksort(links, teiler-1) quicksort(teiler+1, rechts) • Start mit quicksort(1,n) Quicksort – Algorithmus (2) • split(links, rechts) → Pivot Index Setze i := links-1 und j := rechts Setze pivot := rechts wiederhole // Erhöhe i bis ein Element größer gleich Pivot kommt wiederhole i := i + 1 solange a[ i ] < a[pivot] // Erniedrige j bis ein Element kleiner gleich Pivot kommt wiederhole j := j - 1 solange links < j und a[pivot] < a[ j ] falls i < j dann tausche a[ i ] mit a[ j ] solange i < j // setze Pivot an die richtige Stelle und gib seine Position zurück tausche a[i] mit a[pivot] liefere i als Pivot-Index zurück Beispiel • Beispiel mit „einbeispiel“ Laufzeit von Quicksort • Die Laufzeit hängt maßgeblich von der Wahl des Pivot Elements ab: • Worst-Case: Wird das Pivot-Element stets so gewählt, dass es das größte oder kleinste ist, reduziert sich die Länge bei jedem Durchlauf um 1 und die Komplexität ist somit n². Laufzeit von Quicksort • Best-Case: Teilt das Pivot-Element die Länge des Feldes stets in gleich große Teile, so ergibt sich genau ein gleichmäßiger Binärbaum, also Komplexität n*log n. 3.1.5 Heapsort • Gegeben ist ein Array a mit n Werten, also a[ i ] mit i=1..n. • Heapsort – sortiert alle Elemente in einen Heap ein (Phase 1) – Liest dann den Heap geordnet aus (Phase 2) Heapsort Algorithmus • Heapsort: – Phase 1: Erzeuge einen unsortierten Baum mit a[i] Tue für alle inneren Knoten von unten rechts bis zur Wurzel: lasse_sinken(Knoten) – Phase 2: Wiederhole Lies die Wurzel aus (=größtes/kleinstes El.). Setze das rechteste Blatt an die Wurzel lasse_sinken(Wurzel) bis Heap leer Heapsort Algorithmus (2) • lasse_sinken() prüft, ob der angegebene Knoten der Heap-Eigenschaft widerspricht und tauscht dann ggfs. mit dem größeren Nachfolger. • lasse_sinken(Knoten): Prüfe, ob der Knoten kleiner (größer) ist, als einer seiner Nachfolger Falls ja, tausche mit dem größeren Nachfolger lasse_sinken(getauschter_Nachfolger) Heapsort Eigenschaften • Heapsort benötigt in jedem Fall maximal n*log n Durchläufe • Allerdings benötigt Heap-Sort den zusätzlichen Speicher für den Heap 3.2 Hashing • Hashing ist ein Verfahren zum sehr schnellen Abspeichern und Wiederfinden von Daten. • Im besten Fall funktioniert dies sogar in konstanter Zeit, unabhängig von der Anzahl der Elemente. Hashing • Hashing setzt voraus, dass die Anzahl der zu speichernden Elemente vorweg abschätzbar ist. Die Methode verwendet Arrays, die richtig dimensioniert werden müssen. • Die Elemente werden über einen Schlüssel (Key) gespeichert und wieder gefunden. Hashing • Beispiel-Anwendung: Compilerbau Verwaltung der „Symboltabelle“ (Mehrere Informationen zu jedem Symbol) Schlüssel/Symbol x1 test PI Information Variable, Typ int Funktion, Typ int, Param string, double Konstante, Typ double, Wert 3.14159 Hashing • Idee: Die Elemente werden nicht sequentiell in eine Tabelle eingetragen, sondern die Hash-Funktion liefert den Index in die Tabelle. • Hash-Funktion: h: Key -> int Hashing • Um Keys mathematisch beschreiben zu können, werden sie meist in Zahlen umgewandelt. • Beispiel: Seien die möglichen Schlüssel aus U={a,b,c,...,z}* Dann lässt sich jedem Wort s aus U eine natürliche Zahl n(s) zuordnen, indem die Binärwerte der Ascii-Codes hintereinander geschrieben werden. Hashing • Die Hash-Funktion h soll nun diese Werte in Indizes umwandeln. Bei einer Tabelle mit m Einträgen also h : U → [0...m-1] • h soll möglichst einfach zu berechnen sein • h soll die Elemente aus U möglichst „zufällig“ und gleichmäßig über die Menge [0...m-1] streuen. Hash-Funktionen • Beispiele für Hash-Funktionen: – Kongruenzmethode: Man wähle m als Primzahl h(s) := n(s) MOD m – Multiplikationsmethode: Man wähle eine Konstante a zw. 0 und 1. t(s) := a * n(s) h(s):= m*( t(s) - t(s) ) Hash-Funktionen • Problem: Da meistens gilt, dass |U| wesentlich größer ist als m, liefert die Hashfunktion für unterschiedliche Keys dieselben Werte, also h(s) = h(t) bei s ungleich t → „Kollision“ Ohne Kollisionen • Annahme: Es kommen keine Kollisionen vor. (bild ohne Kollisionen) • Dann verlaufen die Grundoperationen mit einem einzigen Zugriff: – Einfügen: a[h(s)] := Daten – Löschen: a[h(s)] := <empty> – Prüfen: if a[h(s)] not <empty> Mit Kollisionen • Annahme: Es kommen Kollisionen vor. (bild mit Kollisionen) • Dann wird eine „Kollisionsauflösungsstrategie“ benötigt, z.B. – Hashing mit Verkettung – Offenes Hashing Hashing mit Verkettung • Hier enthält jeder Array-Eintrag einen Pointer, der entweder auf NULL, auf ein Element oder – im Falle der Kollision – auf eine verkettete Liste von Elementen zeigt. (bild mit verk. Liste) Hashing mit Verkettung • Die Algorithmen sind trotz der Verkettung relativ einfach. • Beispiel: Einfügen eines Elementes s Einfügen(s): i := h(s) p := neues LLElement(s, a[ i ]); a[ i ] := p Hashing mit Verkettung • Beispiel: Test eines Elementes s Test(s) → bool: i := h(s) p := a[ i ] wiederhole solange p nicht NULL Falls p.key = s return true p := p.next return false Hashing mit Verkettung - Analyse • Sei n die Anzahl der gespeicherten Elemente. Sei m die Größe des Arrays. Dann ist b=n/m der Belegungsfaktor. Wenn h(s) die Keys gleichmäßig streut, so sind in jeder verketteten Liste im Mittel b Elemente. • Der mittlere Suchaufwand liegt somit bei (1+b) und ist damit unabhängig von n !! Offenes Hashing • Beim offenen Hashing werden die Daten im Kollisionsfall nicht in einer Art „Überlaufliste“ gespeichert, sondern es wird innerhalb der Liste nach einem alternativen Platz gesucht. • Bemerkung: Beim Hashing mit Verkettung war die Anzahl der Einträge nicht auf m beschränkt, beim offenen Hashing schon. Offenes Hashing • Lineares Sondieren: Ist a[h(s)] bereits besetzt, so wird a[h(s)+1] geprüft, danach a[h(s)+2] usw. • Nachteil: „Clusterbildung“: Es entstehen Bereiche, in denen viele aufeinanderfolgende Plätze belegt sind. Dies verschlechtert die mittlere Suchzeit. Offenes Hashing • Double Hashing: Ist a[h(s)] bereits besetzt, wird mit einer zweiten Hash-Funktion h' die Schrittweite bestimmt: Sei i = h(s) und a[ i ] bereits belegt Bestimme ein j Prüfe (i + j) mod m Prüfe (i + 2j) mod m Prüfe (i + 3j) mod m ... 3.3 O-Notation • Die Laufzeit eines Algorithmus für große n bezeichneten wir bisher als „linear“, „quadratisch“, „exponentiell“, etc. • Auf die genaue Angabe weiterer Faktoren kam es hierbei nicht an (da z.B. maschinenabhängig). • Diese Abstraktion wird durch die „O-Notation“ ebenfalls ermöglicht. O-Notation • Definition: Eine Funktion f(n) ist von der Art O(g(n)), oder symbolisch f(n)=O(g(n)), falls es Zahlen c und n0 gibt, so dass für alle n>n0 gilt: |f(n)| <= c* |g(n)| O-Notation • Beispiel: 3n²+10n-4 = O(n²) Beweis: Wähle c=4, dann muss nur noch ein n0 gefunden werden so dass 3n²+10n-4 < 4n² für alle n>n0. (Lsg. n0=10) O-Notation • Innerhalb der O-Notation werden nur einfache Funktionen verwendet: – O(1) (lineare Komplexität) – O(log log n) – O(log n) – O(n) – O(n log n) – O(n²) – O(n³) – ... – O(nlog n) – O(2n) – O(22n) 3.4 Kompressionsalgorithmen • Datenkompression (Datenkomprimierung) ist die Reduktion des Speicherbedarfs von Daten • Die Datenmenge wird reduziert, indem eine günstigere Repräsentation bestimmt wird, mit der sich die gleichen Daten in kürzerer Form darstellen lassen. Kompressionsalgorithmen • 2 Möglichkeiten der Kompression: – Verlustfreie Kompression: Die kodierten Daten entsprechen nach Anwendung der Dekodiervorschrift exakt denen des Originals. – Verlustbehafteten Kompression: Die Daten lassen sich nicht (in jedem Fall) fehlerfrei rekonstruieren. Redundanz / Irrelevanz • Redundanz bezeichnet das mehrfache Vorhandensein ein und derselben Information oder das umfangreiche Beschreiben einer Information, die auch kürzer dargestellt werden kann. Ein Merkmal ist dann redundant, wenn es ohne Informationsverlust weggelassen werden kann. • Irrelevanz bezeichnet die „Unbedeutsamkeit“ von Daten. Ein Merkmal ist dann irrelevant, wenn es ohne größere Auswirkung weggelassen werden kann. Redundanz / Irrelevanz • Beispiel für Redundanz: In einer Datenbank werden Kunden gespeichert: Nr Name Wohnort --------------------------------3524 Enderle Weißenhorn 876 Maier Isny Und Rechnungen: Nr Betrag --------------------1 102,30 2 35,90 Kunde -----------Enderle Maier KuNr ---------3524 876 • Speichern der Kundennummer würde ausreichen. D.h., der zusätzliche Name ist redundant. Redundanz / Irrelevanz • Noch ein Beispiel für Redundanz: In amerikanischen Institutionen gibt es manchmal ein „Department of Redundancy Department“ • Kein Beispiel für Redundanz: Proofread carefully to see if you any words out! Redundanz / Irrelevanz • Beispiel für Irrelevanz: Kompressionsalgorithmen • 2 Möglichkeiten der Kompression: – Verlustfreie Kompression: Die kodierten Daten entsprechen nach Anwendung der Dekodiervorschrift exakt denen des Originals. → Entfernen Redundanz – Verlustbehafteten Kompression: Die Daten lassen sich nicht (in jedem Fall) fehlerfrei rekonstruieren. → Entfernen Irrelevanz 3.4.1 Textkompression • Unkomprimierte Texte belegen vergleichsweise wenig Speicherplatz. • Trotzdem kann der benötigte Speicherplatz durch ein Verfahren zur verlustfreien Kompression auf bis zu 10% des ursprünglich benötigten Platzes reduziert werden. • Beispiel-Kodierung: Ausgangstext: AUCH EIN KLEINER BEITRAG IST EIN BEITRAG Kodiertext: AUCH EIN KLEINER BEITRAG IST -4 -3 Beispiel • Ausgangstext: AUCH EIN KLEINER BEITRAG IST EIN BEITRAG Kodiertext: AUCH EIN KLEINER BEITRAG IST -4 -3 • Kodierung: – Wird ein bereits benutztes Wort gefunden, wird statt des Wortes die relative Position des letzten Auftretens des Wortes gespeichert. (Bis max. n) – Hierfür müssen alle Wörter gespeichert werden! (Hashing!) • Dekodierung: – Die letzen n Wörter werden in einem Ringspeicher gehalten. Bei Auftreten einer Zahl -i wird das i-letzte Wort eingesetzt. Tokenbasierte Kompression • Tokenbasierte Kompression: Häufig wiederkehrende Schlüsselwörter werden durch Abkürzungen, sog. „Tokens“, ersetzt. • Beispiel: Ausgangstext: print "Hallo"; print "Ich bin's" Kodiertext: 3F "Hallo"; 3F "Hier" • Dem Kodierer und Dekodierer müssen die Token bekannt sein. • Oder die Token müssen am Anfang des Textes definiert sein. RLE – Run-length Encoding • „Lauflängenkodierung“ • In manchen Anwendungen kommen sehr wenige Zeichen oft hintereinander vor. • Beispiel: Schwarz-weiß Bild (z.B. Windows Bitmap) Einzelne Pixel mit W und S gekennzeichnet: WWWWWWWWWWSWWWWWWWWWWSSS • Idee: Häufigkeit der Zeichen wird gezählt und gespeichert. • Nach Anwendung der Lauflängenkodierung erhält man:10W1S10W3S RLE – Run-length Encoding • Problem: Einzelne Zeichen blähen den kodierten Text auf. • Originaltext wird kodiert zu • Mögliche Lösung: – Bei ein- bis dreimaligem Auftreten eines Zeichens bleibt das Original. – Bei mind. 4-maligem Auftreten wird ein Kontrollzeichen eingefügt: Aus WSWSWWWWWWWWWSSSSSSWSWWW wird WSWSx9Wx6SWSWWW – Taucht im Datensatz das Kontrollzeichen als Inhalt auf, so wird es doppelt kodiert: Aus WSSSSSWWWWWWxWWSWSSSSSSSSSxxxxxWSSW wird Wx5Sx6WxxWWSWx9Sx5xWSSW ABAB 1A1B1A1B Huffman-Code • Annahme: – Die zu komprimierenden Daten sind nicht gleichverteilt. D.h., es gibt Zeichen, die häufiger und andere die weniger häufig vorkommen. • Idee: – Die häufig vorkommenden Wörter (bzw. Buchstaben) bekommen ein kurzes Symbol, die selten vorkommenden ein längeres. – Symbole sind Binärzeichen (0,1,00,01,10,11,000,...) • Gesucht ist die optimale Zuordnung der Binärzeichen zu den Wörtern. Huffman-Code • Beispiel mit einzelnen Buchstaben: – Beispieltext: Blaukraut bleibt Blaukraut und Brautkleid bleibt Brautkleid – Tabelle mit den Häufigkeiten der einzelnen Zeichen: • n=1 • d=3 • B=4 • b=4 • e=4 • i=4 • k=4 • r=4 • = 6 // Leerzeichen • a=6 • l=6 • t=6 • u=7 Huffman-Code • • Optimale Zuordnung über Baum-Algorithmus – Zusammenfassen der kleinsten Elemente – Iteration, bis Baum alle Elemente umfasst – Zuordnung: z.B. „Links“ = 0, „Rechts“ = 1 Bit-Repräsentationen (Beispiel): • r --> 1101 • B --> 1000 • n --> 11100 • l --> 001 • k --> 1100 • i --> 1011 • --> 1111 • e --> 1010 • d --> 11101 • b --> 1001 • a --> 000 • u --> 011 • t --> 010 (→ Tafel) Huffman-Code • • Bit-Repräsentationen (Beispiel): • r --> 1101 • B --> 1000 • n --> 11100 • l --> 001 • k --> 1100 • i --> 1011 • --> 1111 • e --> 1010 • d --> 11101 • b --> 1001 • a --> 000 • u --> 011 • t --> 010 Kodierter Text: 100000100001111001101000011010111110010011010101110010 101111100000100001111001101000011010111101111100111011 111100011010000110101100001101010111110111111001001101 01011100101011111000110100001101011000011010101111101 Huffman-Code • Vergleich des Speicherbedarfs: – Blaukraut bleibt Blaukraut und Brautkleid bleibt Brautkleid = 59 Zeichen à 8 Bit => 472 Bit – Kodierter Text: 100000100001111001101000011010111110010011010101110 010101111100000100001111001101000011010111101111100 111011111100011010000110101100001101010111110111111 001001101010111001010111110001101000011010110000110 10101111101 = 75 Bit – Kompression um Faktor 6,3 (Ohne Speichern der Übersetzungstabelle!) Text-Kompression • Zusammenfassung: – Token-basierte Kompression, wenn Schlüsselworte oft vorkommen (z.B. in Programmiersprachen). – Run-length Encoding, wenn nur wenige Zeichen, diese aber oft hintereinander vorkommen. – Huffman-Code, wenn bestimmte Zeichen öfters vorkommen als andere (z.B. in natürlichen Sprachen) • Alle Algorithmen sind verlustfrei! 3.4.2 Bildkompression • Bilder benötigen meist mehr Speicherplatz als Texte. • Allerdings spielen oft kleine Ungenauigkeiten keine Rolle. • Daher werden sehr oft verlustbehaftete Kompressionsverfahren benutzt. • Aktuelle Verfahren: – GIF – PNG – JPG GIF • „Graphics Interchange Format“ • Verlustfreie Komprimierung • Geringe Farbtiefe (256 Farben) • Ersetzte 1987 bei Compuserve das Format RLE für Schwarz-Weiß-Bilder • Hiermit war es erstmals möglich, farbige Bilder per Modem zu übertragen. GIF • Eigenschaften: – Farbinformation stehen in Farbtabelle mit 256 Einträgen – Jeder Eintrag kann eine Farbe aus 16,7 Mio. möglichen Farben sein. (256³) • Erste 6 Zeichen sind „GIF87a“ oder „GIF89a“ • Ab GIF89a ist auch „Transparent“ als Farbe möglich GIF • Ablauf bei Speicherung: – Das Originalbild darf nur max. 256 Farben besitzen. – Besitzt es mehr als 256 Farben werden Farben durch ähnliche Farben ersetzt. – Zuerst wird die Farbtabelle gespeichert: 256 Einträge a 3 Bytes (für RGB) – Nun werden die einzelnen Pixel als Indizes in die Farbtabelle gespeichert • Hierdurch wird aus jedem 3-Byte Pixel ein 1-Byte Index • Danach wird mit LZW Algorithmus gepackt GIF LZW Algorithmus • Meist mit 12 Bit Index PNG • „Portable Network Graphics“ ist verlustfrei. • PNG entstand als Antwort auf GIF, auf das seit 1994 von Unisys Lizenzgebühren anfallen. • Idee: In vielen Bildern unterscheiden sich benachbarte Pixel nur wenig voneinander. Das bedeutet, dass die Differenzwerte dieser Pixel vom Betrag her recht klein sind. Werden nun statt der originalen Pixeldaten die Differenzwerte zu den vorangegangenen Pixeln verarbeitet, treten oft Folgen kleiner Werte auf. Dies begünstigt die geringe Größe von PNG-Dateien. PNG • Vorfilter: Die Grafik-Software kann einen von 4 Vorfiltern auswählen: – 0 None Keine Vorfilterung. Originalen Pixeldaten. – 1 Sub Differenzen zu dem links benachbarten Pixel. – 2 Up Differenzen zu dem darüber liegenden Pixel. – 3 Average Differenz zu dem Mittelwert aus dem darüber liegenden und dem links benachbarten Pixel. – 4 Paeth „Paeth-Predictor-Wert“ aus dem links benachbarten, dem darüber liegenden und dem schräg links oben benachbarten Pixel. PNG • Zur Komprimierung wird der „Deflate“Algorithmus verwendet, der für ZIP entwickelt wurde. • „Deflate“ ist eine Kombination aus Lempel-Ziv-Storer-Szymanski-Algorithmus und Huffman-Kodierung. • Grob-Idee: Mehrfach vorkommende Kombinationen werden ebenfalls in die Huffman-Codierung eingebaut. JPEG • „Joint Photographic Experts Group“ • Wurde 1982 gegründet und trifft sich 3 mal im Jahr JPEG • Verarbeitungsschritte: – Farbraumumrechnung von RGB nach YCbCr – Tiefpassfilterung und Unterabtastung der Farbabweichungssignale Cb und Cr (verlustbehaftet). – Einteilung in 8×8-Blöcke und diskrete Kosinustransformation dieser Blöcke. – Quantisierung (verlustbehaftet). – Umsortierung. – Entropiekodierung. JPEG • Farbraumumrechnung von RGB nach YCbCr: • Natur/Auge: Helligkeitsunterschiede sind viel besser erkennbar als Farbunterschiede • Deshalb: – hohe Auflösung bei Helligkeit, – nur halbe Auflösung (=1/4) bei Farbe (=Unterabtastung) JPEG • Einteilung in 8×8-Blöcke • Diskrete Kosinustransformation dieser Blöcke. • Im Beispiel kann das Bild mit 6 Koeffizienten gut rekonstruiert werden (= Quantisierung). JPEG • Umsortierung + Entropiekodierung ähnlich Huffman Kodierung, aber nicht mit fester Zeichen-Statistik, sondern mit dynamischen Wahrscheinlichkeiten 3.4.3 Videokompression • Die Datenrate eines digitalisierten herkömmlichen Farbfernsehsignals inklusive Austastlücke liegt bei 216 Mbit/s • Vergleich: MPEG-4 = DVD-Auflösung bei nur 1 Mbit/s 3.4.3 Videokompression • Die Datenrate eines digitalisierten herkömmlichen Farbfernsehsignals inklusive Austastlücke liegt bei 216 Mbit/s • Vergleich: MPEG-4 = DVD-Auflösung bei nur 1 Mbit/s Videokompression • Motion-JPEG (auch bei Apple Quicktime) – Hier werden Einzelbilder eines Videos als JPEG komprimiert. – Vorteil: Das Video kann an jeder Stelle geschnitten werden oder Einzelbilder extrahiert werden – Wird bei Digitalkameras meist verwendet Videokompression • Motion-JPEG (auch bei Apple Quicktime) – Hier werden Einzelbilder eines Videos als JPEG komprimiert. – Vorteil: Das Video kann an jeder Stelle geschnitten werden oder Einzelbilder extrahiert werden – Wird bei Digitalkameras meist verwendet Videokompression • MPEG (auch bei Apple Quicktime) – Ähnlich zu Motion-JPEG, aber mit Bewegungskompensation: • Block motion compensation bei MPEG-2 • Global motion compensation bei MPEG-4 – Konstante Bildinhalte werden komplett ausgefiltert – Für Pixelblöcke, die sich von einem Bild zum nächsten verschoben haben, wird nur der Verschiebevektor gespeichert. 3.5 Backtracking • Backtracking nennt sich eine algorithmische Methode, bei der rekursive Funktionsaufrufe dazu benutzt werden, einen ganzen Suchbaum an Möglichkeiten nach einer oder mehreren Lösungen abzusuchen. • Backtracking lässt sich immer dann einsetzen, wenn ein Problem eine bestimmte Suchtiefe besitzt und man ausgehend von einer bestimmten Teillösung die Gesamtlösung durch Suchen finden kann, wobei man evtl. in „Sackgassen“ geraten kann und dann einen Schritt zurück muss (Backtracking). Backtracking - Algorithmus • Backtracking wird meist rekursiv implementiert. • Ein allgemeiner rekursiver Algorithmus für Backtracking sieht folgendermaßen aus: • backtrack (Teillösung) wenn Teillösung=Gesamtlösung dann gib Teillösung aus (stop/weiter?) ansonsten für jede mögliche Erweiterung der Teillösung tue backtrack (erweiterte Teillösung) Backtracking - Algorithmus • Der Algorithmus wird gestartet mit backtrack (Leere Lösung) • Für die Anwendung des Algorithmus auf ein spezielles Problem müssen folgende Fragen geklärt werden: – Wie wird eine Teil- oder Gesamtlösung repräsentiert? – Wie wird geprüft, ob eine gegebene Teillösung eine Gesamtlösung ist? – Wie werden zu einer Teillösung die möglichen Erweiterungen berechnet? Beispiel: Abwechselnd Ger./Ung. • Ein Vektor mit n Zahlen soll so mit den Zahlen 0...n-1 gefüllt werden, dass immer eine gerade Zahl neben einer ungeraden Zahl steht. • Beispiel: mit n=10 4781056329 Beispiel: Abwechselnd Ger./Ung. • Wie wird eine Teil- oder Gesamtlösung repräsentiert? – Array der Länge n für ganze Zahlen – Teillösung: Elemente 0 bis index-1 sind bereits gefüllt und sortiert • Wie wird geprüft, ob eine gegebene Teillösung eine Gesamtlösung ist? – Wenn index=n dann sind alle gefüllt und sortiert! • Wie werden zu einer Teillösung die möglichen Erweiterungen berechnet? – Das Array-Element index muss neu belegt werden – Dafür kommen nur die Zahlen in Frage, die noch nicht benutzt sind Beispiel: Abwechselnd Ger./Ung. • Beispiel-Programm (C++) (ohne #include etc.) // Globale Variablen // const int ANZAHL = 10; // Maximale Anzahl int // Array mit Teillösung feld[ANZAHL]; bool uebrig[ANZAHL]; // Übrige Zahlen int main() { } init(); // Initialisiere die Arrays backtrack(0); // Starte mit Teillösung 0 Beispiel: Abwechselnd Ger./Ung. void init() { for (int i=0; i<ANZAHL; i++) { // Für alle Elemente... uebrig[i] = true; // ... setze auf „übrig“ } } void fertig() { for (int i=0; i<ANZAHL; i++) { // Für alle Elemente... cout << feld[i] << " "; } cout << endl; } // ... gib sie aus Beispiel: Abwechselnd Ger./Ung. void backtrack(int index) { if (index==ANZAHL) fertig(); else { for (int i=0; i<ANZAHL; i++) { if (uebrig[i] && ((index==0) || ((feld[index-1]+i) % 2==1)) ) { } } } } feld[index] = i; // Nächstes Element uebrig[i] = false; // Zahl nicht mehr übrig backtrack(index+1); // Löse Restproblem uebrig[i] = true; // Zahl wieder übrig Beispiel: Abwechselnd Ger./Ung. • Ausgabe: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 9 8 7 0 1 2 3 4 5 8 7 6 9 0 1 2 3 4 5 8 9 6 7 0 1 2 3 4 7 6 5 8 9 . . . 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 0 1 2 Beispiel: Magisches Quadrat • Ein magisches Quadrat mit der Kantenlänge n ordnet die Zahlen 1...n² so an, dass die Summe – aller Spalten, – aller Zeilen, – und der beiden Diagonalen, gleich sind. • Beispiel für n=3: 4 9 2 3 5 7 8 1 6 • Zusammenhang von n und Summe → Tafel Beispiel: Magisches Quadrat • Wie wird eine Teil- oder Gesamtlösung repräsentiert? – Array der Länge n für ganze Zahlen – Teillösung: Elemente 0 bis index-1 sind bereits gefüllt, aber Korrektheit unklar! • Wie wird geprüft, ob eine gegebene Teillösung eine Gesamtlösung ist? – Wenn index=n dann noch alle Bedingungen prüfen! • Wie werden zu einer Teillösung die möglichen Erweiterungen berechnet? – Das Array-Element index muss neu belegt werden – Dafür kommen nur die Zahlen in Frage, die noch nicht benutzt sind Beispiel: Magisches Quadrat • Beispiel-Programm (C++) (ohne #include etc.) // Globale Variablen // const int ANZAHL = 9; // Maximale Anzahl int // Array mit Teillösung feld[ANZAHL]; bool uebrig[ANZAHL]; // Übrige Zahlen int main() { } init(); // Initialisiere die Arrays backtrack(0); // Starte mit Teillösung 0 Beispiel: Abwechselnd Ger./Ung. void init() { for (int i=0; i<ANZAHL; i++) { // Für alle Elemente... uebrig[i] = true; } } // ... setze auf „übrig“ Beispiel: Magisches Quadrat void backtrack(int index) { if (index==ANZAHL) testeFertig(); else { for (int i=0; i<ANZAHL; i++) { if (uebrig[i]) { } } } } feld[index] = i; // Nächstes Element uebrig[i] = false; // Zahl nicht mehr übrig backtrack(index+1); // Löse Restproblem uebrig[i] = true; // Zahl wieder übrig Beispiel: Magisches Quadrat void testeFertig() { int a = feld[0] + feld[1] + feld[2]; // oben if ( (a == feld[3] + feld[4] + feld[5]) && // mitte (a == feld[6] + feld[7] + feld[8]) && // unten (a == feld[0] + feld[3] + feld[6]) && // links (a == feld[1] + feld[4] + feld[7]) && // mitte (a == feld[2] + feld[5] + feld[8]) && // rechts (a == feld[0] + feld[4] + feld[8]) && // l.o. r.u. (a == feld[2] + feld[4] + feld[6]) ) { // l.o. r.u. cout << feld[0] << " " << feld[1] << " " << feld[2] << endl; cout << feld[3] << " " << feld[4] << " " << feld[5] << endl; cout << feld[6] << " " << feld[7] << " " << feld[8] << endl; cout << endl; } } Beispiel: Magisches Quadrat • Ausgabe: 1 6 5 3 2 7 5 0 7 7 0 5 8 4 0 8 4 0 6 4 2 2 4 6 3 2 7 1 6 5 1 8 3 3 8 1 1 8 3 3 8 1 5 6 1 7 2 3 6 4 2 2 4 6 0 4 8 0 4 8 5 0 7 7 0 5 7 2 3 5 6 1 Beispiel: Damenproblem • Das „Damenproblem“ bezeichnet folgende Aufgabe von C.F.Gauß aus dem Jahr 1850: „Man finde eine Stellung für acht Damen auf einem Schachbrett, so dass sich keine zwei Damen gegenseitig schlagen können.“ Beispiel: Damenproblem • Wie wird eine Teil- oder Gesamtlösung repräsentiert? – Array der Länge n für ganze Zahlen – Teillösung: Elemente 0 bis index-1 sind bereits gefüllt und sortiert • Wie wird geprüft, ob eine gegebene Teillösung eine Gesamtlösung ist? – Wenn index=n dann sind alle gefüllt und sortiert! • Wie werden zu einer Teillösung die möglichen Erweiterungen berechnet? – Das Array-Element index muss neu belegt werden – Dafür kommen nur die Zahlen in Frage, die noch nicht benutzt sind Beispiel: Damenproblem • Beispiel-Programm (C++) (ohne #include etc.) // Globale Variablen // const int ANZAHL = 8; // Maximale Anzahl int // Array mit Teillösung feld[ANZAHL]; bool uebrig[ANZAHL]; // Übrige Zahlen int main() { } init(); // Initialisiere die Arrays backtrack(0); // Starte mit Teillösung 0 Beispiel: Damenproblem void init() { for (int i=0; i<ANZAHL; i++) { // Für alle Elemente... uebrig[i] = true; // ... setze auf „übrig“ } } void fertig() { for (int i=0; i<ANZAHL; i++) { // Für alle Elemente... cout << feld[i] << " "; } cout << endl; } // ... gib sie aus Beispiel: Damenproblem void backtrack(int index) { if (index==ANZAHL) fertig(); else { for (int i=0; i<ANZAHL; i++) { if (uebrig[i] && konsistent(index,i) ) { } } } } feld[index] = i; // Nächstes Element uebrig[i] = false; // Zahl nicht mehr übrig backtrack(index+1); // Löse Restproblem uebrig[i] = true; // Zahl wieder übrig Beispiel: Damenproblem bool konsistent(int index, int i) { for (int z=0; z<index; z++) { if (abs(feld[z]-i) == abs(index-z)) return false; } return true; } Beispiel: Damenproblem • Ausgabe: 0 4 7 5 2 6 1 3 0 5 7 2 6 3 1 4 0 6 3 5 7 1 4 2 0 6 4 7 1 3 5 2 1 3 5 7 2 0 6 4 . . . 7 2 0 5 1 4 6 3 7 3 0 2 5 1 6 4 Weitere Backtracking Beispiele • Folgende Probleme können elegant mit Backtracking gelöst werden: – Springer-Problem – Solitaire – Sudoku – Labyrinth – Hamiltonscher Kreis im Graph – Rucksack-Problem – Erfüllbarkeitsproblem boolscher Ausdrücke – ...