Algoritmo efficiente per il confine di una serie di tessere


12

Ho una griglia di tessere di dimensioni finite note che formano una mappa. Alcune tessere all'interno della mappa sono inserite in un set noto come territorio. Questo territorio è collegato, ma non si sa nulla della sua forma. Il più delle volte sarebbe un blob abbastanza regolare, ma potrebbe essere molto allungato in una direzione e potenzialmente potrebbe anche avere buchi. Sono interessato a trovare il confine (esterno) del territorio.

Cioè, voglio un elenco di tutte le tessere che toccano una delle tessere nel territorio senza che si trovi nel territorio. Qual è un modo efficace per trovarlo?

Per ulteriore difficoltà, capita che le mie tessere siano esadecimali, ma sospetto che ciò non faccia troppa differenza, ogni tessera è ancora etichettata con una coordinata intera xey e, data una tessera, posso facilmente trovare i suoi vicini. Di seguito sono riportati alcuni esempi: il nero è il territorio e il blu il bordo che voglio trovare. Esempio di territori e frontiere Questo di per sé non è un problema difficile. Un semplice algoritmo per questo, in pseudo-python, è:

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)

Tuttavia, questo è lento e vorrei qualcosa di meglio. Ho un ciclo O (n) sul territorio, un altro ciclo (breve, ma comunque) su tutti i vicini, e quindi devo controllare l'appartenenza su due elenchi, uno dei quali è di dimensioni n. Questo dà un terribile ridimensionamento di O (n ^ 2). Posso ridurlo a O (n) usando i set anziché gli elenchi per confine e territorio in modo che l'abbonamento sia veloce da controllare, ma non è comunque eccezionale. Mi aspetto che ci siano molti casi in cui il territorio è grande ma il bordo è piccolo a causa di un semplice ridimensionamento area vs linea. Ad esempio, se il territorio è un esagono di raggio 5, ha dimensioni 91 ma il bordo ha solo dimensioni 36.

Qualcuno può proporre qualcosa di meglio?

Modificare:

Per rispondere ad alcune delle domande seguenti. Il territorio può variare in dimensioni, da circa 20 a 100 circa. L'insieme di tessere che formano il territorio è un attributo di un oggetto ed è questo oggetto che ha bisogno di un insieme di tutte le tessere bordo.

Inizialmente il territorio viene creato come un blocco, quindi ottiene principalmente tessere una per una. In questo caso, è vero che il modo più veloce è solo quello di mantenere un set di bordi e aggiornarlo solo sul riquadro ottenuto. Occasionalmente potrebbe verificarsi un grande cambiamento nel territorio, quindi dovrà essere ricalcolato completamente.

Sono ora dell'opinione che fare la soluzione migliore sia un semplice algoritmo di ricerca dei confini. L'unica ulteriore complessità che ciò comporta è garantire che il confine venga ricalcolato ogni volta che potrebbe essere necessario, ma non di più. Sono abbastanza fiducioso che questo può essere fatto in modo affidabile nel mio quadro attuale.

Per quanto riguarda i tempi, nel mio codice attuale ho alcune routine che devono controllare tutte le tessere del territorio. Non tutti i turni, ma al momento della creazione e occasionalmente in seguito. Ciò richiede oltre il 50% del tempo di esecuzione della mia tuta di codice di prova anche se è una parte molto piccola del programma completo. Ero quindi desideroso di ridurre al minimo le ripetizioni. TUTTAVIA, il codice di test comporta molta più creazione di oggetti rispetto a una normale esecuzione del programma (naturalmente), quindi mi rendo conto che questo potrebbe non essere molto rilevante.


10
Penso che se non si sa nulla della forma, un algoritmo O (N) sembra ragionevole. Qualcosa di più veloce richiederebbe di non guardare ogni elemento del territorio, il che funzionerebbe solo se tu sapessi qualcosa sulla forma, penso.
tra

3
Probabilmente non è necessario farlo molto spesso. Inoltre n non è molto grande, molto meno del numero totale di tessere.
Trilarion,

1
Come vengono create / modificate queste aree? E quanto spesso cambiano? Se sono selezionati tessera per tessera, puoi costruire il tuo elenco di vicini mentre vai, e se non cambiano frequentemente, puoi archiviare una serie di territori e i loro confini e aggiungere o rimuovere da loro mentre procedi (quindi no bisogno di ricalcolarli costantemente).
DaveMongoose,

2
Importante: si tratta di un vero problema di prestazioni diagnosticato e profilato? Con un problema impostato così piccolo (solo poche centinaia di elementi, davvero?) Non penso davvero che questo O (n ^ 2) o O (n) dovrebbe essere un problema. Sembra un'ottimizzazione prematura su un sistema che non verrà eseguito su tutti i frame.
Delioth,

