Bin Packing - part 7

31-DEC-2011

Solution 7: Brute Force discovery

Up until now, we have been using large amounts of packages. And as described before, the effort needed to examine all possible combinations grows exponentially with the number of packages.

But what if you have only a small amount of packages, and if you happen to have sufficient computer power to examine all possible combinations? How would you do that? This solution will look into that.

Optimizations rules

The entire search space is massively big. Let's assume you have 20 packages and expect to fit those in 8 bins. In that case, there are 8 potential positions for each of the 20 packages, which would result in 8^20 combinations. If each combination requires 24 bytes of storage, then recording all possible combinations would require 25,165,824 Terabyte. Nobody has that much storage space. But even if someone did: processing data on this scale will take too long. So we need to reduce the search space where we can, without excluding potentially better solutions.

It goes without saying, that it is never an option to place a package in a bin when there is insufficient space left.

The other rules are formulated below. The examples assume a bin size of 100:

Rule 1: Two bins are identical if they have the same number and size of packages. For example, the bin {60, 10, 30} is the same as {30, 10, 60}. By convention, when filling / listing the bins the packages will be listed in descending size order (biggest first), for example {60, 30, 10}.

Rule 2: Two solutions are identical if they contain the same bins. For example, the solution with the bins {60, 30, 5} and {61, 38} is the same as the solution with the bins {61, 38} and {60, 30, 5}. By convention, when filling / listing the solution, the bins will be listed in descending order of biggest package, for example {61, 38}, {60, 30, 5}

Rule 3: For the purpose of finding the best solution, two solutions can be considered the same if they contain the same packages and have the same unused space per bin. For example, the solution {50, 40, 10}, {80, 5, 5} can be considered "the same" as solution {50, 40, 5, 5}, {80, 10}.

Rule 4: A solution with more wasted space than the desirable number of bins allow for can never reach its goal. For example, if a 7-bins solution will have 13 unused space, then a solution that wastes more than 13 (even before all or even most of the packages have been assinged) can never fit in these 7 bins.

Rule 5: When filling the bins, a bin can be considered filled up when the remaining space is smaller than the smallest remaining package. For example, if a bin has unused space of 2, and the smallest remaining package is 3, then the bin can be considered filled up.

Rule 6: If placing the package completely fills up the bin (no waste), then that is a perfect placement. For that package there is no better placement possible. For example, if you have a package of size 30, and a bin {70}, then this is a perfect fit.

Rule 7: For the purpose of finding the best solutions, two solutions can be considered equivalent if they contain the same distribution of unused space of bins that are not yet filled up (see rule 5). For example, assume that the smallest unplaced packages is 11, then the set {80, 18}, {75, 20}, {50} is equivalent to {80, 20}, {75, 18}, {50}.

Rule 8: If only one more package will fit the bin, then the largest available package that will still fit is the best choice. For example, if the unused space in the bin is 19, and the smallest remaining package is 10, then the biggest remaining package that will fit the bin (size 19 through 10) is the best choice for that bin. So if there is still a package remaining of size 19, then fitting a package of size 10 is not useful.

Rule 9: For the purpose of finding the best solution, if two bins have the same unused space, then it is irrelevant in which of those bins the package will be placed. By convention, the first possible bin will be used.

Rule 10: If a package would fill up the bin, but leave more wasted space than the maximum allowable for the solution, then that package is not an option for that bin. For example, the maximum allowable waste is 7, the smallest remaining package is 15, the package to place is 20, and there is a bin with 30 unused space. This bin is not suitable for this package, because it would fill up the bin with a waste of 10, which would disqualify the solution.

Example

Let's do one example by hand. Consider the following packages that are to be placed in bins of size 100. Know that the best solution found so far is 8 bins.

package_no  size
----------- ------
1           36
2           39
3           43
4           53
5           32
6            7
7           23
8           41
9           55
10          29
11          14
12          44
13          44
14          22
15          32
16          52
17          38
18          16
19          31
20          36

The total size of the bins is 687. The perfect solution would be one with only 7 bins, which would leave only 13 unused space. Since the current best solution is 8, it is only worthwile to see if there is a solution for 7 bins.

So let's take a few steps manually, and see what the SQL script (see further down) is supposed to do.

Since we are placing 20 packages, with the set based approach this takes 20 iterations or steps

Step 1. The first package to place has a size of 55. Currently, all 7 bins have a space_left of 100. In other words, the bins look like this {}, {}, {}, {}, {}, {}, {}.

There is only one possible place to put package 9 (see rule 2). So we'll place it in bin 1. The bins are now {55}, {}, {}, {}, {}, {}, {}.

Step 2. The next package to place has a size of 53. Since it doesn't fit in bin 1, there is just 1 other position to place it (see rule 9), in bin 1. The bins are now {55}, {53}, {}, {}, {}, {}, {}.

Step 3. The next package to place has a size of 52. It doesn't fit in bin 1 or 2. Rule 9 says there is only one other place, in bin 3. The bins are now {55}, {53}, {52}, {}, {}, {}, {}.

Step 4. The next package to place has a size of 44. It fits in bin 1, 2, 3 and 4. However, putting it in bin 1, 2 or 3 would fill up that bin (see rule 5). Rule 8 says that since bin 1 is the best option, and therefore bin 2 and 3 are no option. But that still leaves 1 and 4.

So now we have two potential solutions:
Potential Solution 1 (PS1): {55, 44}, {53}, {52}, {}, {}, {}, {}. Waste 1.
Potential Solution 2 (PS2): {55!}, {53!}, {52!}, {44}, {}, {}, {}.

