Effizienter Algorithmus für die Begrenzung einer Reihe von Kacheln

12

Ich habe ein Raster aus Kacheln bekannter endlicher Größe, die eine Karte bilden. Einige der Kacheln auf der Karte werden in ein Gebiet gelegt. Dieses Gebiet ist verbunden, aber über seine Form ist nichts bekannt. Meistens war es ein ziemlich regelmäßiger Fleck, aber er konnte in eine Richtung sehr lang sein und möglicherweise sogar Löcher haben. Ich bin daran interessiert, die (äußere) Grenze des Territoriums zu finden.

Das heißt, ich möchte eine Liste aller Kacheln, die eine der Kacheln im Gebiet berühren, ohne sich selbst im Gebiet zu befinden. Was ist ein effizienter Weg, dies zu finden?

Für zusätzliche Schwierigkeiten kommt es vor, dass meine Kacheln hexadezimal sind, aber ich vermute, dass dies keinen allzu großen Unterschied macht. Jede Kachel ist immer noch mit einer ganzzahligen x- und y-Koordinate beschriftet, und bei einer gegebenen Kachel kann ich leicht ihre Nachbarn finden. Nachfolgend einige Beispiele: Das Schwarze ist das Territorium und das Blaue die Grenze, die ich finden möchte. Beispiel für Gebiete und Grenzen Dies ist an sich kein schwieriges Problem. Ein einfacher Algorithmus hierfür ist in Pseudopython:

def find_border_of_territory(territory):
    border = []
    for tile in territory:
        for neighbor in tile.neighbors():
            if neighbor not in territory and neighbor not in border:
                border.add(neighbor)

Dies ist jedoch langsam und ich hätte gerne etwas Besseres. Ich habe eine O (n) -Schleife über das Territorium, eine andere Schleife (eine kurze, aber immer noch) über alle Nachbarn, und dann muss ich die Mitgliedschaft über zwei Listen überprüfen, von denen eine die Größe n hat. Das ergibt eine schreckliche Skalierung von O (n ^ 2). Ich kann das auf O (n) reduzieren, indem ich Sets anstelle von Listen für Grenze und Territorium verwende, damit die Mitgliedschaft schnell überprüft werden kann, aber es ist immer noch nicht großartig. Ich gehe davon aus, dass es viele Fälle geben wird, in denen das Territorium groß ist, die Grenze jedoch aufgrund einer einfachen Skalierung zwischen Fläche und Linie klein. Wenn das Territorium beispielsweise ein Feld mit dem Radius 5 ist, hat es die Größe 91, der Rand jedoch nur die Größe 36.

Kann jemand etwas Besseres vorschlagen?

Bearbeiten:

Um einige der folgenden Fragen zu beantworten. Das Territorium kann eine Größe von etwa 20 bis 100 haben. Die Menge der Kacheln, die das Territorium bilden, ist ein Attribut eines Objekts, und für dieses Objekt ist eine Menge aller Randkacheln erforderlich.

Zunächst wird das Territorium als Block erstellt und erhält dann meist nacheinander Kacheln. In diesem Fall ist es wahr, dass der schnellste Weg darin besteht, nur einen Rahmen zu behalten und ihn nur auf dem gewonnenen Plättchen zu aktualisieren. Gelegentlich kann es zu einer großen Änderung des Gebiets kommen. Daher muss es dann vollständig neu berechnet werden.

Ich bin jetzt der Meinung, dass ein einfacher Grenzfindungsalgorithmus die beste Lösung ist. Die einzige zusätzliche Komplexität, die dadurch entsteht, besteht darin, sicherzustellen, dass die Grenze jedes Mal neu berechnet wird, wenn dies erforderlich ist, jedoch nicht mehr. Ich bin ziemlich zuversichtlich, dass dies in meinem aktuellen Framework zuverlässig möglich ist.

Was das Timing angeht, habe ich in meinem aktuellen Code einige Routinen, die jede Kachel des Gebiets überprüfen müssen. Nicht jede Runde, aber bei der Erstellung und gelegentlich danach. Das nimmt über 50% der Laufzeit meines Testcodes ein, obwohl es nur ein sehr kleiner Teil des gesamten Programms ist. Ich war daher bestrebt, alle Wiederholungen zu minimieren. JEDOCH beinhaltet der Testcode (natürlich) viel mehr Objekterstellung als ein normaler Programmablauf, daher ist mir klar, dass dies möglicherweise nicht sehr relevant ist.

