Meu algoritmo que extrai a maior caixa que pode ser feita de caixas menores é muito lento


9

Imagine um mundo baseado em cubos (como Minecraft, Trove ou Cube World), onde tudo é composto de cubos de tamanho idêntico e todos os cubos são do mesmo tipo .

O objetivo é representar o mundo com o menor número de caixas retangulares (mesclando cubos, mas mantendo a forma convexa (também conhecida como forma de caixa retangular)). Meu algoritmo consegue isso, mas seu desempenho é muito lento para o objetivo a que se destina. Existem algoritmos mais rápidos?

O código pseudo-C # para o meu algoritmo é basicamente:

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

Essencialmente, para cada bloco do mundo, ele primeiro descobre quantos blocos contíguos existem em cada uma das três dimensões positivas (+ X, + Y, + Z). E então meio que preenche esse volume e verifica qual é o maior preenchimento que não está faltando nenhum bloco.


Atualização: Como parecia sugerir que isso era para o mecanismo de renderização de um jogo, só quero esclarecer que isso não é para o mecanismo de renderização de um jogo; é para um conversor de arquivos; sem GUI.


2
Talvez o mais adequado para codereview.stackexchange.com
Rotem

4
@ Rotem Talvez, mas na verdade estou procurando algoritmos alternativos ao invés de uma revisão do meu código. Forneci meu código apenas como uma força do hábito.
Sr. Smith

Claro, faz sentido.
Rotem

Perguntas algoritmo são mais adequados para locais SE como ciência da computação ...
Bakuriu

Também depende de quantas vezes você chama o método. Se você chamar todos os quadros ou apenas quando um bloco for alterado. Esses jogos geralmente têm blocos (retângulo de tamanho específico, por exemplo: blocos de 64x64x32), armazenam valores em cache o máximo possível e calculam apenas por bloco. E apenas calcule esses valores nos blocos visíveis.
21715 as

Respostas:


6

Você pode fazer uso do fato de que quando

 BoxIsFilledWithCubes(c,x,y,z)

retorna true, não é necessário verificar BoxIsFilledWithCubes(c,x+1,y,z)todos os cubos no intervalo de coordenadas "(c, x, y, z)" novamente. Você só precisa verificar esses cubos com a nova coordenada x c.x + (x+1). (O mesmo vale para y+1, ou z+1). De maneira mais geral, dividindo uma caixa em duas caixas menores (para as quais você já deve saber se elas estão cheias de cubos ou se não estão cheias), é possível aplicar aqui uma técnica de dividir e conquistar, que se torna mais rápida que a sua abordagem original quando você armazena em cache os resultados intermediários.

Para fazer isso, você começa a implementar BoxIsFilledWithCubes(c,x,y,z)recursivamente, por exemplo:

 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 use a memorização (como discutido aqui ) para evitar chamadas repetidas para BoxIsFilledWithCubesos mesmos parâmetros. Observe que você terá que limpar o cache de memorização ao aplicar uma alteração ao seu worldcontêiner, como por world.RemoveRange. No entanto, acho que isso tornará seu programa mais rápido.


5

Crie um octree com um nó folha aabb do tamanho da sua caixa. Ao percorrer o octree, você pode mesclar nós de maneira barata. Nós completamente preenchidos são triviais para mesclar (nova caixa = aabb pai), enquanto que para nós parcialmente preenchidos, você pode usar uma variante do seu algoritmo atual para verificar a capacidade de mesclagem.


O fato de dois nós serem completamente preenchidos não implica que eles devam ser mesclados; não é um passo para encontrar a caixa maior; se você mesclá-los, provavelmente precisará dividi-los novamente posteriormente, quando a caixa maior for encontrada. Não vejo como uma octree ajuda nesse cenário.
Sr. Smith

Nós completos podem ser mesclados no sentido de que todos os 8 filhos podem se tornar parte de uma única caixa maior. Essa caixa maior não precisará ser dividida posteriormente, mas poderá ser mesclada. A hierarquia permite mesclar rapidamente muitas caixas, os nós completamente preenchidos permitem que você suba de nível sem mais testes.
21715 Wilbert

1

Você parece ter pelo menos O (n ^ 2) (veja a notação O grande ) ao percorrer todas as caixas do mundo em "Begin ()"; depois, para cada caixa, percorrer todas as caixas do mundo em ExtractLargest ( ) Portanto, um mundo com 10 caixas não relacionadas levará 4 vezes mais tempo do que um mundo com 5 caixas não relacionadas.

Portanto, você precisa limitar o número de caixas que ExtractLargest () deve examinar, para fazer isso, você precisa usar algum tipo de pesquisa espacial , enquanto trabalha em 3d, talvez seja necessário uma pesquisa espacial em 3d. No entanto, primeiro comece entendendo a pesquisa espacial em 2D.

Então considere se você terá muitas caixas por cima uma da outra, se não, uma pesquisa espacial em 2D que cubra apenas x, y pode ser suficiente para reduzir o loop.

Octree / quadtree são uma opção, mas existem muitas outras opções para particionamento de espaço ,

Mas você pode usar apenas uma matriz bidimensional de listas ( índice espacial de grade ), onde todas as caixas que cobrem o ponto (a, b) estão na matriz [a, b] .list. Mas o mais lamentavelmente isso levaria a uma matriz muito grande, e quanto à matriz [mod (a, 100), mob (b, 100)]. Tudo isso depende de como são os seus dados .

(Vi a solução da rede funcionar muito bem na vida real.)

Ou faça o que Wilbert diz com uma octree com um nó de folha aabb do tamanho da sua caixa, mas mais tarde você provavelmente encontrará a caixa que o mouse do usuário está apontando etc, mais uma vez, um caso de pesquisa espacial.

( Você deve decidir se está apenas tentando fazer com que este software funcione ou se está tentando entender como ser um programador melhor e, portanto, está mais interessado no aprendizado do que em uma solução rápida. )

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.