Step 5. The next package to place again has a size of 44.
In PS1, there are two possible places for it: bin 2 and 4 (PS3).
In PS2, it fits in 4 and 5 (PS4). Bin 1 is not an option because of rule 8.
PS1: {55, 44}, {53, 44}, {52}, {}, {}, {}, {}. Waste 4.
PS2: {55!}, {53!}, {52!}, {44, 44}, {}, {}, {}.
PS3: {55, 44}, {53!}, {52!}, {44}, {}, {}, {}. Waste 1.
PS4: {55!}, {53!}, {52!}, {44}, {44}, {}, {}.

Step 6. The next package to place has a size of 43.
In PS1, it fits in bin 3 or 4 (PS5)
In PS2, it fits in bin 5
In PS3, it fits in bin 4 or 5 (PS6)
In PS4, it fits in bin 4 or 6 (PS7)
PS1: {55, 44}, {53, 44}, {52,43}, {}, {}, {}, {}. Waste 9.
PS2: {55!}, {53!}, {52!}, {44, 44}, {43}, {}, {}.
PS3: {55, 44}, {53!}, {52!}, {44, 43}, {}, {}, {}. Waste 1.
PS4: {55!}, {53!}, {52!}, {44, 43}, {44}, {}, {}.
PS5: {55, 44}, {53, 44}, {52!}, {43}, {}, {}, {}. Waste 4.
PS6: {55, 44}, {53!}, {52!}, {44}, {43}, {}, {}. Waste 1.
PS7: {55!}, {53!}, {52!}, {44}, {44}, {43}, {}.

Step 7. The next package to place has a size of 41. Note that the smallest available package is 7.
As you can see, the number of combinations can grow fast, in this case from 7 to 20.
In PS1, it fits in bin 4
In PS2, it fits in bin 3, 5 (PS11) or 6 (PS15)
In PS3, it fits in bin 3 or 5 (PS16)
In PS4, it fits in bin 3, 5 (PS8) or 6 (PS17)
In PS5, it fits in bin 3, 4 (PS12) or 5 (PS18)
In PS6, it fits in bin 3, 4 (PS9), 5 (PS13) or 6 (PS19)
In PS7, it fits in bin 3, 4 (PS10), 6 (PS14) or 7 (PS20)
PS1: {55, 44}, {53, 44}, {52,43}, {41}, {}, {}, {}. Waste 9.
PS2: {55!}, {53!}, {52,41}, {44, 44}, {43}, {}, {}.
PS3: {55, 44}, {53!}, {52,41}, {44, 43}, {}, {}, {}. Waste 1.
PS4: {55!}, {53!}, {52,41}, {44, 43}, {44}, {}, {}.
PS5: {55, 44}, {53, 44}, {52,41}, {43}, {}, {}, {}. Waste 4.
PS6: {55, 44}, {53!}, {52,41}, {44}, {43}, {}, {}. Waste 1.
PS7: {55!}, {53!}, {52,41}, {44}, {44}, {43}, {}.
PS8: {55!}, {53!}, {52!}, {44, 43}, {44, 41}, {}, {}.
PS9: {55, 44}, {53!}, {52!}, {44, 41}, {43}, {}, {}. Waste 1.
PS10: {55!}, {53!}, {52!}, {44, 41}, {44}, {43}, {}.
PS11: {55!}, {53!}, {52!}, {44, 44}, {43, 41}, {}, {}.
PS12: {55, 44}, {53, 44}, {52!}, {43, 41}, {}, {}, {}. Waste 4.
PS13: {55, 44}, {53!}, {52!}, {44}, {43, 41}, {}, {}. Waste 1.
PS14: {55!}, {53!}, {52!}, {44}, {44}, {43, 41}, {}.
PS15: {55!}, {53!}, {52!}, {44, 44}, {43}, {41}, {}.
PS16: {55, 44}, {53!}, {52!}, {44, 43}, {41}, {}, {}. Waste 1.
PS17: {55!}, {53!}, {52!}, {44, 43}, {44}, {41}, {}.
PS18: {55, 44}, {53, 44}, {52!}, {43}, {41}, {}, {}. Waste 4.
PS19: {55, 44}, {53!}, {52!}, {44}, {43}, {41}, {}. Waste 1.
PS20: {55!}, {53!}, {52!}, {44}, {44}, {43}, {41}.