ScienceSnake
quelle
10
Ich denke, wenn nichts über die Form bekannt ist, erscheint ein O (N) -Algorithmus vernünftig. Alles, was schneller ist, würde erfordern, nicht jedes Element des Territoriums zu betrachten, was nur funktionieren würde, wenn Sie etwas über die Form wissen, denke ich.
01:46 Uhr
3
Sie müssen das wahrscheinlich nicht sehr oft tun. Auch ist n nicht sehr groß, viel weniger als die Gesamtzahl der Kacheln.
Trilarion
1
Wie werden diese Bereiche angelegt / verändert? Und wie oft wechseln sie? Wenn sie Kachel für Kachel ausgewählt werden, können Sie Ihre Liste der Nachbarn im Laufe der Zeit zusammenstellen. Wenn sie sich nicht häufig ändern, können Sie eine Reihe von Gebieten und deren Grenzen speichern und sie im Laufe der Zeit hinzufügen oder entfernen (also nein müssen sie ständig neu berechnen).
DaveMongoose
2
Wichtig: Handelt es sich um ein tatsächlich diagnostiziertes und profiliertes Leistungsproblem? Bei einem so kleinen Problem (nur ein paar hundert Elemente, wirklich?) Denke ich nicht, dass dieses O (n ^ 2) oder O (n) ein Problem sein sollte. Klingt nach vorzeitiger Optimierung auf einem System, das nicht in jedem Frame ausgeführt wird.
Delioth
1
Der einfache Algorithmus ist O (n), da es höchstens 6 Nachbarn gibt.
Eric

Antworten:

11

Das Auffinden eines Algorithmus erfolgt normalerweise am besten mit einer Datenstruktur, die den Algorithmus vereinfacht.

In diesem Fall Ihr Territorium.

Das Territorium sollte eine ungeordnete (O (1) -Hash) Menge von Grenzen und Elementen sein.

Jedes Mal, wenn Sie dem Gebiet ein Element hinzufügen, durchlaufen Sie benachbarte Kacheln und prüfen, ob es sich um eine Randkachel handeln soll. In diesem Fall handelt es sich um eine Randkachel, wenn es sich nicht um eine Elementkachel handelt.

Wenn Sie ein Element vom Territorium subtrahieren, stellen Sie sicher, dass sich die angrenzenden Kacheln noch im Territorium befinden, und Sie sehen, ob Sie selbst ein Randkachel werden sollten. Wenn dies schnell sein soll, lassen Sie die Randkacheln die Anzahl der benachbarten Kacheln verfolgen.

Dies nimmt O (1) Arbeit in Anspruch, wenn Sie einem Gebiet eine Kachel hinzufügen oder daraus entfernen. Der Besuch der Grenze dauert O (Grenzlänge). Solange Sie deutlich häufiger wissen möchten, wie hoch die Grenze ist, als Sie Elemente zum Gebiet hinzufügen / daraus entfernen, sollte dies gewinnen.

Yakk
quelle
9

Wenn Sie auch in der Mitte Ihres Territoriums Kanten von Löchern finden müssen, ist Ihre Linearität im Bereich der Gebietsgrenze das Beste, was wir tun können. Jedes Plättchen im Inneren könnte möglicherweise ein Loch sein, das wir zählen müssen. Wir müssen daher jedes Plättchen in dem durch die Umrisse des Gebiets begrenzten Bereich mindestens einmal untersuchen, um sicherzugehen, dass wir alle Löcher gefunden haben.

