0/1 Knapsack using Least Cost Branch and Bound

The 0/1 Knapsack problem is a classic combinatorial optimization problem where you are given a set of items, each with a weight and a value, and the goal is to determine the maximum value that can be obtained by selecting a subset of these items while ensuring that the total weight does not exceed a given capacity.

The Least Cost Branch and Bound (LCBB) is a technique used to solve combinatorial optimization problems, including the 0/1 Knapsack problem. It combines the principles of branch and bound with a heuristic that guides the exploration of the solution space.

Let us implement the code:

CODE:

Output:

Explanation:

Item Structure:

Item is a user-defined structure with two attributes: weight and value. Each instance of an Item represents an item in the knapsack problem.

Comparison Function:

compareItems is a function used to compare two items based on their value-to-weight ratio. It takes two Item objects as parameters and returns a boolean indicating whether the first Item has a higher percentage than the second. This function is used as a criterion for sorting items.

Calculate Upper Bound:

calculateUpperBound estimates the upper bound for a given node in the search tree. It takes the list of items, the remaining capacity, current weight, current value, and the current Item's index as parameters. The function iterates through the remaining items, adding their values to the upper bound until the capacity is exhausted or all items are considered. It also adds the fractional part of the next Item if it doesn't fully fit.

Knapsack Branch and Bound:

knapsackBnB is the main recursive function for the branch and bound algorithm. It takes parameters representing the current state of the Knapsack, the current node in the search tree, the current best-known solution (maxValue), and a vector (selected items) to keep track of the indices of selected items. The function explores two branches at each node: one where the current Item is included and one where it is excluded. Pruning is performed by comparing the upper bound of a node with the current best-known solution. If the upper bound is less, the branch is pruned.

Base Case Handling:

The base case of the recursion is reached when all items are considered (currentIndex == items.size()), or the knapsack capacity is reached (current weight == capacity). In the base case, the function checks if the current solution is better than the current best-known solution (maxValue). If it is, the maxValue is updated, and the indices of selected items are stored in the selectedItems vector.

Sorting Items:

Before starting the algorithm, the items are sorted based on the value-to-weight ratio using the compareItems function. Sorting is essential for the Least Cost Branch and Bound approach as it determines the order in which nodes are explored.

Main Function:

In the main function, a sample set of items and knapsack capacity is defined. The items are sorted using the compareItems function to prioritize exploration based on cost. The knapsackBnB function is called with the sorted items and other necessary parameters.

Output Display:

After the algorithm completes its execution, the optimal value and the indices of selected items are displayed. The selected items represent the solution to the 0/1 Knapsack problem.

Let us discuss the time and space complexity of the code:

Time Complexity:

The algorithm's time complexity is affected by the number of nodes explored in the search tree.

Worst Case: In the worst case, the algorithm explores all possible combinations of items, corresponding to the number of nodes in a binary tree with a depth of n (number of items). The worst-case time complexity is exponential, O(2^n).

Average Case: The average time complexity depends on the efficiency of the branch and bound pruning. Sorting the items based on the value-to-weight ratio takes O(n log n) time.

The average case time complexity is often better than the worst case due to pruning, but it can still be exponential in certain scenarios.

Space Complexity:

The space complexity is determined by the additional space required for recursive calls, the stack, and the data structures used.

Stack Space: The depth of the recursion corresponds to the height of the search tree.

In the worst case, the algorithm may use a stack space proportional to the height of the tree, O(n).

The selectedItems vector is used to store the indices of selected items. The space required by the vector is proportional to the number of selected items, which can be at most n. The sorting of items also requires additional space, but it is typically overshadowed by the recursive stack space.

Overall:

The overall space complexity is O(n) due to the stack space and the selectedItems vector.

Applications of 0/1 Knapsack:

Finance:

Portfolio optimization where items represent financial assets, and the Knapsack represents the budget or risk tolerance.

Resource Allocation:

Allocating resources such as time, budget, or manpower to maximize overall value.

Logistics:

Selecting items with weights and values representing shipments to optimize cargo loading in transportation.

Another technique to Solve this problem:

Dynamic Programming Approach: The 0/1 Knapsack problem can be efficiently solved using dynamic programming. The dynamic programming approach involves building a table to store intermediate solutions and gradually solving larger subproblems. This method has a time complexity of O(nW), where n is the number of items and W is the capacity of the Knapsack.

Fractional Knapsack:

In the Fractional Knapsack problem, items can be divided to maximize the total value. This problem is generally easier to solve compared to the 0/1 Knapsack problem. The Greedy Algorithm is commonly used for Fractional Knapsack, where items are sorted by their value-to-weight ratios, and the highest ratio items are selected until the Knapsack is full.