using System;
using System.Collections.Generic;
using System.Linq;
namespace Algorithms.Knapsack;
/// <summary>
/// Branch and bound Knapsack solver.
/// </summary>
/// <typeparam name="T">Type of items in knapsack.</typeparam>
public class BranchAndBoundKnapsackSolver<T>
{
/// <summary>
/// Returns the knapsack containing the items that maximize value while not exceeding weight capacity.
/// Construct a tree structure with total number of items + 1 levels, each node have two child nodes,
/// starting with a dummy item root, each following levels are associated with 1 items, construct the
/// tree in breadth first order to identify the optimal item set.
/// </summary>
/// <param name="items">All items to choose from.</param>
/// <param name="capacity">The maximum weight capacity of the knapsack to be filled.</param>
/// <param name="weightSelector">
/// A function that returns the value of the specified item
/// from the <paramref name="items">items</paramref> list.
/// </param>
/// <param name="valueSelector">
/// A function that returns the weight of the specified item
/// from the <paramref name="items">items</paramref> list.
/// </param>
/// <returns>
/// The array of items that provides the maximum value of the
/// knapsack without exceeding the specified weight <paramref name="capacity">capacity</paramref>.
/// </returns>
public T[] Solve(T[] items, int capacity, Func<T, int> weightSelector, Func<T, double> valueSelector)
{
// This is required for greedy approach in upper bound calculation to work.
items = items.OrderBy(i => valueSelector(i) / weightSelector(i)).ToArray();
// nodesQueue --> used to construct tree in breadth first order
Queue<BranchAndBoundNode> nodesQueue = new();
// maxCumulativeValue --> maximum value while not exceeding weight capacity.
var maxCumulativeValue = 0.0;
// starting node, associated with a temporary created dummy item
BranchAndBoundNode root = new(level: -1, taken: false);
// lastNodeOfOptimalPat --> last item in the optimal item sets identified by this algorithm
BranchAndBoundNode lastNodeOfOptimalPath = root;
nodesQueue.Enqueue(root);
while (nodesQueue.Count != 0)
{
// parent --> parent node which represents the previous item, may or may not be taken into the knapsack
BranchAndBoundNode parent = nodesQueue.Dequeue();
// IF it is the last level, branching cannot be performed
if (parent.Level == items.Length - 1)
{
continue;
}
// create a child node where the associated item is taken into the knapsack
var left = new BranchAndBoundNode(parent.Level + 1, true, parent);
// create a child node where the associated item is not taken into the knapsack
var right = new BranchAndBoundNode(parent.Level + 1, false, parent);
// Since the associated item on current level is taken for the first node,
// set the cumulative weight of first node to cumulative weight of parent node + weight of the associated item,
// set the cumulative value of first node to cumulative value of parent node + value of current level's item.
left.CumulativeWeight = parent.CumulativeWeight + weightSelector(items[left.Level]);
left.CumulativeValue = parent.CumulativeValue + valueSelector(items[left.Level]);
right.CumulativeWeight = parent.CumulativeWeight;
right.CumulativeValue = parent.CumulativeValue;
// IF cumulative weight is smaller than the weight capacity of the knapsack AND
// current cumulative value is larger then the current maxCumulativeValue, update the maxCumulativeValue
if (left.CumulativeWeight <= capacity && left.CumulativeValue > maxCumulativeValue)
{
maxCumulativeValue = left.CumulativeValue;
lastNodeOfOptimalPath = left;
}
left.UpperBound = ComputeUpperBound(left, items, capacity, weightSelector, valueSelector);
right.UpperBound = ComputeUpperBound(right, items, capacity, weightSelector, valueSelector);
// IF upperBound of this node is larger than maxCumulativeValue,
// the current path is still possible to reach or surpass the maximum value,
// add current node to nodesQueue so that nodes below it can be further explored
if (left.UpperBound > maxCumulativeValue && left.CumulativeWeight < capacity)
{
nodesQueue.Enqueue(left);
}
// Cumulative weight is the same as for parent node and < capacity
if (right.UpperBound > maxCumulativeValue)
{
nodesQueue.Enqueue(right);
}
}
return GetItemsFromPath(items, lastNodeOfOptimalPath);
}
// determine items taken based on the path
private static T[] GetItemsFromPath(T[] items, BranchAndBoundNode lastNodeOfPath)
{
List<T> takenItems = new();
// only bogus initial node has no parent
for (var current = lastNodeOfPath; current.Parent is not null; current = current.Parent)
{
if(current.IsTaken)
{
takenItems.Add(items[current.Level]);
}
}
return takenItems.ToArray();
}
/// <summary>
/// Returns the upper bound value of a given node.
/// </summary>
/// <param name="aNode">The given node.</param>
/// <param name="items">All items to choose from.</param>
/// <param name="capacity">The maximum weight capacity of the knapsack to be filled.</param>
/// <param name="weightSelector">
/// A function that returns the value of the specified item
/// from the <paramref name="items">items</paramref> list.
/// </param>
/// <param name="valueSelector">
/// A function that returns the weight of the specified item
/// from the <paramref name="items">items</paramref> list.
/// </param>
/// <returns>
/// upper bound value of the given <paramref name="aNode">node</paramref>.
/// </returns>
private static double ComputeUpperBound(BranchAndBoundNode aNode, T[] items, int capacity, Func<T, int> weightSelector, Func<T, double> valueSelector)
{
var upperBound = aNode.CumulativeValue;
var availableWeight = capacity - aNode.CumulativeWeight;
var nextLevel = aNode.Level + 1;
while (availableWeight > 0 && nextLevel < items.Length)
{
if (weightSelector(items[nextLevel]) <= availableWeight)
{
upperBound += valueSelector(items[nextLevel]);
availableWeight -= weightSelector(items[nextLevel]);
}
else
{
upperBound += valueSelector(items[nextLevel]) / weightSelector(items[nextLevel]) * availableWeight;
availableWeight = 0;
}
nextLevel++;
}
return upperBound;
}
}