Step 8. The next package to place has a size of 39. Note that the smallest available package is 7.
The number of potential solutions more than triples.
In PS1, it fits in bin 4 or 5 (PS56)
In PS2, it fits in bin 2, 5 (PS41) or 6 (PS57)
In PS3, it fits in bin 2 or 5 (PS58)
In PS4, it fits in bin 2, 5 (PS32) or 6 (PS59)
In PS5, it fits in bin 4 or 5 (PS60)
In PS6, it fits in bin 2, 4 (PS33), 5 (PS42) or 6 (PS61)
In PS7, it fits in bin 2, 4 (PS34), 6 (PS43) or 7 (PS62)
In PS8, it fits in bin 2, 3 (PS21) or 6 (PS63)
In PS9, it fits in bin 2, 3 (PS22), 5 (PS44) or 6 (PS64)
In PS10, it fits in bin 2, 3 (PS23), 5 (PS35), 6 (PS45) or 7 (PS65)
In PS11, it fits in bin 2, 3 (PS24) or 6 (PS66)
In PS12, it fits in bin 3 or 5 (PS67)
In PS13, it fits in bin 2, 3 (PS25), 4 (PS36) or 6 (PS68)
In PS14, it fits in bin 2, 3 (PS26), 4 (PS37) or 7 (PS69)
In PS15, it fits in bin 2, 3 (PS27), 5 (PS46), 6 (PS50) or 7 (PS70)
In PS16, it fits in bin 2, 3 (PS28), 5 (PS51) or 6 (PS71)
In PS17, it fits in bin 2, 3 (PS29), 5 (PS38), 6 (PS52) or 7 (PS72)
In PS18, it fits in bin 3, 4 (PS47), 5 (PS53) or 6 (PS73)
In PS19, it fits in bin 2, 3 (PS30), 4 (PS39), 5 (PS48), 6 (PS54) or 7 (PS74)
In PS20, it fits in bin 2, 3 (PS31), 4 (PS40), 6 (PS49) or 7 (PS55)
PS1: {55, 44}, {53, 44}, {52,43}, {41, 39}, {}, {}, {}. Waste 9.
PS2: {55!}, {53, 39}, {52,41}, {44, 44}, {43}, {}, {}.
PS3: {55, 44}, {53, 39}, {52,41}, {44, 43}, {}, {}, {}. Waste 1.
PS4: {55!}, {53, 39}, {52,41}, {44, 43}, {44}, {}, {}.
PS5: {55, 44}, {53, 44}, {52,41}, {43, 39}, {}, {}, {}. Waste 4.
PS6: {55, 44}, {53, 39}, {52,41}, {44}, {43}, {}, {}. Waste 1.
PS7: {55!}, {53, 39}, {52,41}, {44}, {44}, {43}, {}.
PS8: {55!}, {53, 39}, {52!}, {44, 43}, {44, 41}, {}, {}.
PS9: {55, 44}, {53, 39}, {52!}, {44, 41}, {43}, {}, {}. Waste 1.
PS10: {55!}, {53, 39}, {52!}, {44, 41}, {44}, {43}, {}.
PS11: {55!}, {53, 39}, {52!}, {44, 44}, {43, 41}, {}, {}.
PS12: {55, 44}, {53, 44}, {52,39}, {43, 41}, {}, {}, {}. Waste 4.
PS13: {55, 44}, {53, 39}, {52!}, {44}, {43, 41}, {}, {}. Waste 1.
PS14: {55!}, {53, 39}, {52!}, {44}, {44}, {43, 41}, {}.
PS15: {55!}, {53, 39}, {52!}, {44, 44}, {43}, {41}, {}.
PS16: {55, 44}, {53, 39}, {52!}, {44, 43}, {41}, {}, {}. Waste 1.
PS17: {55!}, {53, 39}, {52!}, {44, 43}, {44}, {41}, {}.
PS18: {55, 44}, {53, 44}, {52,39}, {43}, {41}, {}, {}. Waste 4.
PS19: {55, 44}, {53, 39}, {52!}, {44}, {43}, {41}, {}. Waste 1.
PS20: {55!}, {53, 39}, {52!}, {44}, {44}, {43}, {41}.
PS21: {55!}, {53!}, {52,39}, {44, 43}, {44, 41}, {}, {}.
PS22: {55, 44}, {53!}, {52,39}, {44, 41}, {43}, {}, {}. Waste 1.
PS23: {55!}, {53!}, {52,39}, {44, 41}, {44}, {43}, {}.
PS24: {55!}, {53!}, {52,39}, {44, 44}, {43, 41}, {}, {}.
PS25: {55, 44}, {53!}, {52,39}, {44}, {43, 41}, {}, {}. Waste 1.
PS26: {55!}, {53!}, {52,39}, {44}, {44}, {43, 41}, {}.
PS27: {55!}, {53!}, {52,39}, {44, 44}, {43}, {41}, {}.
PS28: {55, 44}, {53!}, {52,39}, {44, 43}, {41}, {}, {}. Waste 1.
PS29: {55!}, {53!}, {52,39}, {44, 43}, {44}, {41}, {}.
PS30: {55, 44}, {53!}, {52,39}, {44}, {43}, {41}, {}. Waste 1.
PS31: {55!}, {53!}, {52,39}, {44}, {44}, {43}, {41}.
PS32: {55!}, {53!}, {52,41}, {44, 43}, {44, 39}, {}, {}.
PS33: {55, 44}, {53!}, {52,41}, {44, 39}, {43}, {}, {}. Waste 1.
PS34: {55!}, {53!}, {52,41}, {44, 39}, {44}, {43}, {}.
PS35: {55!}, {53!}, {52!}, {44, 41}, {44, 39}, {43}, {}.
PS36: {55, 44}, {53!}, {52!}, {44, 39}, {43, 41}, {}, {}. Waste 1.
PS37: {55!}, {53!}, {52!}, {44, 39}, {44}, {43, 41}, {}.
PS38: {55!}, {53!}, {52!}, {44, 43}, {44, 39}, {41}, {}.
PS39: {55, 44}, {53!}, {52!}, {44, 39}, {43}, {41}, {}. Waste 1.
PS40: {55!}, {53!}, {52!}, {44, 39}, {44}, {43}, {41}.
PS41: {55!}, {53!}, {52,41}, {44, 44}, {43, 39}, {}, {}.
PS42: {55, 44}, {53!}, {52,41}, {44}, {43, 39}, {}, {}. Waste 1.
PS43: {55!}, {53!}, {52,41}, {44}, {44}, {43, 39}, {}.
PS44: {55, 44}, {53!}, {52!}, {44, 41}, {43, 39}, {}, {}. Waste 1.
PS45: {55!}, {53!}, {52!}, {44, 41}, {44}, {43, 39}, {}.
PS46: {55!}, {53!}, {52!}, {44, 44}, {43, 39}, {41}, {}.
PS47: {55, 44}, {53, 44}, {52!}, {43, 39}, {41}, {}, {}. Waste 4.
PS48: {55, 44}, {53!}, {52!}, {44}, {43, 39}, {41}, {}. Waste 1.
PS49: {55!}, {53!}, {52!}, {44}, {44}, {43, 39}, {41}.
PS50: {55!}, {53!}, {52!}, {44, 44}, {43}, {41, 39}, {}.
PS51: {55, 44}, {53!}, {52!}, {44, 43}, {41, 39}, {}, {}. Waste 1.
PS52: {55!}, {53!}, {52!}, {44, 43}, {44}, {41, 39}, {}.
PS53: {55, 44}, {53, 44}, {52!}, {43}, {41, 39}, {}, {}. Waste 4.
PS54: {55, 44}, {53!}, {52!}, {44}, {43}, {41, 39}, {}. Waste 1.
PS55: {55!}, {53!}, {52!}, {44}, {44}, {43}, {41, 39}.
PS56: {55, 44}, {53, 44}, {52,43}, {41}, {39}, {}, {}. Waste 9.
PS57: {55!}, {53!}, {52,41}, {44, 44}, {43}, {39}, {}.
PS58: {55, 44}, {53!}, {52,41}, {44, 43}, {39}, {}, {}. Waste 1.
PS59: {55!}, {53!}, {52,41}, {44, 43}, {44}, {39}, {}.
PS60: {55, 44}, {53, 44}, {52,41}, {43}, {39}, {}, {}. Waste 4.
PS61: {55, 44}, {53!}, {52,41}, {44}, {43}, {39}, {}. Waste 1.
PS62: {55!}, {53!}, {52,41}, {44}, {44}, {43}, {39}.
PS63: {55!}, {53!}, {52!}, {44, 43}, {44, 41}, {39}, {}.
PS64: {55, 44}, {53!}, {52!}, {44, 41}, {43}, {39}, {}. Waste 1.
PS65: {55!}, {53!}, {52!}, {44, 41}, {44}, {43}, {39}.
PS66: {55!}, {53!}, {52!}, {44, 44}, {43, 41}, {39}, {}.
PS67: {55, 44}, {53, 44}, {52!}, {43, 41}, {39}, {}, {}. Waste 4.
PS68: {55, 44}, {53!}, {52!}, {44}, {43, 41}, {39}, {}. Waste 1.
PS69: {55!}, {53!}, {52!}, {44}, {44}, {43, 41}, {39}.
PS70: {55!}, {53!}, {52!}, {44, 44}, {43}, {41}, {39}.
PS71: {55, 44}, {53!}, {52!}, {44, 43}, {41}, {39}, {}. Waste 1.
PS72: {55!}, {53!}, {52!}, {44, 43}, {44}, {41}, {39}.
PS73: {55, 44}, {53, 44}, {52!}, {43}, {41}, {39}, {}. Waste 4.
PS74: {55, 44}, {53!}, {52!}, {44}, {43}, {41}, {39}. Waste 1.

