Mi algoritmo que extrae la caja más grande que se puede hacer de cajas más pequeñas, es demasiado lento


9

Imagine un mundo basado en cubos (como Minecraft, Trove o Cube World) donde todo está formado por cubos de tamaño idéntico y todos los cubos son del mismo tipo .

El objetivo es representar al mundo con el menor número de cajas rectangulares (fusionando cubos pero conservando la forma convexa (también conocida como forma de caja rectangular)). Mi algoritmo tiene éxito en esto, pero su rendimiento es demasiado lento para el propósito previsto. ¿Hay algoritmos más rápidos?

El código pseudo-C # para mi algoritmo es básicamente:

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;
}

Esencialmente, para cada bloque en el mundo, primero encuentra cuántos bloques contiguos hay en cada una de las tres dimensiones positivas (+ X, + Y, + Z). Y luego inunda un poco ese volumen y comprueba cuál es el relleno más grande al que no le faltan bloques.


Actualización: como parecía haber dado a entender que esto era para el motor de renderizado de un juego, solo quiero aclarar que no es para el motor de renderizado de un juego; es para un convertidor de archivos; sin GUI


2
Quizás más adecuado para codereview.stackexchange.com
Rotem

44
@Rotem Quizás, pero en realidad estoy buscando algoritmos alternativos en lugar de una revisión de mi código. He proporcionado mi código solo como una costumbre.
Sr. Smith

Claro, tiene sentido.
Rotem

Las preguntas sobre algoritmos son más adecuadas para sitios de SE como informática ...
Bakuriu

También depende de la frecuencia con la que llame al método. Si lo llama cada cuadro o solo cuando cambia un bloque. Estos juegos generalmente tienen fragmentos (rectángulo de tamaño específico, por ejemplo, bloques de 64x64x32), valores de caché tanto como sea posible y calculan solo por fragmento. Y solo calcule estos valores en los bloques visibles.
the_lotus

Respuestas:


6

Puedes aprovechar el hecho de que cuando

 BoxIsFilledWithCubes(c,x,y,z)

devuelve verdadero, entonces no es necesario verificar BoxIsFilledWithCubes(c,x+1,y,z)todos esos cubos en el rango de coordenadas "(c, x, y, z)" nuevamente. Solo necesita verificar esos cubos con la nueva coordenada x c.x + (x+1). (Lo mismo es cierto para y+1, o z+1). En términos más generales, al dividir una caja en dos cajas más pequeñas (para las cuales quizás ya sepa si ambas están llenas de cubos, o no ambas), puede aplicar aquí una técnica de dividir y conquistar, que se vuelve más rápida que su enfoque original cuando almacena en caché los resultados intermedios.

Para lograr esto, comienza a implementar de forma BoxIsFilledWithCubes(c,x,y,z)recursiva, por ejemplo:

 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

 }

y luego use la memorización (como se discute aquí ) para evitar cualquier llamada repetida BoxIsFilledWithCubescon los mismos parámetros. Tenga en cuenta que tendrá que borrar el caché de la memoria cuando aplique un cambio a su worldcontenedor, como por world.RemoveRange. Sin embargo, supongo que esto hará que su programa sea más rápido.


5

Construya un octree con un nodo de hoja de tamaño aabb del tamaño de su caja. Mientras recorres el octree puedes fusionar nodos de forma económica. Los nodos completamente llenos son triviales para fusionarse (nuevo cuadro = aabb padre), mientras que para nodos parcialmente llenos, puede usar una variante de su algoritmo actual para verificar la capacidad de fusión.


Que dos nodos estén completamente llenos no implica que deberían fusionarse; no es un paso para encontrar la caja más grande; si los combina, es probable que tenga que dividirlos nuevamente en un momento posterior una vez que se encuentre el cuadro más grande. No veo cómo un octree ayuda en este escenario.
Sr. Smith

Los nodos completos se pueden fusionar en el sentido de que los 8 niños pueden formar parte de una sola caja más grande. Este cuadro más grande no tendrá que dividirse más adelante, pero podría fusionarse. La jerarquía le permite fusionar rápidamente muchas cajas, los nodos completamente llenos le permiten subir de nivel sin más pruebas.
Wilbert

1

Parece que tiene al menos O (n ^ 2) (vea la notación O grande ) mientras recorre todos los cuadros del mundo en "Begin ()", luego, para cada cuadro, recorre todos los cuadros del mundo en ExtractLargest ( ) Entonces, un mundo con 10 cajas no relacionadas tomará 4 veces más tiempo que un mundo con 5 cajas no relacionadas.

Por lo tanto, debe limitar el número de cuadros que ExtractLargest () tiene que mirar, para hacerlo necesita usar algún tipo de búsqueda espacial , ya que está trabajando en 3d, es posible que necesite una búsqueda espacial 3d. Sin embargo, primero comience por comprender la búsqueda espacial 2D.

Luego, considere si alguna vez tendrá muchos cuadros uno encima del otro, si no, entonces una búsqueda espacial en 2D que solo cubre x, y puede ser suficiente para reducir su bucle.

Octree / quadtree son una opción, pero hay muchas otras opciones para particionar el espacio ,

Pero es posible que pueda usar una matriz bidimensional de listas ( índice espacial de cuadrícula ), donde todos los cuadros que cubren el punto (a, b) están en la matriz [a, b] .list. Pero lo más probable es que eso conduzca a una matriz demasiado grande, entonces, ¿qué pasa con la lista array [mod (a, 100), mob (b, 100)]? Todo esto depende de cómo son sus datos .

(He visto que la solución de red funciona muy bien en la vida real).

O haga lo que Wilbert dice con un octree con un nodo de hoja de un tamaño del tamaño de su caja, pero más tarde es probable que tenga que encontrar la caja a la que apunta el mouse del usuario, etc., una vez más, un caso de búsqueda espacial.

( Debe decidir si solo está tratando de hacer que este software funcione, o si está tratando de comprender cómo ser un mejor programador y, por lo tanto, está más interesado en el aprendizaje que en una solución rápida ) .

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.