Autor |
Beitrag |
Flamefire
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Do 26.03.09 13:18
Ich habe eine Wegsuche gebastelt, die für eine sehr große Map gedacht ist
Als Algo habe ich am Ende dann A* genommen (Diskussion: hier
Jetzt möchte ich den noch schneller machen.
Derzeit braucht der Algo auf meinem PC (Core2Duo 2,2GHz; 2GB RAM) Bis zu knapp 11sec auf einer 1000x1000 Karte im ungünstigstem Fall.
Im Anhang ist das Projekt mit Source und Beispiel-Labyrinth
Zum Test des worst case einfach die .env datei laden und als start "0" und "0"(steht schon drin) und als ziel "999" und "999" angeben
Einen Button und die Titelleist misbrauche ich z.zt. als Zeitausgabe. Also nicht wundern
Der Code ist nicht sehr sauber, das gebe ich zu aber funktioniert so schon ganz gut.
Kurze Erklärung:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
| TFeld=Array of Byte; TEntry=record X,Y, F ,G ,H:Integer; offen:DWORD; preKnoten:Integer; end; PEntry=^TEntry; TFeld2=Array of TEntry; PList=^TList; TList=record iFeld:Integer; next:PList; end; |
Das sind die Strukturen. Ich habe ein einfaches Array (TFeld) das beinhaltet, welche felder begehbar sind, und ein komplexeres Feld, indem die Werte vermerkt werden. (Arrays sind schneller als 2 riesige listen zu durchsuchen, von denen eine am ende 1Mio einträge hat.
A* arbeitet so:
-Füge das Startfeld in die offene Liste ein
Wiederhole:
Suche das Feld, mit dem geringsten F-Wert (F=Entfernung Start zum Punkt+Luftlinie zum Ziel)
Entferne das Feld aus der offenen Liste
Durchsuche alle umliegenden Felder
Wenn ein Feld neu gefunden ist, füge es mit den werten der offenen Liste hinzu
Wenn ein Feld in der offenen Liste ist, vergleiche den G-Wert (entfernung start->punkt) und behalte das mit dem kleinern wert
sonst ignoriere das feld
wenn er das ziel aus der offenen liste entfernt hat, ist er fertig
Das langsamste an meinem Algo ist die Suche nach dem feld mit dem geringstem F-Wert.
Die Liste enthällt alle offenen Punkte (denke so ist das schneller als alle einträge des array zu durchsuchen)
Wie kann ich das weiter optimieren?
Zuletzt bearbeitet von Flamefire am Mo 13.04.09 10:01, insgesamt 1-mal bearbeitet
|
|
jaenicke
      
Beiträge: 19315
Erhaltene Danke: 1747
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Do 26.03.09 13:27
Kennst du diesen Beitrag? Vielleicht gibt der dir Ideen, ich habe deinen Quelltext jetzt nicht angeschaut, musste bei dem Stichwort aber sofort an den Beitrag denken:
www.delphipraxis.net...ic85844,0,asc,0.html
|
|
BenBE
      
Beiträge: 8721
Erhaltene Danke: 191
Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
|
Verfasst: Do 26.03.09 13:42
Statt einer verketteten Liste könntest Du einen dynamisch wachsenden Ringpuffer für deine Queue hernehmen. Das hatte ich bei meiner Implementierung gemacht. Das spart nicht nur Speicher, sondern ist auch noch schneller, weil Du nicht ständig Daten von bestehenden Einträgen ändern musst.
_________________ Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
|
|
Flamefire 
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Do 26.03.09 14:50
den beitrag kannte ich noch nicht. ist aber nichts neues. im gegenteil: da geht es ausschließlich um grundfunktionen des suchens nach einem weg
@BenBE: was ist ein dynamisch wachsender ringpuffer?
theoretisch könnte ich meine liste offener punkte einfach sortieren. im best case ist das langsamer aber im worst case viel schneller
das problem ist, dass sich die gewichte (f-werte) häufig ändern (können) und dann muss der eintrag neu sortiert werden.
auch nicht ideal
wie sieht also der ringpuffer aus und wie wäre der für meinen fall?
|
|
BenBE
      
Beiträge: 8721
Erhaltene Danke: 191
Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
|
Verfasst: Do 26.03.09 15:31
Für solch einen Ringpuffer speicherst Du nur ein großes Array von Index-Einträgen, sowie 4 Indizes auf Einträge im Array.
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9:
| type TRingPufferEntry = Integer; TRingpufferData = array of TRingPufferEntry; TRingpuffer = record data: TRingPufferData; head: Integer; tail: Integer; itemcount: Integer end; |
Wenn Head über das Ende von data hinaus geht, fängst Du einfach wieder am Anfang an. Zum dynamischen Wachsen prüfst Du nun ferner folgende Fälle:
1. Beim Erreichen des Endes sind viele Einträge am Anfang frei (>50%) --> Alle Einträge werden ohne Speicherplatz-Reservierung an den Anfang verschoben, tail und head entsprechend angepasst (muss atomar sein), sonst einfach Wrap-Around
2. tail=head --> Du vergrößerst den Puffer auf die doppelte größe und kopierst die Daten von 0..head hinter die Einträge tail..oldmax. Neuer Speicher wird zugeordnet, Operation muss atomar sein.
3. tail erreicht head --> Queue leer, optionaler Reset beider Zeiger an den Anfang (muss atomar sein).
Einen neuen Eintrag in diesen Puffergeht Bestcase und Meancase in O(1), Worstcase O(n).
Das Entfernen eines Eintrags geht immer in O(1).
Je nach Lese- und Schreibraten reichen speichermäßig 256KB aus (64K Einträge), ansonsten holt sich die Queue selber ihren Speicher.
Eine Implementierung des Verfahrens findest Du im andren Thread in der verlinkten Unit.
_________________ Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
|
|
Flamefire 
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Do 26.03.09 16:06
uff...so richtig verstehn tue ich das nicht. und in deiner unit kann ich keine implementierung davon finden
was genau ist head und tail?
was heißt "erreichen des endes", "tail=head" und "tail erreicht head"?
wenn ich die idee dahinter richtig verstanden habe, ist es vereinfacht ein dyn. array mit x einträgen (wobei jeder eintrag meine zu speichernden werte enthällt)
am anfang ist die länge 0
wenn ich einen eintrag hinzufügen will, guck ich, ob noch platz ist (also anzahl gespeicherter einträge < länge)
wenn ja hinten anfügen und ende zeiger eins weiter setzen
wenn nein länge verdoppeln
beim löschen eines einrtrags müsste ich dann immer alle einträge die danach kommen bis zur anz.gespeicherter einträge eins rückwärts kopieren.
das würde das häufige neu allocieren von speicher ersparen und auch das freigeben.
wie genau meintest du das?
warum brauche ich "head"? reicht nicht nur tail? und was soll itemcount sein? ist das nicht gleich tail? oder ist das tail-head?
|
|
BenBE
      
Beiträge: 8721
Erhaltene Danke: 191
Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
|
Verfasst: Do 26.03.09 16:24
Flamefire hat folgendes geschrieben : | uff...so richtig verstehn tue ich das nicht. und in deiner unit kann ich keine implementierung davon finden
was genau ist head und tail? |
Siehe QUEUE bzw. RINGPUFFER
Flamefire hat folgendes geschrieben : | was heißt "erreichen des endes", |
Head > High(QueueData)
Flamefire hat folgendes geschrieben : | "tail=head" |
Beim Einfügen eines Eintrags, würde der nächste an den Index, auf den Tail zeigt geschrieben werden --> Queue voll
Flamefire hat folgendes geschrieben : | und "tail erreicht head"? |
Beim Entfernen eines Eintrags, erreicht Tail von hinten den Head --> Queue leer
Flamefire hat folgendes geschrieben : | wenn ich die idee dahinter richtig verstanden habe, ist es vereinfacht ein dyn. array mit x einträgen (wobei jeder eintrag meine zu speichernden werte enthällt) |
Jain. Eigentlich eher eine effizient verwaltete Queue mit paar Zusatzfunktionen, um die Speicherverwaltung dynamisch, aber dennoch effizient zu halten.
Flamefire hat folgendes geschrieben : | am anfang ist die länge 0
wenn ich einen eintrag hinzufügen will, guck ich, ob noch platz ist (also anzahl gespeicherter einträge < länge)
wenn ja hinten anfügen und ende zeiger eins weiter setzen |
Korrekt.
Flamefire hat folgendes geschrieben : | wenn nein länge verdoppeln |
Jain. Verdoppeln nur, wenn nicht einfach neuer Platz geschaffen werden kann. Da das Alloziieren als auch das Umkopieren der Einträge recht aufwändig sind, muss man sich einen recht effizienten Algo überlegen, um diesen Overhead möglichst zu reduzieren. Und nicht immer, wenn man am Ende der Queue ist, muss man wirklich neuen Speicher holen; oftmals reicht es auch einfach, den Platz, der vorne wieder geworden ist, durch Umkopieren wieder zu belegen.
Flamefire hat folgendes geschrieben : | beim löschen eines einrtrags müsste ich dann immer alle einträge die danach kommen bis zur anz.gespeicherter einträge eins rückwärts kopieren. |
Falsch. Du setzt einfach den Tail-Zeiger einen Eintrag weiter ... Daher die O(1) für's Löschen. Speicherverwaltung brauchst Du in dem Fall nicht zu beachten, da Du ansonsten die Queue wieder verkleinerst, obwohl Du wahrscheinlich den Speicher gleich wieder brauchen wirst.
Flamefire hat folgendes geschrieben : | das würde das häufige neu allocieren von speicher ersparen und auch das freigeben.
wie genau meintest du das? |
Hab mal bissl was dazu ergänzt. Ich hab bei meinen Experimenten damit rausgefunden, dass grad für Breitensuche man oftmals einmal am Anfang eine Spitze hat und dann einen recht homogenen Verlauf, also keine wirklichen Peaks, sondern eher Beulen. Durch das Speichermanagement, was ich beschrieben hab, wird genau solch eine Speichernutzung begünstigt.
Flamefire hat folgendes geschrieben : | warum brauche ich "head"? reicht nicht nur tail? und was soll itemcount sein? ist das nicht gleich tail? oder ist das tail-head? |
Siehe die beiden Wiki-Artikel. itemcount := (head-tail+Length(Data)) mod Length(Data) Daher meine Anmerkung, das ist optional, wenn man den Ringpuffer nicht ganz voll laufen lässt.
P.S.: Die Implementation zum Ringpuffer ist Inline in TOAIBackTracing.Execute enthalten. Das sind die Geschichten, die auf die FIFO*-Felder zugreifen.
_________________ Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
|
|
Thorsten83
      
Beiträge: 191
Erhaltene Danke: 1
|
Verfasst: Do 26.03.09 18:58
Hey,
ist dafür nicht eine Prioritätswarteschlange einfacher und besser?
Dadurch würde man den nächsten Knoten in O(log(n)) statt O(n) finden, wenn's als binärer Heap gemacht wird, als Fibb-Heap noch schneller...
|
|
BenBE
      
Beiträge: 8721
Erhaltene Danke: 191
Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
|
Verfasst: Do 26.03.09 19:04
Bei der Breitensuche brauchst Du nur einen beliebigen Knoten finden. Und das geht in O(1). Sicherlich ist es hier sinnvoll, die Knoten möglichst sortiert aufzunehmen, aber eine strenge Sortierung, wie sie sortierte Heap-Strukturen aufweisen ist nicht nötig, bzw. bringt nur marginale Vorteile.
P.S.: Ich betrachte mein Zimmer auch als Heap und komme da trotzdem schnell zum Gesuchten Objekt  Aufräumen ist was für Weicheier 
_________________ Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
|
|
Flamefire 
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Do 26.03.09 19:45
ein ringpuffer ist im prinzip also nur eine implementierung einer queue (warteschlange). also FIFO prinzip
dann entfällt das umkopieren beim entfernen usw.
ok stimmt soweit
nur habe ich keine warteschlange, weil ich keine reine breitensuche habe
ich habe eine Prioritätswarteschlange, wobei sich allerdings die prioritäten häufig ändern, so dass ich jedesmal umsortieren müsste, was das ganze dann doch wieder vieles an vorteil nimmt
denn wenn man hier z.b. einen AVL-baum hat (also balancierter binärbaum, beim dem die linken einträge kleiner und die rechten einträge größer als der aktuelle schlüssel ist) müsste man häufig schlüssel entfernen und hat auf einmal freie enden. wäre die effektivität wieder weg.
|
|
Thorsten83
      
Beiträge: 191
Erhaltene Danke: 1
|
Verfasst: Do 26.03.09 20:02
Hab mal eine quick&dirty-Lösung programmiert...
Für 1000x1000 Knoten brauche ich ca. 1,5 Sekunden (mit binärem Heap)
Du musst halt folgendes bedenken:
Wenn du in deiner Warteschlange 10.000 Elemente hast brauchst du mit einem einfachen Array
- 10.000 Zugriffe zum Finden des Minimums
- 1 Zugriff zum "Aufrücken"
Mit nem Heap:
- 14 Zugriffe zum Entfernen des Minimums
- Im schlimmsten Fall für (in diesem Beispiel) 8 Knoten ein erneutes Einsortieren, also 8*14 = 112 Zugriffe...
|
|
Flamefire 
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Do 26.03.09 20:26
Hast du die Lösung noch?
Würde jetzt ungern selbst eine schreiben wenn du deine noch hast
mir würden die deklarationen und die prozedur für das sortierte einfügen reichen.
ansonsten wäre noch die möglichkeit der sortierten liste
das einfügen dauert länger
aber das auslesen geht in 1 schritt
|
|
Thorsten83
      
Beiträge: 191
Erhaltene Danke: 1
|
Verfasst: Do 26.03.09 20:29
ups, ich nehme alles zurück und behaupte das Gegenteil
Wenn ich keinen Heap benutze bin ich bei 800ms...
Bzw. ich benutze den Heap schon noch, nehme allerdings als Key überall 0, damit verkommt das Ding zu einem dynamischen Array.
Ich kann den Source hier gerne posten, da ich auf dem Laptop aber weder C# noch Delphi habe ist das in Java geschrieben...
edit: Hier ist er:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118:
| public class BinaryHeap { private int[] keys; private Node[] values; private int initialSize = 16; private int last; private int size; public BinaryHeap(){ keys = new int[initialSize]; values = new Node[initialSize]; last = 0; size = initialSize; } public boolean isEmpty(){ return last == 0; } public void insert(int key, Node node){ /*=== resize if array is full ===*/ if(last+1 == keys.length){ resize(2*keys.length, keys.length); } last++; keys[last] = key; values[last] = node; node.indexOnList = last; upheap(last); } public Node min(){ return values[1]; } public Node removeMin(){ Node result = values[1]; keys[1] = keys[last]; values[1] = values[last]; last--; downheap(1); /*=== resize if the array's almost empty ===*/ if(last<size/4 && last >= initialSize){ resize(size/2, size); } return result; } private void downheap(int index){ if(index < last){ int lSun = index*2; int rSun = lSun+1; int minSun; if(rSun<=last){ if(keys[rSun]<keys[lSun]){ minSun = rSun; } else{ minSun = lSun; } if(keys[minSun]<keys[index]){ swap(keys, minSun, index); swap(values, minSun, index); downheap(minSun); } } else if (lSun<=last){ if(keys[lSun]<keys[index]){ swap(keys, lSun, index); swap(values, lSun, index); downheap(lSun); } } } } private void upheap(int index){ if(index>1){ int father = index/2; if(keys[father] > keys[index]){ swap(keys, father, index); swap(values, father, index); upheap(father); } } } private void swap(int[] arr, int a, int b){ int tmp = arr[a]; arr[a] = arr[b]; arr[b] = tmp; } private void swap(Node[] arr, int a, int b){ /*=== swap the Nodes: ===*/ Node tmp = arr[a]; arr[a] = arr[b]; arr[b] = tmp; /*=== swap the indices, too ===*/ arr[a].indexOnList = a; arr[b].indexOnList = b; } private void resize(int newSize, int oldSize){ int[] arr = new int[newSize]; Node[] arr2 = new Node[newSize]; size = newSize; int n = min(newSize, oldSize); for(int i=0 ; i<n ; i++){ arr[i] = keys[i]; arr2[i] = values[i]; } keys = arr; values = arr2; } private int min(int a, int b){ if(a<b) return a; return b; } public int size(){ return last; } public void changeDist(Node node, int newDist){ node.dist = newDist; keys[node.indexOnList] = newDist; upheap(node.indexOnList); downheap(node.indexOnList); } } |
|
|
BenBE
      
Beiträge: 8721
Erhaltene Danke: 191
Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
|
Verfasst: Do 26.03.09 20:54
Hier mal meine Implementierung im Anhang. Ist noch die Original-Fassung (einer langweiligen Informatik-Stunde von vor 4 Jahren) ohne großartige Optimierungen. Nutz hier das Canvas als Datenspeicher --> Daher ist die Performance etwas arg mies.
Einloggen, um Attachments anzusehen!
_________________ Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
|
|
Flamefire 
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Do 26.03.09 22:33
Zuletzt bearbeitet von Flamefire am Mo 13.04.09 10:02, insgesamt 1-mal bearbeitet
|
|
Flamefire 
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Fr 27.03.09 12:26
ok ich bin fertig
der Algo ist jetzt um den Faktor 10(!!!) schneller. Löst das 1000x1000 Feld in weniger als 1sec
Das reicht aus.
Zur suche nach Feldern habe ich die Intervallhalbierung verwendet. Hat nochmal einiges gebracht.
Wenn jemand noch (mögliche) Fehler im obigen Quelltest findet, sagt bitte bescheid. aber es funktioniert erst mal alles
Zuletzt bearbeitet von Flamefire am Mo 13.04.09 10:01, insgesamt 1-mal bearbeitet
|
|
BenBE
      
Beiträge: 8721
Erhaltene Danke: 191
Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
|
Verfasst: Fr 27.03.09 12:36
Speicherplatz ist heute billig. Du darfst also auch in deinem Quelltext aussagekräftige Bezeichner mit mehr als einem Buchstaben verwenden  Auch für Kommentare bieten heutige Festplatten genug Spielraum 
_________________ Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.
|
|
Flamefire 
      
Beiträge: 1207
Erhaltene Danke: 31
Win 10
Delphi 2009 Pro, C++ (Visual Studio)
|
Verfasst: Fr 27.03.09 12:39
jaja ich weiß...bin aber einfach schreibfaul
und bei mir ist "i,j..." immer eine schleifenvariable "tmp" missbrauche ich für verschiedenes "w,h" sind breite und höhe und "l,r" sind links und rechts
ich will immer so wenig wie möglich haben um schnell was zu sehen. bei ewig langen namen fällt mir das schwerer
kommentare...na gut...aber eigendlich brauch ich die nicht, da ich den quelltext als einziger (normalerweise) zu gesicht bekomme
in dem fall wären sie gut gewesen, aber zu spät
läuft und läuft gut und schluss 
|
|
|