After this step it is worthwile to validate if all solutions can still (potentially) accommodate all large values. The next 7 values are 38, 36, 36, 32, 32, 31 and 29.

That means there should be at least one bin that has unused space of 38.
That there should be at least 3 bins with unused space of 36 (or half of that with unused space of 72).
That there should be at least 5 bins with unused space of 32.
That there should be at least 6 bins with unused space of 31.
That there should be at least 7 bins with unused space of 29.

Look at potential solution 55:
PS55: {55}, {53}, {52}, {44}, {44}, {43}, {41, 39}.
In this case, the bins 0, 1, 2, 3, 4 and 5 each allow for just one package of 29, so there are not 7 spots left, which disqualifies this potential solution. Therefore it is removed from the list.

In the code later in the article, this is done in the pruning process.

Step 9. The next package to place has a size of 38. Note that the smallest available package is 7.
Again, the number of potential solutions more than triples, from 74 to 296.

The more potential solutions there are, the more a pruning process can eliminate pointless ones. There are seven potential solutions that cannot accommodate sufficient packages of size 29, bringing the total amount of potential solutions to 289.

Step 10. The next package to place has a size of 36. The smallest available package is 7, maximum allowed waste is 13.
Again, the number of potential solutions more than triples, from 289 to 1093. This excludes the 7 potential solutions in which the package could not be placed, such as:
PS202: {55!}, {53, 39}, {52!}, {44!}, {44!}, {43!}, {41, 38}.

There are 49 potential solutions that cannot accommodate sufficient packages of size 29, bringing the number of potential solutions down to 1044.

Step 11. The next package to place again has a size of 36. The smallest available package is 7, maximum allowed waste is 13.
Again, the number of potential solutions more than triples, from 1044 to 3445. Pruning brings it down to 3145.

Step 12. The next package to place has a size of 32.
The growth slows down slightly, growing from 3145 to 8267. Pruning has a considerable effect and brings it down to 6392.

Step 13. The next package to place again has a size of 32.
This one is interesting, because there are 1064 cases where there is an exact match for this package. Rule 6 says that this trumps all other options, so it limits the creation of new potential solutions. It grows from 6392 to 10931. After pruning there are 7972 left.

Step 14. The next package to place has a size of 31.
The number of potential solutions grows slightly, from 7972 to 10718, but after pruning it the search area is actually reduced to 6212 potential solutions. 15 potential solutions could be pruned because they exceeded the maximum allowed waste.
In other words, the peak (maximum amount of potential solutions) was passed.

Step 15. The next package to place has a size of 29.
Again there are hundreds of exact matches, limiting growth of the search area. It shrinks from 6212 to 6074, mainly because of 538 potential solutions that could not accommodate this package. Pruning reduces this massively to 2743.

Step 16. The next package to place has a size of 23.
Again there are hundreds of exact matches. The search area grows from 2743 to 3121. Pruning reduces this to 2191, partly because 24 potential solutions waste too much.

Step 17. The next package to place has a size of 22.
The search area shrinks, from 2191 to 1543, mainly because hundreds of potential solutions could not accommodate this package. Pruning brings it further down to 680.

Step 18. The next package to place has a size of 16.
The search area shrinks from 680 to 473. The majority of the placements are perfect matches (see rule 6), and in 45 cases, a particular bin had to be filled up (see rule 8). Also, 210 of potential solutions could not accommodate this package. Pruning brings it further down to 427.

