Il mio algoritmo che estrae la scatola più grande che può essere fatta da scatole più piccole, è troppo lento


9

Immagina un mondo basato su cubi (come Minecraft, Trove o Cube World) in cui tutto è composto da cubi di dimensioni identiche e tutti i cubi sono dello stesso tipo .

L'obiettivo è quello di rappresentare il mondo con il minor numero di scatole rettangolari (unendo i cubi ma mantenendo la forma convessa (ovvero una forma di scatola rettangolare)). Il mio algoritmo ci riesce, ma le sue prestazioni sono troppo lente per lo scopo previsto. Ci sono algoritmi più veloci?

Lo pseudo-codice C # per il mio algoritmo è sostanzialmente:

struct Coordinate { int x,y,z; }; //<-- integer based grid
HashSet<Coordinate> world; // <-- contains all the cubes

//width, height, and length represent how many cubes it spans
struct RectangularBox { Coordinate coord; int width,height,length; }

void Begin()
{
    List<RectangularBox> fewestBoxes = new List<RectangularBox>();
    while(world.Count > 0)
    {
         RectangularBox currentLargest = ExtractLargest();
         fewestBoxes.Add(currentLargest);
         world.RemoveRange(currentLargest.ContainedCubes());
    }
    //done; `fewestBoxes` contains the fewest rectangular boxes needed.
}

private RectangularBox ExtractLargest()
{
    RectangularBox largestBox = new RectangularBox();
    foreach (Coordinate origin in world)
    {
        RectangularBox box = FindMaximumSpan(origin);
        if (box.CalculateVolume() >= largestBox.CalculateVolume())
            largestBox = box;
    }
    return largestBox;
}

private RectangularBox FindMaximumSpan(Coordinate origin)
{
    int maxX, maxY,maxZ;
    while (world.Contains(origin.Offset(maxX, 0, 0))) maxX++;
    while (world.Contains(origin.Offset(0, maxY, 0))) maxY++;
    while (world.Contains(origin.Offset(0, 0, maxZ))) maxZ++;

    RectangularBox largestBox;
    for (int extentX = 0; extentX <= maxX; extentX++)
        for (int extentY = 0; extentY <= maxY; extentY++)
            for (int extentZ = 0; extentZ <= maxZ; extentZ++)
            {
                int lengthX = extentX + 1;
                int lengthY = extentY + 1;
                int lengthZ = extentZ + 1;
                if (BoxIsFilledWithCubes(origin, lengthX, lengthY, lengthZ))
                {
                    int totalVolume = lengthX * lengthY * lengthZ;
                    if (totalVolume >= largestBox.ComputeVolume())
                        largestBox = new RectangularBox(origin, lengthX, lengthY, lengthZ);
                }
                else
                    break;
            }
    return largestBox;
}

private bool BoxIsFilledWithCubes(Coordinate coord, 
    int lengthX, int lengthY, int lengthZ)
{
    for (int gX = 0; gX < lengthX; gX++)
        for (int gY = 0; gY < lengthY; gY++)
            for (int gZ = 0; gZ < lengthZ; gZ++)
                if (!world.Contains(coord.Offset(gX, gY, gZ)))
                    return false;
    return true;
}

In sostanza, per ogni blocco del mondo, trova prima quanti blocchi contigui ci sono in ciascuna delle tre dimensioni positive (+ X, + Y, + Z). E poi riempie un po 'quel volume e controlla qual è il riempimento più grande a cui non mancano blocchi.


Aggiornamento: dal momento che sembravo implicare che questo fosse per il motore di rendering di un gioco, voglio solo chiarire, questo non è per il motore di rendering di un gioco; è per un convertitore di file; nessuna interfaccia grafica.


2
Forse più adatto per codereview.stackexchange.com
Rotem

4
@Rotem Forse, ma in realtà sto cercando algoritmi alternativi piuttosto che una recensione del mio codice. Ho fornito il mio codice proprio come una forza dell'abitudine.
Mr. Smith,

Certo, ha senso.
Rotem,

Le domande sugli algoritmi sono più adatte ai siti SE come l'informatica ...
Bakuriu,

Dipende anche da quanto spesso chiami il metodo. Se lo chiami ogni frame o solo quando cambia un blocco. Questi giochi di solito hanno blocchi (rettangolo di dimensioni specifiche es: blocchi 64x64x32), valori di cache il più possibile e calcolati solo per blocco. E calcola questi valori solo sui blocchi visibili.
the_lotus,

Risposte:


6

Puoi sfruttare il fatto che quando

 BoxIsFilledWithCubes(c,x,y,z)

