perf: Add lower-bound pruning to ExhaustiveFitEngine
Precompute suffix sums of remaining item volumes and use them to prune branches that cannot beat the current best solution. Raises DefaultMaxItems from 20 to 25 (~84ms worst case). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,9 @@ namespace CutList.Core.Nesting
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default maximum number of items before falling back to AdvancedFitEngine.
|
/// Default maximum number of items before falling back to AdvancedFitEngine.
|
||||||
/// Testing showed 20 items is safe (~100ms worst case), while 21+ can take seconds.
|
/// Testing showed 25 items is safe (~84ms worst case), while 30+ can take seconds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const int DefaultMaxItems = 20;
|
public const int DefaultMaxItems = 25;
|
||||||
|
|
||||||
private readonly IEngine _fallbackEngine;
|
private readonly IEngine _fallbackEngine;
|
||||||
private readonly int _maxItems;
|
private readonly int _maxItems;
|
||||||
@@ -67,7 +67,15 @@ namespace CutList.Core.Nesting
|
|||||||
BinCount = 0
|
BinCount = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
Search(sortedItems, 0, currentState, bestSolution, request);
|
// Precompute suffix sums of item lengths (including spacing per item)
|
||||||
|
// for lower-bound pruning. suffixVolume[i] = total volume of items[i..n-1].
|
||||||
|
var suffixVolume = new double[sortedItems.Count + 1];
|
||||||
|
for (int i = sortedItems.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
suffixVolume[i] = suffixVolume[i + 1] + sortedItems[i].Length + request.Spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
Search(sortedItems, 0, currentState, bestSolution, request, suffixVolume);
|
||||||
|
|
||||||
// Build result from best solution
|
// Build result from best solution
|
||||||
var result = new PackResult();
|
var result = new PackResult();
|
||||||
@@ -101,7 +109,8 @@ namespace CutList.Core.Nesting
|
|||||||
int itemIndex,
|
int itemIndex,
|
||||||
SearchState current,
|
SearchState current,
|
||||||
SearchState best,
|
SearchState best,
|
||||||
PackingRequest request)
|
PackingRequest request,
|
||||||
|
double[] suffixVolume)
|
||||||
{
|
{
|
||||||
// All items placed - check if this is better
|
// All items placed - check if this is better
|
||||||
if (itemIndex >= items.Count)
|
if (itemIndex >= items.Count)
|
||||||
@@ -123,6 +132,18 @@ namespace CutList.Core.Nesting
|
|||||||
if (current.BinCount >= request.MaxBinCount)
|
if (current.BinCount >= request.MaxBinCount)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Lower-bound pruning: remaining items need at least this many additional bins
|
||||||
|
double remainingVolume = suffixVolume[itemIndex];
|
||||||
|
double availableInExisting = 0;
|
||||||
|
for (int b = 0; b < current.Bins.Count; b++)
|
||||||
|
{
|
||||||
|
availableInExisting += request.StockLength - GetBinUsedLength(current.Bins[b], request.Spacing);
|
||||||
|
}
|
||||||
|
double overflow = remainingVolume - availableInExisting;
|
||||||
|
int additionalBinsNeeded = overflow > 0 ? (int)Math.Ceiling(overflow / request.StockLength) : 0;
|
||||||
|
if (current.BinCount + additionalBinsNeeded >= best.BinCount)
|
||||||
|
return;
|
||||||
|
|
||||||
var item = items[itemIndex];
|
var item = items[itemIndex];
|
||||||
|
|
||||||
// Symmetry breaking: if this item has the same length as the previous item,
|
// Symmetry breaking: if this item has the same length as the previous item,
|
||||||
@@ -148,7 +169,7 @@ namespace CutList.Core.Nesting
|
|||||||
current.Bins[i].Add(item);
|
current.Bins[i].Add(item);
|
||||||
var prevBinIndex = current.LastBinIndexUsed;
|
var prevBinIndex = current.LastBinIndexUsed;
|
||||||
current.LastBinIndexUsed = i;
|
current.LastBinIndexUsed = i;
|
||||||
Search(items, itemIndex + 1, current, best, request);
|
Search(items, itemIndex + 1, current, best, request, suffixVolume);
|
||||||
current.LastBinIndexUsed = prevBinIndex;
|
current.LastBinIndexUsed = prevBinIndex;
|
||||||
current.Bins[i].RemoveAt(current.Bins[i].Count - 1);
|
current.Bins[i].RemoveAt(current.Bins[i].Count - 1);
|
||||||
}
|
}
|
||||||
@@ -162,7 +183,7 @@ namespace CutList.Core.Nesting
|
|||||||
current.BinCount++;
|
current.BinCount++;
|
||||||
var prevBinIndex = current.LastBinIndexUsed;
|
var prevBinIndex = current.LastBinIndexUsed;
|
||||||
current.LastBinIndexUsed = newBinIndex;
|
current.LastBinIndexUsed = newBinIndex;
|
||||||
Search(items, itemIndex + 1, current, best, request);
|
Search(items, itemIndex + 1, current, best, request, suffixVolume);
|
||||||
current.LastBinIndexUsed = prevBinIndex;
|
current.LastBinIndexUsed = prevBinIndex;
|
||||||
current.Bins.RemoveAt(current.Bins.Count - 1);
|
current.Bins.RemoveAt(current.Bins.Count - 1);
|
||||||
current.BinCount--;
|
current.BinCount--;
|
||||||
|
|||||||
Reference in New Issue
Block a user