Step 19. The next package to place has a size of 14. The smallest available package is 7, maximum allowed waste is 13.
The search area shrinks from 427 to 217. Many of the placements are perfect matches (see rule 6), and fill ups (see rule 8). Again, 210 of potential solutions could not accommodate this package, which had the biggest effect on the search area. Pruning did not change anything.

Step 20. The last package to place has a size of 7.
92 out of the 217 potential solutions hold up. So the conclusion is, that there are in fact solutions for these packages with only 7 bins. One possible solution is:
PS2584: {55, 44}, {53, 44}, {52, 41, 7}, {43, 32, 23}, {39, 36, 22}, {38, 32, 29}, {36, 31, 16, 14}.

This example takes 22 seconds to run on my system. The Assign Package step of iteration 13 took the longest: 3.1 seconds. The purging was slowest in iteration 14, taking 1.8 seconds. In this peak phase, there were 7972 potential solutions (or over 10000, depending on when you count).

Code

So let's convert this into code.

-- set up base tables
CREATE TABLE dbo.solutions
(solution     bigint   IDENTITY PRIMARY KEY CLUSTERED
,processed    tinyint  NOT NULL
,total_waste  smallint NOT NULL
,derived_from bigint       NULL
,bin_no       int          NULL
)

CREATE TABLE dbo.positions
(solution    bigint   NOT NULL
,bin_no      int      NOT NULL
,space_left  smallint     NULL
,CONSTRAINT PK_positions PRIMARY KEY CLUSTERED (solution, bin_no)
)

CREATE TABLE dbo.tries_P
(solution       bigint   NOT NULL
,package_no     int      NOT NULL
,size           smallint NOT NULL
,bin_no         int          NULL
,CONSTRAINT PK_tries_P PRIMARY KEY CLUSTERED(solution,package_no)
)
CREATE INDEX IX_tries_P_package_no ON dbo.tries_P (package_no)

CREATE TABLE dbo.tries_B
(solution     bigint   NOT NULL
,bin_no       int      NOT NULL
,space_left   smallint NOT NULL
,fill_up      int          NULL
,derived_from bigint       NULL
,changed      bit          NULL
,CONSTRAINT PK_tries_B PRIMARY KEY CLUSTERED (solution,bin_no)
)
CREATE INDEX IX_tries_B_space_left ON dbo.tries_B (space_left, solution) INCLUDE (bin_no, fill_up)


-- helper table
CREATE TABLE dbo.numbers (n int PRIMARY KEY)

INSERT INTO dbo.numbers VALUES (1)
Declare @i int
Set @i=1
While 100000 > @i
Begin
  INSERT INTO dbo.numbers
  SELECT n+@i FROM dbo.numbers
  
  Set @i=@i*2
End

-- scenario setup
CREATE TABLE dbo.packages
(package_no int      IDENTITY PRIMARY KEY
,size       smallint NOT NULL
,bin_no     int          NULL
)
CREATE INDEX IX_packages_binno_size ON dbo.packages(bin_no,size)

INSERT INTO dbo.packages(size) VALUES (36)
INSERT INTO dbo.packages(size) VALUES (39)
INSERT INTO dbo.packages(size) VALUES (43)
INSERT INTO dbo.packages(size) VALUES (53)
INSERT INTO dbo.packages(size) VALUES (32)
INSERT INTO dbo.packages(size) VALUES (7)
INSERT INTO dbo.packages(size) VALUES (23)
INSERT INTO dbo.packages(size) VALUES (41)
INSERT INTO dbo.packages(size) VALUES (55)
INSERT INTO dbo.packages(size) VALUES (29)
INSERT INTO dbo.packages(size) VALUES (14)
INSERT INTO dbo.packages(size) VALUES (44)
INSERT INTO dbo.packages(size) VALUES (44)
INSERT INTO dbo.packages(size) VALUES (22)
INSERT INTO dbo.packages(size) VALUES (32)
INSERT INTO dbo.packages(size) VALUES (52)
INSERT INTO dbo.packages(size) VALUES (38)
INSERT INTO dbo.packages(size) VALUES (16)
INSERT INTO dbo.packages(size) VALUES (31)
INSERT INTO dbo.packages(size) VALUES (36)

The main code (see later in this article) relies on three stored procedures that do the real work. The first one is pretty simple, it is the code to determine the next package to place. It will always fetch the largest remaining package, because the rest of the code cannot handle an arbitrary order. So if you use the code in this article, then don't change the logic of this stored procedure

CREATE PROCEDURE dbo.determine_next_package
(@bin_size smallint
,@debug    bit)
AS
Begin
  -- Big to small
  SET NOCOUNT ON
  Declare @package_no int
  SELECT TOP 1 @package_no = package_no
  FROM dbo.tries_P
  WHERE solution=1
  AND   bin_no IS NULL
  ORDER BY size DESC
  
  If @package_no IS NOT NULL AND @debug = 1
    SELECT 'Biggest Remaining' AS Method, @package_no AS package_no, size FROM dbo.tries_P WHERE solution=1 AND package_no = @package_no
  
  If @package_no IS NULL AND @debug = 1
    SELECT 'NO PACKAGE FOUND' AS Method
  
  Return @package_no
End

Next, there is the main part of the code: the stored procedure that will assign a package to a bin, and will create alternative potential solutions when required.

