Skip to content

Commit f335df4

Browse files
Add branch and bound 0-1 knapsack problem solver (#230)
* Add BranchAndBoundKnapsackSolverTests.cs * Add BranchAndBoundKnapsackSolver.cs * Add BranchAndBoundNode.cs * Update README.md and DIRECTORY.md * Cleanup code and add tests Co-authored-by: Andrii Siriak <[email protected]>
1 parent 18a5393 commit f335df4

File tree

5 files changed

+312
-0
lines changed

5 files changed

+312
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using Algorithms.Knapsack;
3+
using NUnit.Framework;
4+
using FluentAssertions;
5+
6+
namespace Algorithms.Tests.Knapsack
7+
{
8+
public static class BranchAndBoundKnapsackSolverTests
9+
{
10+
[Test]
11+
public static void BranchAndBoundTest_Example1_Success()
12+
{
13+
// Arrange
14+
var items = new[] {'A', 'B', 'C', 'D'};
15+
var values = new[] {18, 20, 14, 18};
16+
var weights = new[] {2, 4, 6, 9};
17+
18+
var capacity = 15;
19+
20+
Func<char, int> weightSelector = x => weights[Array.IndexOf(items, x)];
21+
Func<char, double> valueSelector = x => values[Array.IndexOf(items, x)];
22+
23+
// Act
24+
var solver = new BranchAndBoundKnapsackSolver<char>();
25+
var actualResult = solver.Solve(items, capacity, weightSelector, valueSelector);
26+
27+
// Assert
28+
actualResult.Should().BeEquivalentTo('A', 'B', 'D');
29+
}
30+
31+
[Test]
32+
public static void BranchAndBoundTest_Example2_Success()
33+
{
34+
// Arrange
35+
var items = new[] {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'};
36+
var values = new[] { 505, 352, 458, 220, 354, 414, 498, 545, 473, 543 };
37+
var weights = new[] {23, 26, 20, 18, 32, 27, 29, 26, 30, 27};
38+
39+
var capacity = 67;
40+
41+
Func<char, int> weightSelector = x => weights[Array.IndexOf(items, x)];
42+
Func<char, double> valueSelector = x => values[Array.IndexOf(items, x)];
43+
44+
// Act
45+
var solver = new BranchAndBoundKnapsackSolver<char>();
46+
var actualResult = solver.Solve(items, capacity, weightSelector, valueSelector);
47+
48+
// Assert
49+
actualResult.Should().BeEquivalentTo('H', 'D', 'A');
50+
}
51+
52+
[Test]
53+
public static void BranchAndBoundTest_CapacityIsZero_NothingTaken()
54+
{
55+
// Arrange
56+
var items = new[] {'A', 'B', 'C', 'D'};
57+
var values = new[] {18, 20, 14, 18};
58+
var weights = new[] {2, 4, 6, 9};
59+
60+
var capacity = 0;
61+
62+
Func<char, int> weightSelector = x => weights[Array.IndexOf(items, x)];
63+
Func<char, double> valueSelector = x => values[Array.IndexOf(items, x)];
64+
65+
// Act
66+
var solver = new BranchAndBoundKnapsackSolver<char>();
67+
var actualResult = solver.Solve(items, capacity, weightSelector, valueSelector);
68+
69+
// Assert
70+
actualResult.Should().BeEmpty();
71+
}
72+
73+
[Test]
74+
public static void BranchAndBoundTest_PlentyCapacity_EverythingIsTaken()
75+
{
76+
// Arrange
77+
var items = new[] {'A', 'B', 'C', 'D'};
78+
var values = new[] {18, 20, 14, 18};
79+
var weights = new[] {2, 4, 6, 9};
80+
81+
var capacity = 1000;
82+
83+
Func<char, int> weightSelector = x => weights[Array.IndexOf(items, x)];
84+
Func<char, double> valueSelector = x => values[Array.IndexOf(items, x)];
85+
86+
// Act
87+
var solver = new BranchAndBoundKnapsackSolver<char>();
88+
var actualResult = solver.Solve(items, capacity, weightSelector, valueSelector);
89+
90+
// Assert
91+
actualResult.Should().BeEquivalentTo(items);
92+
}
93+
94+
[Test]
95+
public static void BranchAndBoundTest_NoItems_NothingTaken()
96+
{
97+
// Arrange
98+
var items = Array.Empty<char>();
99+
var values = Array.Empty<int>();
100+
var weights = Array.Empty<int>();
101+
102+
var capacity = 15;
103+
104+
Func<char, int> weightSelector = x => weights[Array.IndexOf(items, x)];
105+
Func<char, double> valueSelector = x => values[Array.IndexOf(items, x)];
106+
107+
// Act
108+
var solver = new BranchAndBoundKnapsackSolver<char>();
109+
var actualResult = solver.Solve(items, capacity, weightSelector, valueSelector);
110+
111+
// Assert
112+
actualResult.Should().BeEmpty();
113+
}
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Algorithms.Knapsack
6+
{
7+
/// <summary>
8+
/// Branch and bound Knapsack solver.
9+
/// </summary>
10+
/// <typeparam name="T">Type of items in knapsack.</typeparam>
11+
public class BranchAndBoundKnapsackSolver<T>
12+
{
13+
/// <summary>
14+
/// Returns the knapsack containing the items that maximize value while not exceeding weight capacity.
15+
/// Construct a tree structure with total number of items + 1 levels, each node have two child nodes,
16+
/// starting with a dummy item root, each following levels are associated with 1 items, construct the
17+
/// tree in breadth first order to identify the optimal item set.
18+
/// </summary>
19+
/// <param name="items">All items to choose from.</param>
20+
/// <param name="capacity">The maximum weight capacity of the knapsack to be filled.</param>
21+
/// <param name="weightSelector">
22+
/// A function that returns the value of the specified item
23+
/// from the <paramref name="items">items</paramref> list.
24+
/// </param>
25+
/// <param name="valueSelector">
26+
/// A function that returns the weight of the specified item
27+
/// from the <paramref name="items">items</paramref> list.
28+
/// </param>
29+
/// <returns>
30+
/// The array of items that provides the maximum value of the
31+
/// knapsack without exceeding the specified weight <paramref name="capacity">capacity</paramref>.
32+
/// </returns>
33+
public T[] Solve(T[] items, int capacity, Func<T, int> weightSelector, Func<T, double> valueSelector)
34+
{
35+
// This is required for greedy approach in upper bound calculation to work.
36+
items = items.OrderBy(i => valueSelector(i) / weightSelector(i)).ToArray();
37+
38+
// nodesQueue --> used to construct tree in breadth first order
39+
Queue<BranchAndBoundNode> nodesQueue = new();
40+
41+
// maxCumulativeValue --> maximum value while not exceeding weight capacity.
42+
var maxCumulativeValue = 0.0;
43+
44+
// starting node, associated with a temporary created dummy item
45+
BranchAndBoundNode root = new(level: -1, taken: false);
46+
47+
// lastNodeOfOptimalPat --> last item in the optimal item sets identified by this algorithm
48+
BranchAndBoundNode lastNodeOfOptimalPath = root;
49+
50+
nodesQueue.Enqueue(root);
51+
52+
while (nodesQueue.Count != 0)
53+
{
54+
// parent --> parent node which represents the previous item, may or may not be taken into the knapsack
55+
BranchAndBoundNode parent = nodesQueue.Dequeue();
56+
57+
// IF it is the last level, branching cannot be performed
58+
if (parent.Level == items.Length - 1)
59+
{
60+
continue;
61+
}
62+
63+
// create a child node where the associated item is taken into the knapsack
64+
var left = new BranchAndBoundNode(parent.Level + 1, true, parent);
65+
66+
// create a child node where the associated item is not taken into the knapsack
67+
var right = new BranchAndBoundNode(parent.Level + 1, false, parent);
68+
69+
// Since the associated item on current level is taken for the first node,
70+
// set the cumulative weight of first node to cumulative weight of parent node + weight of the associated item,
71+
// set the cumulative value of first node to cumulative value of parent node + value of current level's item.
72+
left.CumulativeWeight = parent.CumulativeWeight + weightSelector(items[left.Level]);
73+
left.CumulativeValue = parent.CumulativeValue + valueSelector(items[left.Level]);
74+
right.CumulativeWeight = parent.CumulativeWeight;
75+
right.CumulativeValue = parent.CumulativeValue;
76+
77+
// IF cumulative weight is smaller than the weight capacity of the knapsack AND
78+
// current cumulative value is larger then the current maxCumulativeValue, update the maxCumulativeValue
79+
if (left.CumulativeWeight <= capacity && left.CumulativeValue > maxCumulativeValue)
80+
{
81+
maxCumulativeValue = left.CumulativeValue;
82+
lastNodeOfOptimalPath = left;
83+
}
84+
85+
left.UpperBound = ComputeUpperBound(left, items, capacity, weightSelector, valueSelector);
86+
right.UpperBound = ComputeUpperBound(right, items, capacity, weightSelector, valueSelector);
87+
88+
// IF upperBound of this node is larger than maxCumulativeValue,
89+
// the current path is still possible to reach or surpass the maximum value,
90+
// add current node to nodesQueue so that nodes below it can be further explored
91+
if (left.UpperBound > maxCumulativeValue && left.CumulativeWeight < capacity)
92+
{
93+
nodesQueue.Enqueue(left);
94+
}
95+
96+
// Cumulative weight is the same as for parent node and < capacity
97+
if (right.UpperBound > maxCumulativeValue)
98+
{
99+
nodesQueue.Enqueue(right);
100+
}
101+
}
102+
103+
return GetItemsFromPath(items, lastNodeOfOptimalPath);
104+
}
105+
106+
// determine items taken based on the path
107+
private static T[] GetItemsFromPath(T[] items, BranchAndBoundNode lastNodeOfPath)
108+
{
109+
List<T> takenItems = new();
110+
111+
// only bogus initial node has no parent
112+
for (var current = lastNodeOfPath; current.Parent is not null; current = current.Parent)
113+
{
114+
if(current.IsTaken)
115+
{
116+
takenItems.Add(items[current.Level]);
117+
}
118+
}
119+
120+
return takenItems.ToArray();
121+
}
122+
123+
/// <summary>
124+
/// Returns the upper bound value of a given node.
125+
/// </summary>
126+
/// <param name="aNode">The given node.</param>
127+
/// <param name="items">All items to choose from.</param>
128+
/// <param name="capacity">The maximum weight capacity of the knapsack to be filled.</param>
129+
/// <param name="weightSelector">
130+
/// A function that returns the value of the specified item
131+
/// from the <paramref name="items">items</paramref> list.
132+
/// </param>
133+
/// <param name="valueSelector">
134+
/// A function that returns the weight of the specified item
135+
/// from the <paramref name="items">items</paramref> list.
136+
/// </param>
137+
/// <returns>
138+
/// upper bound value of the given <paramref name="aNode">node</paramref>.
139+
/// </returns>
140+
private static double ComputeUpperBound(BranchAndBoundNode aNode, T[] items, int capacity, Func<T, int> weightSelector, Func<T, double> valueSelector)
141+
{
142+
var upperBound = aNode.CumulativeValue;
143+
var availableWeight = capacity - aNode.CumulativeWeight;
144+
var nextLevel = aNode.Level + 1;
145+
146+
while (availableWeight > 0 && nextLevel < items.Length)
147+
{
148+
if (weightSelector(items[nextLevel]) <= availableWeight)
149+
{
150+
upperBound += valueSelector(items[nextLevel]);
151+
availableWeight -= weightSelector(items[nextLevel]);
152+
}
153+
else
154+
{
155+
upperBound += valueSelector(items[nextLevel]) / weightSelector(items[nextLevel]) * availableWeight;
156+
availableWeight = 0;
157+
}
158+
159+
nextLevel++;
160+
}
161+
162+
return upperBound;
163+
}
164+
}
165+
}
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Algorithms.Knapsack
2+
{
3+
public class BranchAndBoundNode
4+
{
5+
// isTaken --> true = the item where index = level is taken, vice versa
6+
public bool IsTaken { get; }
7+
8+
// cumulativeWeight --> um of weight of item associated in each nodes starting from root to this node (only item that is taken)
9+
public int CumulativeWeight { get; set; }
10+
11+
// cumulativeValue --> sum of value of item associated in each nodes starting from root to this node (only item that is taken)
12+
public double CumulativeValue { get; set; }
13+
14+
// upperBound --> largest possible value after taking/not taking the item associated to this node (fractional)
15+
public double UpperBound { get; set; }
16+
17+
// level --> level of the node in the tree structure
18+
public int Level { get; }
19+
20+
// parent node
21+
public BranchAndBoundNode? Parent { get; }
22+
23+
public BranchAndBoundNode(int level, bool taken, BranchAndBoundNode? parent = null)
24+
{
25+
Level = level;
26+
IsTaken = taken;
27+
Parent = parent;
28+
}
29+
}
30+
}

DIRECTORY.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
## Knapsack problem
1414
* [Naive solver](https://github.com/TheAlgorithms/C-Sharp/blob/master/Algorithms/Knapsack/NaiveKnapsackSolver.cs)
1515
* [Dynamic Programming solver](https://github.com/TheAlgorithms/C-Sharp/blob/master/Algorithms/Knapsack/DynamicProgrammingKnapsackSolver.cs)
16+
* [Branch and bound solver](https://github.com/TheAlgorithms/C-Sharp/blob/master/Algorithms/Knapsack/BranchAndBoundKnapsackSolver.cs)
1617
## Linear Algebra
1718
* Eigenvalue
1819
* [Power Iteration](https://github.com/TheAlgorithms/C-Sharp/blob/master/Algorithms/LinearAlgebra/Eigenvalue/PowerIteration.cs)

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This repository contains algorithms and data structures implemented in C# for ed
2222
* [Knapsack problem](./Algorithms/Knapsack)
2323
* [Naive solver](./Algorithms/Knapsack/NaiveKnapsackSolver.cs)
2424
* [Dynamic Programming solver](./Algorithms/Knapsack/DynamicProgrammingKnapsackSolver.cs)
25+
* [Branch and bound solver](./Algorithms/Knapsack/BranchAndBoundKnapsackSolver.cs)
2526
* [Linear Algebra](./Algorithms/LinearAlgebra)
2627
* [Eigenvalue](./Algorithms/LinearAlgebra/Eigenvalue)
2728
* [Power Iteration](./Algorithms/LinearAlgebra/Eigenvalue/PowerIteration.cs)

0 commit comments

Comments
 (0)