Wenn Sie jedoch nur den äußeren Rand suchen (nicht die inneren Löcher), können wir dies effizienter tun:

  1. Finden Sie eine Kante, die Ihr Territorium trennt. Sie können dies tun, indem Sie ...

    • (Wenn Sie mindestens ein Gebietsplättchen kennen und wissen, dass Sie genau ein verbundenes Gebietsplättchen auf Ihrer Karte haben)

      ... beginnend bei einer beliebigen Kachel in Ihrem Gebiet bis zum nächsten Rand Ihrer Karte. Denken Sie dabei an die letzte Kante, an der Sie von einem Gebietsplättchen zu einem Gebietsfremden übergegangen sind. Sobald Sie die Kante der Karte erreicht haben, ist diese gespeicherte Kante Ihre Startkante.

      Dieser Scan ist im Durchmesser der Karte linear.

    • oder (wenn Sie vorab nicht wissen, wo sich eines Ihrer Gebietsplättchen befindet, oder Ihre Karte möglicherweise mehrere nicht verbundene Gebiete enthält)

      ... scannen Sie ab einem Kartenrand in jeder Zeile, bis Sie auf ein Geländefeld stoßen. Die letzte Kante, die Sie von Gelände zu Gelände überquert haben, ist Ihre Startkante.

      Dieser Scan kann im Bereich der Karte im schlimmsten Fall linear sein (quadratischer Durchmesser). Wenn Sie jedoch Einschränkungen für Ihre Suche haben (z. B. wenn Sie wissen, dass das Gebiet fast immer die mittleren Zeilen kreuzt), können Sie diesen Fehler beheben. Fallverhalten.

  2. Beginnen Sie an Ihrer in Schritt 1 gefundenen Startkante und folgen Sie ihr entlang des Umfangs Ihres Geländes. Fügen Sie dann jedes Nicht-Geländefeld an der Außenseite zu Ihrer Randkollektion hinzu, bis Sie zur Startkante zurückkehren.

    Dieser Schritt zum Verfolgen der Kante verläuft nicht in der Fläche, sondern im Umfang Ihrer Geländekontur linear . Der Nachteil ist, dass der Code komplizierter ist, da Sie jede Art von Drehung berücksichtigen müssen, die die Kante ausführen kann, und vermeiden, dass Randkacheln an den Eingängen doppelt gezählt werden.

Wenn Ihre Beispiele für Ihre reale Datengröße innerhalb weniger Größenordnungen repräsentativ sind, würde ich mich für die naive Bereichssuche entscheiden - sie ist auf einer so kleinen Anzahl von Kacheln immer noch blitzschnell und wesentlich einfacher zu schreiben , verstehen und pflegen (was normalerweise zu weniger Fehlern führt!)

DMGregory
quelle
7

Hinweis : Ob sich ein Plättchen an der Grenze befindet oder nicht, hängt nur von ihm und seinen Nachbarn ab.

Deswegen:

  • Es ist einfach, diese Abfrage träge auszuführen. Zum Beispiel: Sie müssen nicht auf der gesamten Karte nach der Grenze suchen, sondern nur auf dem, was sichtbar ist.

  • Es ist einfach, diese Abfrage parallel auszuführen. Tatsächlich könnte ich mir einen Shader-Code vorstellen, der dies tut. Und wenn Sie es für etwas anderes als die Visualisierung benötigen, können Sie es in eine Textur rendern und diese verwenden.

  • Wenn sich eine Kachel ändert, ändert sich die Grenze nur lokal. Sie müssen also nicht das Ganze erneut berechnen.

Sie können die Grenze auch vorberechnen. Das heißt, wenn Sie das Hexfeld bevölkern, können Sie entscheiden, ob zu diesem Zeitpunkt eine Kachel begrenzt ist. Das bedeutet, dass:

  • Wenn Sie eine Schleife verwenden, um das Raster aufzufüllen, können Sie auch festlegen, was eine Grenze ist.
  • Wenn Sie mit einem leeren Raster beginnen und zu ändernde Kacheln auswählen, können Sie die Grenze lokal aktualisieren.

Verwenden Sie keine Liste für die Grenze. Verwenden Sie ein Set, wenn Sie wirklich müssen ( ich weiß nicht, wofür Sie die Grenze wollen. ). Wenn Sie jedoch festlegen, dass eine Kachel eine Begrenzung oder kein Attribut der Kachel ist, müssen Sie nicht in eine andere Datenstruktur wechseln, um dies zu überprüfen.

Theraot
quelle
2

Bewegen Sie Ihren Bereich um eine Kachel nach oben, rechts und rechts nach unten. Entfernen Sie anschließend den ursprünglichen Bereich.

Das Zusammenführen aller sechs Sätze sollte O (n), das Sortieren von O (n.log (n)) und die Differenz von O (n) sein. Wenn die ursprünglichen Kacheln in einem sortierten Format gespeichert sind, kann der zusammengeführte Satz auch in O (n) sortiert werden.

Ich glaube nicht, dass es einen Algorithmus mit weniger als O (n) gibt, da Sie auf jede Kachel mindestens einmal zugreifen müssen.