restituisce vero, quindi non è necessario ricontrollare BoxIsFilledWithCubes(c,x+1,y,z)tutti quei cubi nell'intervallo di coordinate "(c, x, y, z)". Devi solo controllare quei cubi con la nuova coordinata x c.x + (x+1). (Lo stesso vale per y+1o z+1). Più in generale, suddividendo una scatola in due scatole più piccole (per le quali potresti già sapere se sono entrambi pieni di cubi o non entrambi riempiti), puoi applicare una tecnica di divisione e conquista qui, che diventa più veloce del tuo approccio originale quando si memorizzano nella cache i risultati intermedi.

A tale scopo, inizi a implementare in BoxIsFilledWithCubes(c,x,y,z)modo ricorsivo, ad esempio:

 bool BoxIsFilledWithCubes(coord,lx,ly,lz)
 {
     if(lx==0|| ly==0 || lz==0)
        return true;
     if(lx==1 && ly==1 && lz==1)
          return world.Contains(coord);
     if(lx>=ly && lx>=lz)  // if lx is the maximum of lx,ly,lz ....
         return BoxIsFilledWithCubes(coord,lx/2,ly,lz) 
             && BoxIsFilledWithCubes(coord.Offset(lx/2,0,0), lx-lx/2, ly, lz);
     else if(ly>=lz && ly>=lx)  
         // ... analogously when ly or lz is the maximum

 }

e quindi utilizzare la memoization (come è discusso qui ) per evitare chiamate ripetute BoxIsFilledWithCubescon gli stessi parametri. Nota che dovrai cancellare la cache di memoization quando applichi una modifica al tuo worldcontenitore, come per esempio world.RemoveRange. Tuttavia credo che questo renderà il tuo programma più veloce.


5

Costruisci un ottetto con un nodo fogliato con dimensioni pari alla dimensione della tua scatola. Durante l'attraversamento dell'ottetto è possibile unire in modo economico nodi. I nodi completamente riempiti sono banali da unire (nuova casella = parent aabb), mentre per i nodi parzialmente riempiti, puoi usare una variante del tuo algoritmo corrente per verificare la capacità di unione.


Il fatto che due nodi siano completamente riempiti non implica che debbano essere uniti; non è un passo verso la ricerca della scatola più grande; se li unisci, probabilmente dovrai dividerli di nuovo in un secondo momento una volta trovata la casella più grande. Non vedo come un ottetto aiuta in questo scenario.
Mr. Smith,

I nodi completi possono essere uniti nel senso che tutti e 8 i bambini possono diventare parte di un unico riquadro più grande. Questa scatola più grande non dovrà essere divisa in seguito, ma potrebbe essere unita. La gerarchia ti consente di unire rapidamente molte caselle, i nodi completamente riempiti ti consentono di saltare di livello senza ulteriori test.
Wilbert,

1

Sembra che tu sia almeno O (n ^ 2) (vedi la grande notazione O ) mentre esegui il ciclo su tutte le caselle del mondo in "Begin ()", quindi per ciascuna casella, esegui il ciclo su tutte le caselle del mondo in ExtractLargest ( ). Quindi un mondo con 10 scatole non correlate richiederà 4 volte più a lungo di un mondo con 5 scatole non correlate.

Pertanto, è necessario limitare il numero di caselle che deve essere estratto da ExtractLargest (), per fare ciò è necessario utilizzare un tipo di ricerca spaziale , poiché si lavora in 3d, potrebbe essere necessaria una ricerca spaziale 3d. Tuttavia, iniziare innanzitutto comprendendo la ricerca spaziale 2D.

Quindi considera se avrai mai più caselle una sopra l'altra, altrimenti una ricerca spaziale 2d che copre solo x, y potrebbe essere sufficiente per ridurre il ciclo.

Octree / quadtree sono un'opzione, ma ci sono molte altre opzioni per il partizionamento dello spazio ,

Ma potresti essere in grado di utilizzare solo una matrice di elenchi bidimensionali ( indice spaziale griglia ), in cui tutte le caselle che coprono il punto (a, b) sono nella matrice [a, b] .list. Ma la cosa più leccata porterebbe a un array troppo grande, quindi che dire di array [mod (a, 100), mob (b, 100)]. List? Tutto dipende da come sono i tuoi dati .

(Ho visto la soluzione grid funzionare molto bene nella vita reale.)

Oppure fai quello che dice Wilbert con un ottetto con un nodo foglia con dimensioni delle dimensioni della tua scatola, ma in seguito dovrai probabilmente trovare la scatola su cui punta il mouse dell'utente, ecc., Ancora una volta un caso di ricerca spaziale.

( Devi decidere se stai solo cercando di far funzionare questo software o se stai cercando di capire come essere un programmatore migliore e quindi sei più interessato all'apprendimento di una soluzione rapida. )

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.