1
L'algoritmo semplice è O (n) poiché ci sono al massimo 6 vicini.
Eric

Risposte:


11

La ricerca di un algoritmo viene generalmente eseguita meglio con una struttura di dati che semplifica l'algoritmo.

In questo caso, il tuo territorio.

Il territorio dovrebbe essere un insieme non ordinato (O (1) hash) di bordi ed elementi.

Ogni volta che aggiungi un elemento al territorio, fai scorrere le tessere adiacenti e vedi se devono essere tessere di bordo; in questo caso, sono una tessera bordo se non sono una tessera elemento.

Ogni volta che sottrai un elemento dal territorio, ti assicuri che le sue tessere adiacenti siano ancora nel territorio e vedi se te stesso dovrebbe diventare una tessera bordo. Se hai bisogno che questo sia veloce, chiedi alle tessere del bordo di tenere traccia del loro "conteggio adiacente".

Questo richiede O (1) lavoro ogni volta che aggiungi o rimuovi una tessera da o verso un territorio. La visita del bordo richiede O (lunghezza del bordo). Finché vuoi sapere "cos'è il bordo" in modo significativamente più frequente di quanto aggiungi / rimuovi elementi dal territorio, questo dovrebbe vincere.


9

Se hai bisogno di trovare bordi dei buchi anche nel mezzo del tuo territorio, allora il tuo lineare nell'area del confine del territorio è il meglio che possiamo fare. Qualsiasi piastrella all'interno potrebbe potenzialmente essere un buco che dobbiamo contare, quindi dobbiamo guardare ogni piastrella nell'area delimitata dal contorno del territorio almeno una volta per essere sicuri di aver trovato tutti i buchi.

Ma se ti preoccupi solo di trovare il bordo esterno (non i fori interni), possiamo farlo in modo un po 'più efficiente:

  1. Trova un bordo che separa il tuo territorio. Puoi farlo tramite ...

    • (se conosci almeno un riquadro del territorio e sai di avere esattamente un BLOB di territorio collegato sulla mappa)

      ... a partire da una tessera arbitraria nel tuo territorio e spostandoti verso il bordo più vicino della tua mappa. Mentre lo fai, ricorda l'ultimo bordo in cui sei passato da una tessera territorio a una tessera non territorio. Una volta colpito il bordo della mappa, questo bordo ricordato è il bordo iniziale.

      Questa scansione è lineare nel diametro della mappa.

    • oppure (se non sai dove sono in anticipo le tessere del tuo territorio o la tua mappa può contenere diversi territori disconnessi)

      ... iniziando da un bordo della mappa, scansiona lungo ogni riga fino a colpire una tessera terreno. L'ultimo bordo che hai attraversato da non terreno a terreno è il tuo bordo iniziale.

      Questa scansione potrebbe essere nel peggiore dei casi lineare nell'area della mappa (quadratica nel suo diametro), ma se hai dei limiti per limitare la tua ricerca (diciamo, sai che il territorio attraversa quasi sempre le file centrali) puoi migliorare questo peggiore- comportamento del caso.

  2. A partire dal bordo di partenza trovato nel passaggio 1, seguilo attorno al perimetro del tuo terreno, aggiungendo ogni tessera non terreno esterna alla raccolta dei bordi, fino a tornare al bordo di partenza.

    Questo passaggio successivo è lineare nel perimetro del contorno del terreno, anziché nella sua area. Il rovescio della medaglia è che il codice è più complicato perché è necessario tenere conto di ogni tipo di svolta che può essere eseguita dal bordo ed evitare il doppio conteggio delle tessere bordo agli ingressi.

Se i tuoi esempi sono rappresentativi delle dimensioni dei tuoi dati reali entro un paio di ordini di grandezza, allora io stesso andrei per la ricerca ingenua dell'area - sarà ancora incredibilmente veloce su un numero così piccolo di tessere, ed è sostanzialmente più semplice da scrivere , capire e mantenere (in genere porta a un minor numero di bug!)


7

Avviso : la presenza o meno di una tessera sul bordo dipende solo da essa e dai suoi vicini.

Per questo motivo:

  • È facile eseguire questa query pigramente. Ad esempio: non è necessario cercare il confine sull'intera mappa, ma solo su ciò che è visibile.

  • È facile eseguire questa query in parallelo. In effetti, potrei immaginare un codice shader che lo faccia. E se ne hai bisogno per qualcos'altro che la visualizzazione, puoi renderizzare una trama e usarla.

  • Se una tessera cambia, il limite cambia solo localmente, il che significa che non è necessario calcolare nuovamente l'intera cosa.

