Home > Mobile >  What is wrong with my implementation of the knapsack problem
What is wrong with my implementation of the knapsack problem

Time:06-19

items = [[profit, weight]...]

items = [[44,92], [46,4], [90,43], [72,83], [91,84], [40,68], [75,92], [35,82], [8,6], [54,44], [78,32], [40,18], [77,56], [15,83], [61,25], [17,96], [75,70], [29,48], [75,14], [63,58]]
max_weight = 269
def knapsack_bruteforce(items, max_weight):
    def backtrack(i, curr_profit, curr_weight):       
        if(i 1 >= len(items) or curr_weight   items[i 1][1] > max_weight):
            return curr_profit
            
        return max(backtrack(i 1, curr_profit   items[i 1][0], curr_weight   items[i 1][1]), backtrack(i 1, curr_profit, curr_weight))
        
    return backtrack(-1, 0, 0)

knapsack_bruteforce(items, max_weight) should return 550 as the maximum profit but I'm getting 528 instead.

CodePudding user response:

The problem is in the second part of the if condition:

if(i 1 >= len(items) or curr_weight   items[i 1][1] > max_weight):
    return curr_profit

When the second condition is true, you should still allow the second recursive call to be done -- the one where this weight is not included -- as there might still be a way to add another item (that has less weight). But as you return immediately here, that attempt is never made.

Without changing more than necessary to your code, you can fix this, by bailing out (returning a negative number) when the weight excess has already been made. So split your if into two:

if curr_weight > max_weight:
    return -1
if i 1 >= len(items):
    return curr_profit

CodePudding user response:

The problem is that you aren't exhaustively trying all the combinations; each time you remove an item, your recursive call needs to consider all the other items, not just the items after the one you removed.

Here's an example of a working brute-force solution; I added a functools.cache decorator (which requires immutable inputs, hence a tuple of tuples instead of a dict of lists) so I wouldn't have to wait all day for it to return the answer and make sure it works, but it should return the same answer without the memoization (just a lot more slowly). Memoization makes it go faster because otherwise you end up re-computing the exact same intermediate answers (arrived at in slightly different orders) quite a few times.

The thing to pay attention to is that in that max call we're iterating over all of items, and doing the recursive call with items[:i] items[i 1:], i.e. all the items except the i-th item.

from functools import cache

@cache
def knapsack(items: tuple[tuple[int, int], ...], max_weight: int) -> int:
    if all(weight > max_weight for _profit, weight in items):
        return 0  # can't add anything to the sack

    return max(
        profit   knapsack(items[:i]   items[i 1:], max_weight - weight)
        for i, (profit, weight) in enumerate(items)
        if weight <= max_weight
    )


print(knapsack((
    (44,92), (46,4), (90,43), (72,83), (91,84), (40,68), (75,92),
    (35,82), (8,6), (54,44), (78,32), (40,18), (77,56), (15,83),
    (61,25), (17,96), (75,70), (29,48), (75,14), (63,58)
), 269))  # 550
  • Related