Felipe
Felipe

Reputation: 697

Tallest tower with stacked boxes in the given order

Given N boxes. How can i find the tallest tower made with them in the given order ? (Given order means that the first box must be at the base of the tower and so on). All boxes must be used to make a valid tower.

It is possible to rotate the box on any axis in a way that any of its 6 faces gets parallel to the ground, however the perimeter of such face must be completely restrained inside the perimeter of the superior face of the box below it. In the case of the first box it is possible to choose any face, because the ground is big enough.

To solve this problem i've tried the following:
- Firstly the code generates the rotations for each rectangle (just a permutation of the dimensions)
- secondly constructing a dynamic programming solution for each box and each possible rotation
- finally search for the highest tower made (in the dp table)

But my algorithm is taking wrong answer in unknown test cases. What is wrong with it ? Dynamic programming is the best approach to solve this problem ?

Here is my code:

#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstdlib>
#include <cstring>

struct rectangle{
    int coords[3];
    rectangle(){ coords[0] = coords[1] = coords[2] = 0; }
    rectangle(int a, int b, int c){coords[0] = a; coords[1] = b; coords[2] = c; }
};

bool canStack(rectangle &current_rectangle, rectangle &last_rectangle){
    for (int i = 0; i < 2; ++i)
        if(current_rectangle.coords[i] > last_rectangle.coords[i])
            return false;
    return true;
}

//six is the number of rotations for each rectangle
int dp(std::vector< std::vector<rectangle>  > &v){
    int memoization[6][v.size()];
    memset(memoization, -1, sizeof(memoization));

    //all rotations of the first rectangle can be used
    for (int i = 0; i < 6; ++i) {
        memoization[i][0] = v[0][i].coords[2];
    }

    //for each rectangle
    for (int i = 1; i < v.size(); ++i) {
        //for each possible permutation of the current rectangle
        for (int j = 0; j < 6; ++j) {
            //for each permutation of the previous rectangle
            for (int k = 0; k < 6; ++k) {
                rectangle &prev = v[i - 1][k];
                rectangle &curr = v[i][j];
                //is possible to put the current rectangle with the previous rectangle ?
                if( canStack(curr, prev) ) {
                    memoization[j][i] = std::max(memoization[j][i], curr.coords[2] + memoization[k][i-1]);
                }
            }
        }
    }

    //what is the best solution ?
    int ret = -1;
    for (int i = 0; i < 6; ++i) {
        ret = std::max(memoization[i][v.size()-1], ret);
    }
    return ret;
}

int main ( void ) {
    int n;
    scanf("%d", &n);

    std::vector< std::vector<rectangle>  > v(n);

    for (int i = 0; i < n; ++i) {
        rectangle r;
        scanf("%d %d %d", &r.coords[0], &r.coords[1], &r.coords[2]);

        //generate all rotations with the given rectangle (all combinations of the coordinates)
        for (int j = 0; j < 3; ++j)
            for (int k = 0; k < 3; ++k)
                if(j != k) //micro optimization disease
                    for (int l = 0; l < 3; ++l)
                        if(l != j && l != k)
                            v[i].push_back( rectangle(r.coords[j], r.coords[k], r.coords[l]) );

    }

    printf("%d\n", dp(v));
}

Input Description

A test case starts with an integer N, representing the number of boxes (1 ≤ N ≤ 10^5). Following there will be N rows, each containing three integers, A, B and C, representing the dimensions of the boxes (1 ≤ A, B, C ≤ 10^4).

Output Description

Print one row containing one integer, representing the maximum height of the stack if it’s possible to pile all the N boxes, or -1 otherwise.

Sample Input

2
5 2 2
1 3 4

Sample Output

6

Sample image for the given input and output.

Sample image

Upvotes: 0

Views: 1045

Answers (2)

Weak to Enuma Elish
Weak to Enuma Elish

Reputation: 4637

Usually you're given the test case that made you fail. Otherwise, finding the problem is a lot harder.

You can always approach it from a different angle! I'm going to leave out the boring parts that are easily replicated.

struct Box { unsigned int dim[3]; };

Box will store the dimensions of each... box. When it comes time to read the dimensions, it needs to be sorted so that dim[0] >= dim[1] >= dim[2].

The idea is to loop and read the next box each iteration. It then compares the second largest dimension of the new box with the second largest dimension of the last box, and same with the third largest. If in either case the newer box is larger, it adjusts the older box to compare the first largest and third largest dimension. If that fails too, then the first and second largest. This way, it always prefers using a larger dimension as the vertical one.

If it had to rotate a box, it goes to the next box down and checks that the rotation doesn't need to be adjusted there too. It continues until there are no more boxes or it didn't need to rotate the next box. If at any time, all three rotations for a box failed to make it large enough, it stops because there is no solution.