Puoi anche pre-calcolare il confine. Cioè, se stai popolando l'esagono, puoi decidere se una tessera è limite in quel momento. Ciò significa che:

  • Se usi un loop per popolare la griglia, ed è lo stesso che usi per decidere cosa è un limite.
  • Se inizi con una griglia vuota e scegli i riquadri da modificare, puoi aggiornare il confine localmente.

Non utilizzare un elenco per il confine. Utilizzare un set se proprio lo si deve ( non so per cosa si desidera il limite ). Tuttavia, se si crea qualunque riquadro sia un limite o meno un attributo del riquadro, non è necessario passare a un'altra struttura di dati per verificare.


2

Sposta la tua area in alto di una tessera, poi in alto a destra, poi in basso a destra, ecc. Successivamente rimuovi l'area originale.

L'unione di tutti e sei i set dovrebbe essere O (n), l'ordinamento O (n.log (n)), impostare la differenza O (n). Se i riquadri originali sono memorizzati in un formato ordinato, il set unito può essere ordinato anche in O (n).

Non penso che esista un algoritmo con meno di O (n), dato che è necessario accedere a ciascun riquadro almeno una volta.


1

Ho appena scritto un post sul blog su come farlo. Questo utilizza il primo metodo menzionato da @DMGregory iniziando con una cella di bordo e marciando attorno al perimetro. È in C # invece di Python ma dovrebbe essere abbastanza facile da adattare.

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


0

POSTO ORIGINALE:

Non posso commentare questo sito, quindi proverò a rispondere con un algoritmo pseudocodice.

Sai che ogni singolo territorio ha al massimo sei vicini che fanno parte del confine. Per ogni riquadro del territorio, aggiungi i sei riquadri vicini a un potenziale elenco di bordi. Quindi sottrai ogni tessera nel territorio dal bordo e rimarrai solo con le tessere bordo. Funzionerà meglio se si utilizza un set non ordinato per memorizzare ogni elenco. Spero di essere stato utile.

MODIFICA Ci sono modi molto più efficaci della semplice iterazione. Come ho cercato di affermare nella mia risposta (ora eliminata) di seguito, puoi ottenere O (1) nei casi migliori e O (n) nel caso peggiore.

Aggiunta di una tessera a un territorio O (1) - O (N):

Nel caso di non vicini, devi semplicemente creare un nuovo territorio.

Nel caso di un vicino, aggiungi la nuova tessera al territorio esistente.

Nel caso di 5 o 6 vicini, sai che è tutto collegato, quindi aggiungi la nuova tessera al territorio esistente. Queste sono tutte operazioni O (1) e anche l'aggiornamento dei nuovi territori di confine è O (1), poiché è una semplice unione di un insieme con un altro.

Nel caso di 2, 3 o 4 territori vicini, potresti dover unire fino a 3 territori unici. Questo è O (N) sulla dimensione del territorio combinato.

Rimozione di una piastrella da un territorio O (1) - O (N):

Con zero vicini cancella il territorio. O (1)

Con un vicino rimuovi la tessera dal territorio. O (1)

Con due o più vicini, è possibile creare fino a 3 nuovi territori. Questo è O (N).

Nelle ultime settimane ho trascorso il mio tempo libero a sviluppare un programma dimostrativo che è un semplice gioco di territorio a base esadecimale. Cerca di aumentare le tue entrate posizionando i territori uno accanto all'altro. 3 giocatori, rosso, verde e blu competono per generare il maggior guadagno posizionando strategicamente le tessere su un campo di gioco limitato.

Puoi scaricare il gioco qui (in formato .7z) hex.7z

Semplice controllo del mouse LMB posiziona una tessera (può essere posizionata solo dove evidenziata con il mouse). Punteggio in alto, reddito in basso. Vedi se riesci a trovare una strategia efficace.

Il codice può essere trovato qui:

Aquila / EagleTest

Per compilare dal codice sorgente sono necessari Eagle e Allegro 5. Entrambi compilano con cmake. Il gioco esadecimale si sviluppa attualmente con il progetto CB.

Capovolgi quei voti negativi. :)


Questo è essenzialmente ciò che fa l'algoritmo nell'OP, anche se controllare i riquadri vicini prima dell'inclusione è leggermente più veloce che rimuoverli tutti alla fine.
ScienceSnake

È praticamente lo stesso, ma se li sottraggerai solo una volta più efficiente
BugSquasher

Ho completamente aggiornato la mia risposta ed eliminato la risposta extranneous di seguito.
BugSquasher,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.