What is wrong with my implementation of the knapsack problem [closed] - python

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 8 months ago.
Improve this question
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.

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

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

How can I optimize my Python code to run faster? [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 7 months ago.
The community reviewed whether to reopen this question 7 months ago and left it closed:
Original close reason(s) were not resolved
Improve this question
Define an element of a list of items to be a dominator if every element to its right (not just the one element that is immediately to its right) is strictly smaller than that element. Note how by this definition, the last item of the list is automatically a dominator. This function should count how many elements in items are dominators, and return that count. For example, the dominators of the list [42, 7, 12, 9, 13, 5] would be the elements 42, 13 and 5. The last element of the list is trivially a dominator, regardless of its value, since nothing greater follows it.
I am working on this practice problem and have a code that works, however it's quite slow. How can I best optimize this to run faster?
def count_dominators(items):
item_count = len(items)
count = 0
if item_count==0:
return count;
count+=1;
if item_count==1:
return count;
for i in range(0,len(items)-1):
flag=1
for j in range(i+1,len(items)):
if(items[j]>=items[i]):
flag=0;
break;
if(flag==1):
count+=1;
return count
import numpy as np
def count_dominators(items):
dominators = np.unique(np.maximum.accumulate(seq[::-1]))
return dominators.size, dominators
seq = [42, 7, 12, 9, 13, 5]
print(count_dominators(seq))
>>>(3, array([5, 13, 42]))
Iterating in reverse order will reduce this from a O(n²) to O(n) as you'll keep a running maximum as you go, without needing to rescan all trailing elements for each new element. So without relying on complex utilities you didn't write, you can just do:
def count_dominators(items):
max_so_far = float('-inf') # Smaller than any value to it's replaced immediately,
# identifying first element as dominator by definition
count = 0
for item in reversed(items): # Iterate in reverse order so we only need to know max
# so far to identify new dominators
if item > max_so_far:
max_so_far = item
count += 1
return count
If you want more magic to do less of the work yourself, Python's itertools module provides an accumulate function that can repeatedly perform an operation on elements as it goes, yielding the result so far. If you use max as the operation, then it generates a stream of numbers that changes only when you see a new dominator. Convert the result to a set or use itertools.groupby (which is roughly equivalent to the uniq command line utility when run without arguments, making it a cheap way to deduplicate adjacent duplicates) to dedupe, and the length of the set or the number of groups is the dominator count:
from itertools import accumulate, groupby
def count_dominators(items):
return sum(1 for _, _ in groupby(accumulate(reversed(items), max)))
or at the cost of a technically unnecessary, growing for each new dominator, set (likely faster, as itertools.groupby has more overhead than you might expect, and the number of elements collected after deduplication is likely small enough to be irrelevant):
from itertools import accumulate
def count_dominators(items):
return len(set(accumulate(reversed(items), max)))
You could replace the sum(1 for _, _ in (and the matching trailing parenthesis) with a more efficient recipe for counting elements in an iterator if you like (though it hardly matters unless the inputs are huge).
Assuming there are only positive numbers, you can run on a list from its end and mark the biggest number you've met. It will be O(n) complexity, instead of O(n^2) that you've suggested.
Something like that shall work:
def count_dominators(items):
count = 0
last_biggest = -1
for i in range(len(items)-1,-1,-1):
if items[i] > last_biggest:
last_biggest = items[i]
count += 1
return count

Check if preceding objects exist [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 10 months ago.
Improve this question
I need to write a method in Python to check that if a key is provided by the user, the preceding key must also be provided.
Example: If the user provides a list ['id_4','id_5','id_6'...], this will throw an error because the user did not provide the preceding keys, 'id_1','id_2','id_3'.
A valid list: ['id_1','id_2','id_3','id_4','id_5','id_6']
An invalid list: ['id_6'] (missing id_1 to id_5)
Note: this could be done with a dictionary or set, just curious if there are more elegant solutions out there.
If the preceding key must be in the list for each key, by recurrence this means that all key from the start must be present.
I think an ideal tool to validate this is a set:
def valid(l):
return set(l) == {f'id_{i}' for i in range(1, len(l)+1)}
# if you prefer to throw an exception, use
# assert set(l) == {f'id_{i}' for i in range(1, len(l)+1)}
valid(['id_4','id_5','id_6'])
# False
valid(['id_1','id_2','id_3','id_4'])
# True
IDs in order
If the IDs also need to be sorted, you can compare the pairs. For this itertools.pairwise can help (NB. pairwise requires python ≥ 3.10, for previous versions, check the recipe in the doc)
def valid(l):
return (l and l[0] == 'id_1'
and all(a+1 == b for a,b in
pairwise(int(x.rpartition('_')[-1])
for x in l))
)
valid(['id_4','id_5','id_6'])
# False
valid(['id_1','id_2','id_3','id_4'])
# True
valid(['id_1','id_3','id_2'])
# False
Cyclic sort if no duplicates are allowed
If you cannot use additional space (sets, dictionaries), then you can in-place sort, via cyclic sort.
This will be o(n) complexity with o(1) space.
Because element 1 -> n must be in a list of size n, we can assume if an item is greater than n, the list is not valid.
Then we can try and check for duplicates.
By the end, all elements 1-> n must be in the list of size n with no duplicates which satisfies your requirement. This solution is better than the dictionary/set approach as it uses no additional space.
def cyclicSort(l):
def getNumFromID(id):
return int(id[3:])
i = 0
while i < len(l):
correctIndex = getNumFromID(l[i]) - 1
if correctIndex > len(l):
return False
if i != correctIndex:
numberAtCorrectIndex = getNumFromID(l[correctIndex])
if numberAtCorrectIndex > len(l):
return False
if numberAtCorrectIndex != l[i]:
if l[correctIndex] == l[i]:
# duplicate found
return False
#swap the numbers after confirming neither are out of bounds
l[correctIndex], l[i] = l[i], l[correctIndex]
else:
i += 1
return True
print(cyclicSort(['id_1','id_2','id_3','id_4','id_5','id_6']))
print(cyclicSort(['id_1']))
print(cyclicSort(['id_6']))
print(cyclicSort(['id_2','id_3','id_4','id_5','id_6','id_1']))
print(cyclicSort(['id_1','id_2','id_3','id_4','id_5','id_5']))
Sorted list approach checking preceding elements##
If the list is already sorted, then
def checkPreceding(l):
id = 1
baseStr = "id_"
for idToCheck in l:
if idToCheck != baseStr + str(id):
return False
id += 1
return True
print(checkPreceding(['id_1','id_2','id_3','id_4','id_5','id_6']))
print(checkPreceding(['id_1']))
print(checkPreceding(['id_6']))
If the list is sorted, it's better to use checkPreceding approach since you will only need O(1) space.
However, if it is not sorted, you can use a dictionary/set for o(n) complexity with o(n) space or cyclic sort for o(1) space.

int is not an interable? [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
trying to make an average function myself as requested by a book I'm using to learn python. This is what I have and I need it to basically have a requirement of at least 1 argument to avoid division by zero, but to accept any amount of other arguments. But I keep getting this error. Here's my code. Any help is appreciated!
1 def average(x, *args):
----> 2 return sum(x, *args) / len(x, *args)
3
TypeError: 'int' object is not iterable
sum() and len() only take in iterables, which are objects that python can iterate through, like strings, lists, sets, etc.
You need to close the integers with brackets, so it would be a list, and then, you can put the iterable into sum() and len().
def average(x, *args):
return sum([x, *args]) / len([x, *args])
print(average(1, 4, 2, 6))
Output:
3.25
Neither sum nor len take an arbitrary number of arguments. If you want average to do so, you need to pass them all as a single iterable to sum and as a single object (that supports the length protocol) to len.
def average(x, *args):
all_nums = [x]
all_nums.extend(args)
return sum(all_nums)/len(all_nums)
(I suppose all_nums = [x, *args] would work as well; I spend too much time in Python 2 for that to feel natural yet.)
If you wanted average to be more like sum and take an arbitrary iterable, it would be easier to compute the sum and length in parallel, rather than use sum directly (since len does not work with arbitrary iterables)
def average(nums):
length = 0
total = 0
for x in nums:
length += 1
total += x
return total/length

how exactly is the recursion working in this code [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 4 years ago.
Improve this question
I have this code , and it is not mine , I saw it online and i am wondering how is the recursion working , can someone please explain !
(this function accepts a list of integers(distances) and a value 'r' lets say 10) and it returns all the possibilities of how we can reach 100 using the distances) lets say the list is [3,5,2,5] and the value r is 10 ! so to make 10 we need [5,5] or [3,2,5] ,this is the code:
def greedy(r, distances):
if r < 0:
return []
if r == 0:
return [[]]
solutions = []
for last_distance in d:
combos = greedy(r - last_distance, d)
for combo in combos:
combo.append(last_distance)
if(not solutions.__contains__(combo)):
solutions.append(combo)
return solutions
i hope i made my self clear
I'd suggest using python print function
print("")
At multiple locations to actually see the recursion happening. You would get a better idea than someone trying to explain it to you.
Although the function itself is quite simple. For every element in distances, the function subtracts the element from the required r value and checks the value with the if conditions.
If r value is zero ie, the for some elements in distance, their sum is r, a multi list is returned, which furthermore has the said elements.
At the end, the complete list of elements whose sum adds to r are appended to solution list and returned.
Say that given a list distances0 of length n, you are able to return all tuples i1,..,infrom the list that sums to a number r for any number r.
You would like to do the same for a list distances of length n+1.
Since you know how to solve the problem for a list of size n, you will do the following: for every element last_distance of distances, return all tuples i1,...,in that sums to r-last_distance from a list distances0 which is similar to distances but without the element last_distance (and hence of length n).
I think there might be some mistakes in the code, here should be a working version:
def greedy(r, distances):
if r < 0:
return []
if r == 0:
return [[]]
solutions = []
for i in range(len(distances)):
d = distances[:i]+distances[i+1:]
last_distance = distances[i]
combos = greedy(r - last_distance, d)
for combo in combos:
combo.append(last_distance)
if(not solutions.__contains__(combo)):
solutions.append(combo)
return solutions

Implementation of quick select in python. function is unstable and returns different values [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
I tried to implement quick select to find the m'th smallest number in the list. When I run the program it returns the correct values sometime and incorrect values other times on the same array. What am I doing wrong her is the code
def select_mth_smallest(A, m):
pivot = np.random.choice(A)
# split into 3 partitions
A1 = []
A2 = []
A3 = []
for item in A:
if item < pivot:
A1.append(item)
if item > pivot:
A3.append(item)
else:
A2.append(item)
#find where m'th largest element is and recurse accordingly
if len(A1) >= m:
return select_mth_smallest(A1, m)
elif (len(A1) + len(A2)) >= m:
return pivot
else:
return select_mth_smallest(A3,m - (len(A1)+len(A2)))
Here is an input where the algorithm fails.
A = [1,2,3,4,5]
select_mth_smallest(A,5)
When I repeatedly execute this above statement I get, 5(correct) and 4(incorrect) alternatingly.
One thing that particularly baffles me (I am new to python) is that why I get different return values for the function when repeated with the same input. Looks rather sporadic.. BTW I am using Jupyter
You are adding some items to multiple partitions.
if item < pivot:
A1.append(item)
if item > pivot:
A3.append(item)
else:
A2.append(item)
A1 is the set of items less than the pivot. A3 is the set of items greater than the pivot. A2, however, is the set of items less than or equal to the pivot, because the 2nd if statement executes for all items, and one or the other branch will execute.
You want one, single if statement with an elif and else clause here.
if item < pivot:
A1.append(item)
elif item > pivot:
A3.append(item)
else:
A2.append(item)

Categories

Resources