Last active
September 29, 2024 14:24
-
-
Save USM-F/1287f512de4ffb2fb852e98be1ac271d to your computer and use it in GitHub Desktop.
Dynamic programming solution of Multiple-Choice Knapsack Problem (MCKP) in Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#groups is list of integers in ascending order without gaps | |
def getRow(lists, row): | |
res = [] | |
for l in lists: | |
for i in range(len(l)): | |
if i==row: | |
res.append(l[i]) | |
return res | |
def multipleChoiceKnapsack(W, weights, values, groups): | |
n = len(values) | |
K = [[0 for x in range(W+1)] for x in range(n+1)] | |
for w in range(W+1): | |
for i in range(n+1): | |
if i==0 or w==0: | |
K[i][w] = 0 | |
elif weights[i-1]<=w: | |
sub_max = 0 | |
prev_group = groups[i-1]-1 | |
sub_K = getRow(K, w-weights[i-1]) | |
for j in range(n+1): | |
if groups[j-1]==prev_group and sub_K[j]>sub_max: | |
sub_max = sub_K[j] | |
K[i][w] = max(sub_max+values[i-1], K[i-1][w]) | |
else: | |
K[i][w] = K[i-1][w] | |
return K[n][W] | |
#Example | |
values = [60, 100, 120] | |
weights = [10, 20, 30] | |
groups = [0, 1, 2] | |
W = 50 | |
print(multipleChoiceKnapsack(W, weights, values, groups)) #220 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@USM-F incorrect regarding a best case reduction to O(Wn^2). Here is a much, much improved version of this original gist with code readability updated. It accurately predicts the runtime before beginning (on my machine, you'll obviously have to tweak the magic number on your machine for whatever IPC you get, or whatever. Allows you to throw an error and use a less optimal algorithm if you want, in time sensitive applications).
This is O(capacity * n * maxGroupSize).
It has an average runtime of O(capacity * n * sqrt(maxGroupSize)) (honestly, I can't explain why, I would have expected * avgGroupSize. But for whatever reason, this trends towards sqrt(maxGroupSize). If someone figures it out, do explain, because it sure looks like it should be avgGroupSize. But the emprical estimations do not lie, this trends towards sqrt).
It also outputs the actual items that were included in the max solution (this is trickier than 0-1 knapsack, if you try that solution work back it will incorrectly think multiple items from same group are included; a single additional check for skipping to the next group must be added).
There were two key performance improvements.
First, the getRow method in the original here created completely unnecessary sublists. I replaced that with direct indexes into K for a 4x speedup.
Second,
was not necessary and can be optimized into a precomputed set of group start and end indexes so that that n^2 becomes
which has two upsides; one it causes the groupCount=1 case to become a single scan and run hundreds of times faster than it otherwise would have with the n^2, but also it brings that n^2 down to n*maxGroupSize worst case.
Some runtime comparisons AFTER the getRow 4x deletion but before the n^2 reduction, vs the new runtime:
capacity 750 with 200 items = 570ms (now 80ms)
capacity 750 with 100 items = 138ms (now 27ms)
capacity 75 with 400 items = 138ms (now 20ms)
capacity 75 with 400 items, avg weight 10 = 217ms (now 15ms)
capacity 75 with 400 items, avg weight 2 = 246ms (now 20ms)
Enjoy:
Here, have free tests, too: