Chosen Solution - Random Restart Hill Climbing
After a day of experimentation and research, the best approach I've found was also the most simple one. Just repeatedly trying random configurations until I got an acceptable solution (e.g. "Random Restart Hill Climbing"). Simulated annealing works well too, but I haven't noticed a big enough improvement to justify the extra complexity compared to RRHC - however I admittedly did not spend any real effort tuning the parameters (temperature, schedule, etc.).
I should say that I'm fairly happy with this approach so far - it returns a "good enough" answer pretty quickly, is tunable if I want a more exhaustive search vs faster (more vs less trials), and is flexible in what I accept as "good enough" in the sense that I can just modify the "cost" function - in this case a simple distance between the target l,w,h and the current l,w,h. If, for example, I wanted to penalize being off on height more severely than length or width I could simply multiply the (htarget - hactual) * 2
and give more weight to that particular part of the cost.
That said, for educational purposes I want to go through the other approaches I tried, and see if maybe someone can point out where I went wrong in my backtracking approach specifically. Or alternatively, if I didn't miss anything and maybe RRHC is just a better algorithm for this type of problem, that would be nice to know as well.
This is not the first time I've found RRHC to work well for problems where you need a "good enough" solution and where finding the global optimum is "icing on the cake". In my experience it's a hammer for a lot of nails - surprisingly effective given it's simplicity. One extension I might try later is running multiple parallel threads of search - even better.
Attempted Solution - Backtracking Search
I spent quite a bit of time working on a backtracking search, but my implementation wastes time searching infeasible parts of the solution space. With enough effort I think I could come up with better heuristics to prune the search space, but for now I've given up.
The problem I ran into with the backtracking approach is the following:
Let's say I want to select 4 pieces of furniture and my target l,w,h is 140,80,300.
I run the algorithm:
- 1st selection: Furniture(30,20,120) - all constraints good
- 2nd selection: Furniture(100,50,40) - all constraints good
- 3rd ... etc.
All constraints good simply means I haven't exceeded the targets. Additionally, at each selection step, I filter the list of furniture so that any items which would exceed the remaining l,w,h are removed.
For example, after 2nd selection we have assigned 30+100,20+50,40+120 = 130,70,160 leaving a remaining l,w,h to allocate of 140-130, 80-70, 300-160 = 10,10,140. Any item which has a l>10, w>10, or h>140 wouled be filtered out before we continue the search.
The problem is that while this filtering will make sure to exclude any items that would be in conflict at the immediate next step, it does not consider that a certain state will eventually become a dead-end at later steps. If the only pieces of furniture left now all have a length of 10, they will pass this filtering step yet we know that the current arrangement cannot possibly lead to a solution because by the fourth step there is no budget left for length - we will have exhausted it on step 3 when we assign 10 leaving zero remaining length.
Hacky workaround?
I can do a form of "forward checking" at every step:
- find the min and max for each l,w,h property in the remaining furniture
- if at step 2 we have 140
h
remaining, but the maxh
is 60, we know we can never arrive at a solution since 2*60 = 120 < 140.
In general - at every step n
we filter the furniture list such that:
- the max for a given attribute * n steps >= attribute remaining to be allocated.
- the min for a given attribute * n steps <= attribute remaining to be allocated.
I think this would work, and terminate parts of the search space which will not be feasible much higher up in the search tree, but it feels hacky.
Other Solutions Considered
Mixed Integer Linear Programming: Since I have to select n
items there is a clear integer constraint, meaning linear solvers and, more generally, convex optimization is out the window. I thought MILP might be a possible approach but after some research, I'm not so sure. For one - I cannot think of a way to formulate this as a MILP. The only knob we have to turn is which pieces we select, and whatever l,w,h properties that piece has is a given. For that reason it seemed more of a search problem than MILP, but that could be due to my inexperience with formulating the problem in the proper form.
Constraint Satisfaction Algorithms: Backtracking search while checking constraints is actually in this family of algorithms. I think my proposed "look ahead" is vaguely reminiscent of backtracking search with the "forward checking" heuristic. There are other approaches looking at arc consistency, picking least/most constraint variables as neighbors, etc... but similar to MILP it wasn't clear to me if this problem could be formulated in such a way to leverage those options.
Conclusion
So - is the proposed solution reasonable? Do you think I missed any approaches which would be a better fit (including some of the alternative approaches I tried unsuccessfully?)