Daily fantasy lineups contain a set number of players: for example, DraftKings has 1 QB, 2 RB, 3 WR, 1 TE, 1 flex, and 1 defense. This is the perfect setup for the multiple-choice knapsack problem (see http://www.diku.dk/users/pisinger/95-1.pdf).
We will use a modified version of the dynamic programming method used to solve the regular knapsack problem in order to solve the multiple-choice knapsack problem. We deal with the problem of flex picks by solving problems with restrictions of 3 RB, 4WR, and 2 flex, respectively, and then picking the highest-scoring solution.
The code for the algorithm is as follows. The remove_dom function is an auxiliary used to remove dominated items to speed up the algorithm and return an approximation. We do not use it.
# objects is an array of items, each item has a name, weight, and price def remove_dom(objects): # sort objects in increasing order of weight objects = objects[objects[:,1].argsort()] # iterate through all pairs of objects, if there is an object i such that w_i>w_j but p_i<p_j, # remove object i because it is dominated by object j new_objects = np.empty([len(objects[0])]) for i in range(len(objects)): is_dom = False for j in range(i): if objects[i][2] < objects[j][2]: is_dom = True if not is_dom: new_objects = np.vstack((new_objects,objects[i])) return new_objects[1:] class Contents: items = [] types = [] value = 0 def __init__(self, num_types): self.types = [0 for i in range(num_types)] def add(self, object_num, object_value, object_type): new_contents = Contents(len(self.types)) new_contents.items = list(self.items) new_contents.items.append(object_num) new_contents.types = list(self.types) new_contents.types[object_type] += 1 new_contents.value = self.value new_contents.value += object_value return new_contents # objects array has form [name, weight, value, type] # cat_indices is a list of the last indices for each category; for example, if cat_indices=[1,2], # item 0 would be in category 0, item 1 would be in category 1, and the rest of the items would be in category 2 # cap_cap is a list of the capacity, or maximum number of items, in each category def multi_knapsack(objects, weight_limit, cat_indices, cat_cap): assert len(cat_indices) == len(cat_cap) cat = len(cat_indices) # number of categories # don't remove dominated objects; this makes it harder to exactly fulfill the salary cap ''' split_arr = np.split(objects, cat_indices) for i in range(len(split_arr)-1): split_arr[i] = remove_dom(split_arr[i]) after_index = len(split_arr[i]) # update cat_indices to account for dropped items if i==0: cat_indices[i] = after_index else: cat_indices[i] = cat_indices[i-1] + after_index new_objects = np.vstack(split_arr[:-1]) ''' new_objects = objects max_array = [[[Contents(len(cat_cap)) for k in range(cat_cap[int(new_objects[j][3])]+1)] for i in range(weight_limit+1)] for j in range(len(new_objects))] max_array = [[[Contents(len(cat_cap))] for i in range(weight_limit+1)]] + max_array for i in range(len(new_objects)): weight = int(new_objects[i][1]) value = int(new_objects[i][2]) object_type = int(new_objects[i][3]) last_same_type = False if i != 0: last_same_type = object_type == int(new_objects[i-1][3]) for w in range(1,weight_limit+1): for n in range(cat_cap[object_type]+1): if weight max_array[i][w][n].value: max_array[i+1][w][n] = max_array[i][w-weight][n-1].add(i,value,object_type) else: max_array[i+1][w][n] = max_array[i][w][n] elif n==1: max_array[i+1][w][n] = max_array[i][w-weight][-1].add(i,value,object_type) else: if last_same_type: max_array[i+1][w][n] = max_array[i][w][n] else: max_array[i+1][w][n] = max_array[i][w][-1] return [new_objects[i][0] for i in max_array[len(new_objects)][weight_limit][-1].items]