Мой алгоритм, который извлекает самый большой ящик, который может быть сделан из меньших ящиков, слишком медленный

9

Представьте себе мир, основанный на кубах (например, Minecraft, Trove или Cube World), где все состоит из кубов одинакового размера, и все кубы одного типа .

Цель состоит в том, чтобы представить мир с наименьшим количеством прямоугольных прямоугольников (объединяя кубы, но сохраняя выпуклую форму (или прямоугольную форму)). Мой алгоритм преуспевает в этом, но его производительность слишком медленная для его предназначения. Есть ли более быстрые алгоритмы?

Псевдо-C # -код для моего алгоритма в основном:

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

По сути, для каждого блока в мире, он сначала находит, сколько константных блоков есть в каждом из трех положительных измерений (+ X, + Y, + Z). И затем он как бы заполняет этот объем и проверяет, какая из них самая большая, не пропустив ни одного блока.


Обновление: так как я, казалось, подразумевал, что это было для движка рендеринга игры, я просто хочу уточнить, это не для движка рендеринга игры; это для файлового конвертера; нет графического интерфейса

Г-н Смит
источник
2
Возможно, больше подходит для codereview.stackexchange.com
Rotem
4
@Rotem Возможно, но я на самом деле ищу альтернативные алгоритмы, а не обзор моего кода. Я предоставил свой код просто как привычку.
Мистер Смит
Конечно, имеет смысл.
Rotem
Вопросы алгоритма больше подходят для сайтов SE, таких как информатика ...
Бакуриу
Это также зависит от того, как часто вы вызываете метод. Если вы называете это каждый кадр или только когда блок меняется. Эти игры обычно имеют куски (прямоугольник определенного размера, например: блоки 64x64x32), максимально кэшируют значения и рассчитывают только по блокам. И только рассчитать эти значения по видимым блокам.
the_lotus

Ответы:

6

Вы можете использовать тот факт, что когда

 BoxIsFilledWithCubes(c,x,y,z)

возвращает true, тогда нет необходимости проверять BoxIsFilledWithCubes(c,x+1,y,z)все эти кубы в координатном диапазоне "(c, x, y, z)" снова. Вам нужно только проверить эти кубики с новой x-координатой c.x + (x+1). (То же самое верно для y+1или z+1). В более общем смысле, разделив прямоугольник на два меньших прямоугольника (для которых вы, возможно, уже знаете, заполнены ли они кубиками или не заполнены оба), вы можете применить здесь метод «разделяй и властвуй», который становится быстрее, чем ваш. оригинальный подход при кешировании промежуточных результатов.

Для этого вы начинаете BoxIsFilledWithCubes(c,x,y,z)рекурсивную реализацию , например:

 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

 }

и затем используйте памятку (как это обсуждается здесь ), чтобы избежать повторных вызовов BoxIsFilledWithCubesс теми же параметрами. Обратите внимание, что вам придется очистить кэш worldнапоминания, когда вы примените изменение к своему контейнеру, например, с помощью world.RemoveRange. Тем не менее, я думаю, это сделает вашу программу быстрее.

Док Браун
источник
5

Создайте октодерево с листовым узлом размером с ваш ящик. Проходя через октри, вы можете дешево объединить узлы. Полностью заполненные узлы тривиальны для слияния (новое поле = родительский aabb), в то время как для частично заполненных узлов вы можете использовать вариант текущего алгоритма для проверки возможности слияния.

Уилберт
источник
То, что два узла полностью заполнены, не означает, что они должны быть объединены; это не шаг к поиску самой большой коробки; если вы объедините их, вам, вероятно, придется разделить их позже, как только будет найден самый большой блок. Я не вижу, как октри помогает в этом сценарии.
мистер Смит
Полные узлы могут быть объединены в том смысле, что все 8 дочерних элементов могут стать частью одного большего блока. Эту большую коробку позже не нужно будет разбивать, но она может быть объединена. Иерархия позволяет быстро объединять множество ящиков, полностью заполненные узлы позволяют подняться на уровень без дальнейшего тестирования.
Уилберт
1

Похоже, что вы, по крайней мере, O (n ^ 2) (см. Большие обозначения O ), когда вы перебираете все блоки в мире в «Begin ()», а затем для каждого блока вы перебираете все блоки в мире в ExtractLargest ( ). Таким образом, мир с 10 несвязанными коробками займет в 4 раза больше времени, чем мир с 5 несвязанными коробками.

Поэтому вам нужно ограничить количество блоков, на которые должна смотреть ExtractLargest (), для этого вам нужно использовать некоторый тип пространственного поиска , так как при работе в 3d вам может понадобиться трехмерный пространственный поиск. Однако сначала начните с понимания двумерного пространственного поиска.

Затем подумайте, будет ли у вас когда-нибудь много блоков поверх друг друга, если нет, то двухмерного пространственного поиска, который охватывает только x, y, может быть достаточно для сокращения вашего цикла.

Octree / quadtree - один из вариантов, но есть много других вариантов разделения пространства ,

Но вы можете просто использовать двумерный массив списков ( пространственный индекс сетки ), где все поля, которые охватывают точку (a, b), находятся в массиве [a, b] .list. Но, скорее всего, это привело бы к слишком большому массиву, так как насчет массива [mod (a, 100), mob (b, 100)]. List? Все зависит от того, на что похожи ваши данные .

(Я видел, как сеточное решение очень хорошо работает в реальной жизни.)

Или сделайте то, что говорит Уилберт, с октодеревом с размером листа aabb, равным размеру вашего блока, но позже вам, скорее всего, придется найти блок, на который указывает мышь пользователя, и т. Д., Снова случай пространственного поиска.

( Вы должны решить, пытаетесь ли вы заставить это программное обеспечение работать или вы пытаетесь понять, как стать лучшим программистом и, следовательно, больше заинтересованы в обучении, чем в быстром решении. )

Ян
источник