tommsch
quelle
1

Ich habe gerade einen Blog-Beitrag darüber geschrieben, wie das geht. Dies verwendet die erste Methode, die @DMGregory erwähnt hat, beginnend mit einer Kantenzelle und um den Umfang herumgehend. Es ist in C # anstelle von Python, sollte aber ziemlich einfach anzupassen sein.

https://dillonshook.com/hex-city-borders/

DShook
quelle
0

ORIGINAL POST:

Ich kann diese Site nicht kommentieren, daher werde ich versuchen, mit einem Pseudocode-Algorithmus zu antworten.

Sie wissen, dass jedes einzelne Territorium höchstens sechs Nachbarn hat, die Teil der Grenze sind. Fügen Sie für jedes Plättchen im Gebiet die sechs benachbarten Plättchen einer potenziellen Grenzliste hinzu. Dann subtrahiere jedes Plättchen im Territorium von der Grenze und du hast nur noch die Randplättchen. Es funktioniert am besten, wenn Sie eine ungeordnete Menge zum Speichern jeder Liste verwenden. Hoffe ich war hilfreich.

BEARBEITEN Es gibt viel effektivere Möglichkeiten als die einfache Iteration. Wie ich in meiner (jetzt gelöschten) Antwort weiter unten dargelegt habe, können Sie im besten Fall O (1) und im schlechtesten Fall O (n) erreichen.

Hinzufügen eines Plättchens zu einem Gebiet O (1) - O (N):

Wenn Sie keine Nachbarn haben, legen Sie einfach ein neues Territorium an.

Bei einem Nachbarn fügen Sie das neue Plättchen dem vorhandenen Gebiet hinzu.

Bei 5 oder 6 Nachbarn wissen Sie, dass alles miteinander verbunden ist, und fügen die neue Karte dem vorhandenen Gebiet hinzu. Dies sind alles O (1) -Operationen, und die Aktualisierung der neuen Grenzgebiete ist ebenfalls O (1), da es sich um eine einfache Zusammenführung eines Satzes mit einem anderen handelt.

Bei 2, 3 oder 4 benachbarten Gebieten müssen Sie möglicherweise bis zu 3 einzelne Gebiete zusammenlegen. Dies ist O (N) für die kombinierte Gebietsgröße.

Entfernen eines Plättchens aus einem Gebiet O (1) - O (N):

Mit null Nachbarn löschen Sie das Territorium. O (1)

Mit einem Nachbarn entferne das Plättchen aus dem Territorium. O (1)

Bei zwei oder mehr Nachbarn können bis zu drei neue Gebiete erstellt werden. Das ist O (N).

Ich habe in den letzten Wochen meine Freizeit damit verbracht, ein Demonstrationsprogramm zu entwickeln, das ein einfaches Gebietsspiel auf Hex-Basis ist. Versuchen Sie, Ihr Einkommen zu erhöhen, indem Sie Gebiete nebeneinander platzieren. 3 Spieler, Rot, Grün und Blau, konkurrieren um die meisten Einnahmen, indem sie strategisch Kacheln auf einem begrenzten Spielfeld platzieren.

Sie können das Spiel hier herunterladen (im .7z-Format) hex.7z

Einfache Maussteuerung LMB platziert eine Kachel (kann nur dort platziert werden, wo sie mit der Maus markiert ist). Ergebnis oben, Einkommen unten. Finden Sie heraus, ob Sie eine effektive Strategie entwickeln können.

Code finden Sie hier:

Eagle / EagleTest

Um aus dem Quellcode zu bauen, benötigen Sie Eagle und Allegro 5. Beide bauen mit cmake. Hex-Spiel baut derzeit mit CB-Projekt.

Drehen Sie diese Stimmen auf den Kopf. :)

BugSquasher
quelle
Dies ist im Wesentlichen das, was der Algorithmus im OP tut, obwohl das Überprüfen der Nachbarkacheln vor dem Einbeziehen etwas schneller ist als das Entfernen aller Kacheln am Ende.
ScienceSnake
Es ist im Grunde das gleiche, aber wenn Sie sie nur einmal subtrahieren, ist es effizienter
BugSquasher
Ich habe meine Antwort vollständig aktualisiert und die unten stehende extranneous Antwort gelöscht.
BugSquasher