CREATE PROCEDURE dbo.assign_package
(@package_no            int
,@bin_size              smallint
,@number_of_target_bins int
,@debug                 bit
) AS
Begin
  SET NOCOUNT ON
  Declare @size            smallint
  Declare @smallest        smallint
  Declare @second_smallest smallint
  
  Set @size = (SELECT size FROM dbo.tries_P WHERE solution=1 AND package_no=@package_no)
  Set @smallest = (SELECT MIN(size) FROM dbo.tries_P WHERE solution=1 AND bin_no IS NULL)
  If (SELECT COUNT(*) FROM dbo.tries_P WHERE solution=1 AND bin_no IS NULL AND size=@smallest) > 1
    Set @second_smallest = @smallest
  Else
    Set @second_smallest = (SELECT MIN(size) FROM dbo.tries_P WHERE solution=1 AND bin_no IS NULL AND size > @smallest)
  
  UPDATE dbo.solutions  SET processed=0 WHERE processed > 0
  UPDATE dbo.tries_B    SET changed=0, derived_from=NULL
  
  -- ** Exact match
  TRUNCATE TABLE dbo.positions
  
  INSERT INTO dbo.positions (solution, bin_no)
  SELECT solution, MIN(bin_no)
  FROM  dbo.tries_B B
  WHERE B.space_left = @size
  GROUP BY solution
  
  If @@ROWCOUNT > 0
  Begin
    UPDATE dbo.tries_P
    SET bin_no = (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
    WHERE package_no = @package_no
    AND   EXISTS (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
   
    UPDATE dbo.tries_B
    SET space_left = space_left - @size
    ,   changed    = 1
    WHERE EXISTS (
      SELECT *
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_B.solution
      AND   T.bin_no   = dbo.tries_B.bin_no
    )
    
    UPDATE dbo.solutions
    SET processed = 1
    WHERE EXISTS (
      SELECT *
      FROM dbo.positions T
      WHERE T.solution = dbo.solutions.solution
    )
    
    If @debug = 1
      SELECT 'Exact Match' AS assignment, COUNT(*) AS solutions
      FROM dbo.positions
  End
  
  
  -- ** Real Fill Up
  TRUNCATE TABLE dbo.positions
  
  INSERT INTO dbo.positions (solution, bin_no)
  SELECT solution, bin_no
  FROM (
    SELECT B.solution, B.bin_no, ROW_NUMBER() OVER (PARTITION BY B.solution ORDER BY B.space_left) AS rn
    FROM  dbo.tries_B   B
    JOIN  dbo.solutions S
      ON S.solution = B.solution
    WHERE S.processed = 0
    AND   B.space_left >= @size
    AND   B.space_left <  @second_smallest
  ) T
  WHERE rn = 1
  
  If @@ROWCOUNT > 0
  Begin
    -- handle existing solutions
    UPDATE dbo.tries_P
    SET bin_no = (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
    WHERE package_no = @package_no
    AND   EXISTS (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
    
    UPDATE dbo.tries_B
    SET space_left = space_left - @size
    ,   changed    = 1
    WHERE EXISTS (
      SELECT *
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_B.solution
      AND   T.bin_no   = dbo.tries_B.bin_no
    )
    
    UPDATE dbo.solutions
    SET processed = 1
    WHERE EXISTS (
      SELECT *
      FROM dbo.positions T
      WHERE T.solution = dbo.solutions.solution
    )
    
    If @debug = 1
      SELECT 'Real Fill Up' AS assignment, COUNT(*) AS solutions
      FROM dbo.positions
  End
  
  
  -- ** Fill up
  TRUNCATE TABLE dbo.positions
  
  INSERT INTO dbo.positions (solution, bin_no)
  SELECT solution, bin_no
  FROM (
    SELECT B.solution, B.bin_no, ROW_NUMBER() OVER (PARTITION BY B.solution ORDER BY B.space_left) AS rn
    FROM  dbo.tries_B   B
    JOIN  dbo.solutions S
      ON S.solution = B.solution
    WHERE S.processed = 0
    AND   B.space_left >= @size
    AND   B.space_left <  @smallest + @second_smallest
    AND   B.fill_up    <  @size
  ) T
  WHERE rn = 1
  
  If @@ROWCOUNT > 0
  Begin
    -- create and handle new solutions
    INSERT INTO dbo.solutions (total_waste, processed, derived_from, bin_no)
    SELECT 0, 2, B.solution, MIN(B.bin_no)
    FROM dbo.positions T
    JOIN dbo.tries_B   B
      ON B.solution = T.solution
    WHERE B.space_left >= @smallest + @second_smallest
    AND   B.space_left >= @size
    AND   B.fill_up    <  @size
    GROUP BY B.solution, B.space_left
    
    INSERT INTO dbo.tries_P (solution, package_no, size, bin_no)
    SELECT S.solution, P.package_no, P.size, CASE WHEN package_no = @package_no THEN S.bin_no ELSE P.bin_no END
    FROM dbo.solutions S
    JOIN dbo.tries_P   P
      ON P.solution = S.derived_from
    WHERE S.processed = 2
    
    INSERT INTO dbo.tries_B (solution, bin_no, space_left, fill_up, derived_from, changed)
    SELECT S.solution, B.bin_no, @bin_size - ISNULL((
      SELECT SUM(P.size)
      FROM  dbo.tries_P P
      WHERE P.solution = S.solution
      AND   P.bin_no   = B.bin_no
    ),0), CASE WHEN B.space_left - @size < @smallest AND @size > B.fill_up THEN @size ELSE B.fill_up END
    , S.derived_from, CASE WHEN S.bin_no = B.bin_no THEN 1 ELSE 0 END
    FROM dbo.solutions S
    JOIN dbo.tries_B   B
      ON B.solution = S.derived_from
    WHERE S.processed = 2
    
    If @debug = 1
    Begin
      Declare @rc int
      Set @rc=(SELECT COUNT(*) FROM dbo.solutions WHERE processed=2)
      If @rc>0
        SELECT 'Fill Up' AS assignment, @rc AS new_solutions
    End
    
    UPDATE dbo.solutions
    SET   processed=1
    WHERE processed=2
    
    -- handle existing solutions
    UPDATE dbo.tries_P
    SET bin_no = (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
    WHERE package_no = @package_no
    AND   EXISTS (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
    
    UPDATE dbo.tries_B
    SET space_left = space_left - @size
    ,   changed    = 1
    WHERE EXISTS (
      SELECT *
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_B.solution
      AND   T.bin_no   = dbo.tries_B.bin_no
    )
    
    UPDATE dbo.solutions
    SET processed = 1
    WHERE EXISTS (
      SELECT *
      FROM dbo.positions T
      WHERE T.solution = dbo.solutions.solution
    )
    
    If @debug = 1
      SELECT 'Fill Up' AS assignment, COUNT(*) AS solutions
      FROM dbo.positions
  End
  
  
  -- ** Other / Regular
  TRUNCATE TABLE dbo.positions
  
  INSERT INTO dbo.positions (solution, bin_no, space_left)
  SELECT solution, bin_no, space_left
  FROM (
    SELECT B.solution, B.bin_no, B.space_left, ROW_NUMBER() OVER (PARTITION BY B.solution ORDER BY B.space_left) AS rn
    FROM  dbo.tries_B   B
    JOIN  dbo.solutions S
      ON S.solution = B.solution
    WHERE S.processed = 0
    AND   B.space_left >= @size
    AND  (B.fill_up    <  @size  OR  B.space_left - @size >=  @smallest)
  ) T
  WHERE rn = 1
  
  If @@rowcount > 0
  Begin
    -- create and handle new solutions
    -- the CASE expressions handles suboptimal fill ups (prevents pointless solutions).
    INSERT INTO dbo.solutions (total_waste, processed, derived_from, bin_no)
    SELECT 0, 2, B.solution, MIN(B.bin_no)
    FROM dbo.positions T
    JOIN dbo.tries_B   B
      ON B.solution = T.solution
    WHERE B.space_left <> T.space_left
    AND   B.space_left >= @size
    AND  (B.fill_up    <  @size  OR  B.space_left - @size >=  @smallest)
    AND   CASE WHEN T.space_left - @size < @smallest AND B.space_left - @size < @smallest THEN 0 ELSE 1 END = 1
    GROUP BY B.solution, B.space_left
    
    If @@ROWCOUNT > 0
    Begin
      -- *** can take a long time, longer than the next
      -- ** HASH JOIN outperforms LOOP JOIN here
      INSERT INTO dbo.tries_P (solution, package_no, size, bin_no)
      SELECT S.solution, P.package_no, P.size, CASE WHEN package_no = @package_no THEN S.bin_no ELSE P.bin_no END
      FROM dbo.solutions S
      INNER HASH JOIN dbo.tries_P   P
        ON P.solution = S.derived_from
      WHERE S.processed = 2
      OPTION (force order)
      
      -- *** can take quite some time...
      -- ** HASH JOIN outperforms LOOP JOIN here
      INSERT INTO dbo.tries_B (solution, bin_no, space_left, fill_up, derived_from, changed)
      SELECT S.solution, B.bin_no, @bin_size - ISNULL((
        SELECT SUM(P.size)
        FROM  dbo.tries_P P
        WHERE P.solution = S.solution
        AND   P.bin_no   = B.bin_no
      ),0), CASE WHEN B.space_left - @size < @smallest AND @size > B.fill_up THEN @size ELSE B.fill_up END
      , S.derived_from, CASE WHEN S.bin_no = B.bin_no THEN 1 ELSE 0 END
      FROM dbo.solutions S
      INNER HASH JOIN dbo.tries_B   B
        ON B.solution = S.derived_from
      WHERE S.processed = 2
      OPTION (force order)
      
      If @debug = 1
        SELECT 'Regular' AS assignment, COUNT(*) AS new_solutions
        FROM dbo.solutions
        WHERE processed=2
      
      UPDATE dbo.solutions
      SET   processed=1
      WHERE processed=2
    End
    
    -- handle existing solutions
    UPDATE dbo.tries_P
    SET bin_no = (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
    WHERE package_no = @package_no
    AND   EXISTS (
      SELECT bin_no
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_P.solution
    )
    
    UPDATE dbo.tries_B
    SET space_left = space_left - @size
    ,   changed    = 1
    WHERE EXISTS (
      SELECT *
      FROM dbo.positions T
      WHERE T.solution = dbo.tries_B.solution
      AND   T.bin_no   = dbo.tries_B.bin_no
    )
    
    If @debug = 1
      SELECT 'Regular' AS assignment, COUNT(*) AS solutions
      FROM dbo.positions
  End
  
  Set @smallest = (SELECT MIN(size) FROM dbo.tries_P WHERE solution=1 AND bin_no IS NULL)
  
  UPDATE dbo.solutions
  SET total_waste = ISNULL((
    SELECT SUM(space_left)
    FROM dbo.tries_B B
    WHERE B.solution = dbo.solutions.solution
    AND   B.space_left < @smallest
  ),0)
End

And finally, there is the stored procedure that removes potential solutions that cannot complete or are duplicate.

Also, this stored procedure will make sure that there is always potential solution 1. So after running this stored procedure, if there is no longer solution 1, then there is no solution!

CREATE PROCEDURE dbo.prune_bins
(@bin_size              smallint
,@number_of_target_bins int
,@debug                 bit
) AS
Begin
  SET NOCOUNT ON
  Declare @rc        int
  Declare @max_waste int
  Declare @size      int
  Declare @biggest   int
  Declare @bin_no    int
  
  -- eliminate incomplete solutions
  SELECT @size=MIN(space_left)
  FROM (
    SELECT SUM(space_left) AS space_left
    FROM dbo.tries_B
    GROUP BY solution
  ) Regular
  
  DELETE FROM dbo.solutions
  WHERE EXISTS (
    SELECT SUM(space_left) 
    FROM dbo.tries_B B
    WHERE B.solution = dbo.solutions.solution
    HAVING SUM(space_left) > @size
  )
  
  -- delete solution with too much waste
  Set @max_waste = (@bin_size * @number_of_target_bins) - (
    SELECT SUM(size)
    FROM dbo.tries_P
    WHERE solution=1
  )
  
  DELETE FROM dbo.solutions
  WHERE total_waste > @max_waste
  Set @rc = @@ROWCOUNT
  
  If @rc > 0 AND @debug = 1
    SELECT 'Too much waste' AS Method, @rc AS solutions
  
  -- quick cross check if there is sufficient free space for large amount of big packages
  Set @rc = 0
  Set @biggest = (SELECT MAX(size) FROM dbo.tries_P WHERE solution=1 AND bin_no IS NULL)
  Set @size = @biggest
  
  While @size > @biggest / 2
  Begin
    Set @rc = (SELECT COUNT(*) FROM dbo.tries_P WHERE solution=1 AND bin_no IS NULL and size >= @size)
    
    DELETE FROM dbo.solutions
    WHERE (
      SELECT SUM(space_left / @size) 
      FROM dbo.tries_B B
      WHERE B.solution = dbo.solutions.solution
    ) < @rc
    Set @rc=@@ROWCOUNT
    
    If @rc > 0 AND @debug = 1
      SELECT 'Not all big packages fit' AS Method, @rc AS solutions
    
    Set @size = (SELECT MAX(size) FROM dbo.tries_P WHERE solution=1 AND bin_no IS NULL AND size < @size)
  End
  
  -- solutions may have been removed. Clean up, so it is consistent again
  DELETE FROM dbo.tries_B
  WHERE NOT EXISTS (
    SELECT *
    FROM dbo.solutions S
    WHERE S.solution = dbo.tries_B.solution
  )
  
  DELETE FROM dbo.tries_P
  WHERE NOT EXISTS (
    SELECT *
    FROM dbo.solutions S
    WHERE S.solution = dbo.tries_P.solution
  )
  
  IF NOT EXISTS (
    SELECT *
    FROM dbo.solutions
    WHERE solution=1
  )
  Begin
    -- solution 1 was removed. Rebase a solution
    Declare @old int
    Set @old = (SELECT TOP 1 solution FROM dbo.solutions)
    
    If @debug = 1
      SELECT 'Re-establishing solution 1' AS Method, @old AS old_solution
    
    SET IDENTITY_INSERT dbo.solutions ON
    
    INSERT INTO dbo.solutions (solution, total_waste, processed, derived_from, bin_no)
    SELECT 1, total_waste, processed, derived_from, bin_no
    FROM dbo.solutions
    WHERE solution=@old
    
    SET IDENTITY_INSERT dbo.solutions OFF
    
    DELETE FROM dbo.solutions
    WHERE solution=@old
    
    UPDATE dbo.tries_B
    SET solution=1
    WHERE solution=@old
    
    UPDATE dbo.tries_P
    SET solution=1
    WHERE solution=@old
  End
 End

And finally the main executable batch. It does the final preparation and loops through all the packages, placing them one at a time, and reporting progress while running. When found, the solution is printed.

Make sure you set the number of target bins appropriately!

TRUNCATE TABLE dbo.positions
TRUNCATE TABLE dbo.solutions
TRUNCATE TABLE dbo.tries_B
TRUNCATE TABLE dbo.tries_P

Declare @solution bigint
INSERT INTO dbo.solutions (processed,total_waste) VALUES (0,0)
set @solution=@@IDENTITY

INSERT INTO dbo.tries_P (solution, package_no, bin_no, size)
SELECT @solution, package_no, NULL, size
FROM dbo.packages

Declare @number_of_target_bins int
Set @number_of_target_bins = 7
Declare @bin_size   smallint
Set @bin_size = 100

INSERT INTO dbo.tries_B (solution, bin_no, space_left, fill_up, changed)
SELECT @solution, n, @bin_size, 0, 0
FROM dbo.numbers
WHERE n BETWEEN 1 AND @number_of_target_bins

Declare @dt    datetime
Declare @dt_np datetime
Declare @dt_ap datetime
Declare @dt_pb datetime
Declare @iteration int
Declare @package_no int
Declare @debug bit
Set @debug=0

Set @iteration = 1
While EXISTS (SELECT * FROM dbo.solutions)
  AND EXISTS (SELECT * FROM dbo.tries_P WHERE bin_no IS NULL)
Begin
  Set @dt=CURRENT_TIMESTAMP
  EXEC @package_no = dbo.determine_next_package @bin_size, @debug
  Set @dt_np=CURRENT_TIMESTAMP
  EXEC dbo.assign_package @package_no, @bin_size, @number_of_target_bins, @debug
  Set @dt_ap=CURRENT_TIMESTAMP
  EXEC dbo.prune_bins @bin_size, @number_of_target_bins, @debug
  Set @dt_pb=CURRENT_TIMESTAMP
  
  SELECT @iteration AS iteration
  ,      @package_no AS package_no
  ,      COUNT(*) AS solutions
  ,      DATEDIFF(ms,@dt, @dt_pb) AS total_performance
  ,      DATEDIFF(ms,@dt, @dt_np) AS perf_np
  ,      DATEDIFF(ms,@dt_np, @dt_ap) AS perf_ap
  ,      DATEDIFF(ms,@dt_ap, @dt_pb) AS perf_pb
  FROM dbo.solutions
  
  Set @iteration=@iteration+1
End

SELECT * FROM dbo.tries_P WHERE solution=1 ORDER BY bin_no, size DESC, package_no



Part 6 - Solution 6 | Part 8 - Brute Force discovery for many packages


Back to SQL Server main menu. Mail your comments to gertjans@xs4all.nl.