Once all the boxes are in place, it just sums up each one's vertical dimension.

int main()
{
    unsigned int size; //num boxes
    std::cin >> size;

    std::vector<Box> boxes(size); //all boxes
    std::vector<unsigned char> pos(size, 0); //index of vertical dimension

    //gets the index of dimension that isn't vertical
    //largest indicates if it should pick the larger or smaller one
    auto get = [](unsigned char x, bool largest) { if (largest) return x == 0 ? 1 : 0; return x == 2 ? 1 : 2; };

    //check will compare the dimensions of two boxes and return true if the smaller one is under the larger one
    auto check = [&boxes, &pos, &get](unsigned int x, bool largest) { return boxes[x - 1].dim[get(pos[x - 1], largest)] < boxes[x].dim[get(pos[x], largest)]; };

    unsigned int x = 0, y; //indexing variables
    unsigned char change; //detects box rotation change
    bool fail = false; //if it cannot be solved
    for (x = 0; x < size && !fail; ++x)
    {
        //read in the next three dimensions
        //make sure dim[0] >= dim[1] >= dim[2]
        //simple enough to write
        //mine was too ugly and I didn't want to be embarrassed

        y = x;
        while (y && !fail) //when y == 0, no more boxes to check
        {
            change = pos[y - 1];
            while (check(y, true) || check(y, false)) //while invalid rotation
            {
                if (++pos[y - 1] == 3) //rotate, when pos == 3, no solution
                {
                    fail = true;
                    break;
                }
            }
            if (change != pos[y - 1]) //if rotated box
                --y;
            else
                break;
        }
    }
    if (fail)
    {
        std::cout << -1;
    }
    else
    {
        unsigned long long max = 0;
        for (x = 0; x < size; ++x)
            max += boxes[x].dim[pos[x]];
        std::cout << max;
    }
    return 0;
}

It works for the test cases I've written, but given that I don't know what caused yours to fail, I can't tell you what mine does differently (assuming it also doesn't fail your test conditions).

Upvotes: 1

Matt Jordan
Matt Jordan

Reputation: 2181

If you are allowed, this problem might benefit from a tree data structure.

First, define the three possible cases of block:

1) Cube - there is only one possible option for orientation, since every orientation results in the same height (applied toward total height) and the same footprint (applied to the restriction that the footprint of each block is completely contained by the block below it).

2) Square Rectangle - there are three possible orientations for this rectangle with two equal dimensions (for examples, a 4x4x1 or a 4x4x7 would both fit this).

3) All Different Dimensions - there are six possible orientations for this shape, where each side is different from the rest.

For the first box, choose how many orientations its shape allows, and create corresponding nodes at the first level (a root node with zero height will allow using simple binary trees, rather than requiring a more complicated type of tree that allows multiple elements within each node). Then, for each orientation, choose how many orientations the next box allows but only create nodes for those that are valid for the given orientation of the current box. If no orientations are possible given the orientation of the current box, remove that entire unique branch of orientations (the first parent node with multiple valid orientations will have one orientation removed by this pruning, but that parent node and all of its ancestors will be preserved otherwise).

By doing this, you can check for sets of boxes that have no solution by checking whether there are any elements below the root node, since an empty tree indicates that all possible orientations have been pruned away by invalid combinations.

If the tree is not empty, then just walk the tree to find the highest sum of heights within each branch of the tree, recursively up the tree to the root - the sum value is your maximum height, such as the following pseudocode:

std::size_t maximum_height() const{
    if(leftnode == nullptr || rightnode == nullptr)
        return this_node_box_height;
    else{
        auto leftheight = leftnode->maximum_height() + this_node_box_height;
        auto rightheight = rightnode->maximum_height() + this_node_box_height;
        if(leftheight >= rightheight)
            return leftheight;
        else
            return rightheight;
    }
}

The benefits of using a tree data structure are

1) You will greatly reduce the number of possible combinations you have to store and check, because in a tree, the invalid orientations will be eliminated at the earliest possible point - for example, using your 2x2x5 first box, with three possible orientations (as a Square Rectangle), only two orientations are possible because there is no possible way to orient it on its 2x2 end and still fit the 4x3x1 block on it. If on average only two orientations are possible for each block, you will need a much smaller number of nodes than if you compute every possible orientation and then filter them as a second step.

2) Detecting sets of blocks where there is no solution is much easier, because the data structure will only contain valid combinations.

3) Working with the finished tree will be much easier - for example, to find the sequence of orientations of the highest, rather than just the actual height, you could pass an empty std::vector to a modified highest() implementation, and let it append the actual orientation of each highest node as it walks the tree, in addition to returning the height.

Upvotes: 1

Related Questions