Problem solving question: knapsack variation with two variables and different total - python

I'm trying to figure out the solution to this problem, which is quite similar to the knapsack problem, but I'm not sure what states I should have or how to memorize them.
You have an electric car which weighs W units and you want to make it go for as long as possible. To do this you must pick from N batteries which also have an energy e, a weight b and a cost c.
The amount of time your car can go for is t = Etotal / Wtotal (the sum of energies of batteries you chose divided by the sum of the weight of the batteries you chose + the weight of the car itself)
Given that you have a budget B, what is the maximum time your car can go for?
Example:
INPUT:
N = 10 /number of batteries to choose
B = 1000 /budget
W = 20 /weight of car
#N batteries with numbers e (energy), w (weight), c (cost)
40 40 40
1 1 1
70 30 60
100 20 700
80 50 200
30 1 200
100 100 1
20 1 500
30 20 100
70 50 100
OUTPUT:
3.17073170731707

Straightforward DP algorithm
We can compute the minimum cost f(i, j, k) of a solution that achieves exactly total energy j and total weight k by choosing some subset of the first i batteries. This is given by:
f(0, 0, W) = 0
f(0, j!=0, W) = INF
f(0, j, k!=W) = INF
f(i>0, j, k<W) = INF
f(i>0, j, k>=W) = min(f(i-1, j, k), f(i-1, j-E[i], k-W[i]) + C[i])
where E[i], W[i] and C[i] are the energy, weight and cost of battery i, respectively. After computing values of this function for all 0 <= i <= N, 0 <= j <= Sum(E[]) and 0 <= k <= W+Sum(W[]), find the maximum of j/k over all 0 <= j <= Sum(E[]) and 0 <= k <= W+Sum(W[]) such that f(N, j, k) <= B.
A direct implementation using a 3D DP table will take time and space O(N*Sum(E[])*(W+Sum(W[]))) time and space. But since the recursion never needs to reach back further than 1 step in the first parameter i, we can make the outermost loop increase i and drop the first dimension from the DP table, overwriting its entries as we go, to drop the space complexity by a factor of N.
The above DP computes minimum costs, but it could be "rotated" to optimise for any of the three variables (minimum cost for given energy and weight, maximum energy for given cost and weight, or minimum weight for given energy and cost). The most efficient approach is to optimise for the variable with the largest range, since the time and space complexity involve the product of the ranges of the remaining two variables.
Greedy algorithm for unconstrained costs
The following simple O(N*log N)-time, O(N)-space algorithm maximises the distance travelled if there are no cost constraints. I think it's interesting because of the proof of correctness.
Sort batteries in decreasing order by energy divided by weight (you could think of this as "energy density").
Keep adding batteries from this list until the next battery has energy/weight less than the (total energy)/(total weight) of the batteries (and car) chosen so far.
A key element in proving this correct is the observation that, whenever we combine two multisets of batteries (we can consider the car to be an always-chosen battery with energy level 0), the mean of the resulting multiset is strictly in between the original two means. I'll call this the "mean-betweenness" lemma; see Lemma 1 here for a proof. Intuitively this means (hehe) that whenever we can add a battery with higher energy density than the multiset of batteries chosen so far, we should -- since the result of combining these two multisets (the new battery is a multiset of size 1) will be strictly in between them, and thus strictly higher than the multiset of batteries chosen so far.
Running the algorithm above will choose a multiset of batteries in which some initial number s of batteries in the sorted list will be chosen, and no other batteries will be chosen. By the mean-betweenness lemma, the algorithm clearly chooses an optimal multiset of solutions among all solutions having this form (that is, among solutions that choose only some initial number of batteries in the list). To establish that it chooses an optimal solution overall, we need to show that no solution that "skips over" one or more batteries in this list and then chooses one or more batteries further down can be better.
Suppose to the contrary that there exists an optimal solution X that skips a battery, and that this solution is strictly better than the solution Y produced by the greedy algorithm. Let i be the first battery that X skips. Thus X and Y share the first i-1 batteries. There are 2 cases:
E[i]/W[i] is strictly greater than the energy/weight of X. In this case, by the mean-betweenness lemma, we can add battery i to X to produce a solution that is strictly better than X, contradicting the optimality of X.
E[i]/W[i] is less than or equal to the energy/weight of X.
Continuing with case 2, consider the submultiset X' of batteries chosen further down the list by X (by assumption this must contain at least one battery). Because the list is ordered by decreasing energy/weight, these batteries each have energy/weight at most equal to that of battery i (namely, E[i]/W[i]), so by the mean-betweenness lemma their mean energy/weight is also at most equal to E[i]/W[i]. X = (X-X') ∪ X', so by the mean-betweenness lemma, the mean energy/weight of X is strictly between (X-X') and X'. Since the mean energy/weight of X' is less than or equal to the mean energy/weight of X overall, removing the batteries in X' from X to leave (X-X') will in the best case (when the means of X and X' are equal) leave the mean unchanged, and otherwise increase it. Either way, we have constructed a new solution (X-X') with mean energy/weight at least as high as X and which consists of the first i-1 batteries in the list -- that is, a solution of the form that the greedy algorithm is known to maximise over.

Related

Estimate computational cost of a sorting algorithm

Today, my algorithm teacher gave me a little exercise in introducing computational cost. The code is as follows:
A = [8,7,6,5,4,3,2,1]
for y in range (0, len(A)):
el = A[y]
i = y-1
while i >= 0 and A[i] > el:
A[i+1] = A[i]
i = i-1
A[i+1] = el
Without wasting your time: it is an algorithm that takes an array and reorders it. I have to find out what order is O. Considering that all assignment operations use 1 as a cost, the "heaviest" lines are the for and the while. If the for loop is of the order of O (n) with n = len (A) I can't figure out how to calculate the while. Worst case it runs 28 times, but I can't find a correlation with the length of the array. Can someone help me? Many thanks in advance.
For the given input the condition A[i] > el will always be true when it gets evaluated, as every next value el is less than all preceding values A[i]. So the input really triggers a worst case execution.
Then the number if executions of the inner loop increases with one every time the outer loop makes an iteration:
0
1
2
3
...
n-1
The sum of all these is a triangular number: n(n-1)/2, which is O(n²)
The while loop inserts A[y] (for some y) in the subarray A[0..y-1] before it. As you can verify, when performed on increasing y, this makes A[0..y] sorted. The cost of the while loop is proportional to the distance between y and the insertion point. At best, this distance is always 1 (A[y] already in place); at worst, it is y (A[y] should come first, as on the given input); on average, y/2 for a uniform distribution.
Hence, globally, the sort is at best Θ(n), but at worst and on average Θ(n²). (Sum of integers up to n.)

Finding minimum number of jumps increasing the value of the element

Optimizing a leetcode-style question - DP/DFS
The task is the following:
Given N heights, find the minimum number of suboptimal jumps required to go from start to end. [1-D Array]
A jump is suboptimal, if the height of the starting point i is less or equal to the height of the target point j.
A jump is possible, if j-i >= k, where k is the maximal jump distance.
For the first subtask, there is only one k value.
For the second subtask, there are two k values; output the amount of suboptimal jumps for each k value.
For the third subtask, there are 100 k values; output the amount of suboptimal jumps for each k value.
My Attempt
The following snippet is my shot at solving the problem, it gives the correct solution.
This was optimized to handle multiple k values without having to do a lot of unnecessary work.
The Problem is that even a solution with a single k value is o(n^2) in the worst case. (As k <= N)
A solution would be to eliminate the nested for loop, this is what I'm uncertain about how to approach it.
def solve(testcase):
N, Q = 10, 1
h = [1 , 2 , 4 ,2 , 8, 1, 2, 4, 8, 16] # output 3
# ^---- + ---^ 0 ^--- + --^ + ^
k = [3]
l_k = max(k)
distances = [99999999999] * N
distances[N-1] = 0
db = [ [0]*N for i in range(N)]
for i in range(N-2, -1, -1):
minLocalDistance = 99999999999
for j in range(min(i+l_k, N-1), i, -1):
minLocalDistance = min(minLocalDistance, distances[j] + (h[i] <= h[j]))
db[i][j] = distances[j] + (h[i] <= h[j])
distances[i] = minLocalDistance
print(f"Case #{testcase}: {distances[0]}")
NOTE: This is different from the classic min. jumps problem
Consider the best cost to get to a position i. It is the smaller of:
The minimum cost to get to any of the preceding k positions, plus one (a suboptimal jump); or
The minimum cost to get to any of the lower-height position in the same window (an optimal jump).
Case (1) can be handled with the sliding-window-minimum algorithm that you can find described, for example, here: Sliding window maximum in O(n) time. This takes amortized constant time per position, or O(N) all together.
Case (2) has a somewhat obvious solution with a BST: As the window moves, insert each new position into a BST sorted by height. Remove positions that are no longer in the window. Additionally, in each node, store the minimum cost within its subtree. With this structure, you can find the minimum cost for any height bound in O(log k) time.
The expense in case 2 leads to a total complexity of O(N log k) for a single k-value. That's not too bad for complexity, but such BSTs are somewhat complicated and aren't usually provided in standard libraries.
You can make this simpler and faster by recognizing that if the minimum cost in the window is C, then optimal jumps are only beneficial if they come from predecessors of cost C, because cost C+1 is attainable with a sub-optimal jump.
For each cost, then, you can use that same sliding-window-minimum algorithm to keep track of the minimum height in the window for nodes with that cost. Then for case (2), you just need to check to see if that minimum height for the minimum cost is lower than the height you want to jump to.
Maintaining these sliding windows again takes amortized constant time per operation, leading to O(N) time for the whole single-k-value algorithm.
I doubt that there would be any benefit in trying to manage multiple k-values at once.

Calculate element which is most divisible by all elements in the same array (better than O(n^2)) [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 1 year ago.
Improve this question
I have done this example in O(n^2). Given a array, I do the following:
max_key = 0
for k in set(keys):
count = 0
for divisor in keys:
if key < divisor: break
if key% divisor == 0: count += 1
if count > max_key: max_key = count
print(max_key)
An example of this would be:
keys = [2,4,8,2]
Then the element most divisible by all elements in the keys is 8 because there are 4 elements (2,2,4,8) that can divide 8.
Can anyone suggest an approach better than O(n^2) ?
keys = [2,4,5,8,2]
We can try something like memoization (from dynamic programming) to speed up while not doing any repeated calculations.
First, let's keep a hashmap, which stores all the divisors for a number in that array.
num_divisor = {} # hashmap
We keep another hashmap to store if a value is present in the array or not (count of that number).
cnt_num = {2: 2, 4: 1, 5: 1, 8: 1}
Now, we run prime sieve up to max(keys) to find the smallest prime factor for each number up to max(keys).
Now, we traverse the array while factoring out each number (factoring is only O(logn) given we know the smallest prime factor of each number now),
pseudo-code
for a in keys:
temp_a = a # copy
while temp_a != 1:
prime_factor = smallest_prime_factor[temp_a]
temp_a = temp_a / prime_factor
if solution for new temp_a is already known in num_divisor just update it from there (no recalculation)
else:
if new temp_a is in keys, we increment the solution in num_divisor by 1 and continue
overall complexity: max(keys) * log(max(keys)) [for seive] + n * log(max(keys))
This should work well if the keys are uniformly distributed. For cases like,
keys = [2, 4, 1001210], it will do lots of unnecessary computation, so in those cases, it is better to avoid the sieve and instead compute the prime factors directly or in extreme cases, the pairwise divisor calculation should outperform.
I think you could change one of the n into a factor that is pseudo-polynomial by inserting the numbers into a dict (amortized, expected).
keys = [2,4,8,2]
# https://stackoverflow.com/a/280156/2472827
# max key, O(keys)
max_key = max(keys)
# https://stackoverflow.com/a/6582852/2472827
# occurrence counter, O(keys)
count = dict()
for k in keys:
count[k] = count.get(k, 0) + 1
# https://stackoverflow.com/a/18634675/2472827
# transform into an answer counter, O(keys)
answer = dict.fromkeys(keys, 0)
# https://stackoverflow.com/a/1602964/2472827
# fill the answer, O(keys * max_key/min_key)
for a in answer:
max_factor = int(max_key / a)
for factor in range(1, max_factor + 1):
number = a * factor
if number in answer:
answer[number] += count[a]
# O(keys)
a = max(answer, key = answer.get)
print answer[a], "/", len(keys), "list items dividing", a
I think it works in O(n * max_n/min_n) (expected.) In this case, it is pretty good, but if you have a high dynamic range of values, it's easy to make it go slow.
You could potentially improve your code by:
Account for duplicates by putting keys in a counting map first (e.g. so you don't have to parse the '2' twice in your example). This helps if there's a lot of repetition.
If the square root of the value being checked is smaller than the number of keys, check up to the square root of the value being checked (together with the value being checked divided by its divisors). This helps if there are lots of numbers having square roots that are smaller than the total number of elements.
E.g. If we're checking 30 and the list is big, we only need to check: 1 up to 5 to see if they divide 30 and their counts, as well as 30 divided by any of its divisors in this range (30/1=30, 30/2=15, 30/3=10, 30/5=6) and their counts.
E.g. if we're checking 10^100+17, and there are 10 items total, just check each of them in turn.
Neither of these affect worst case analysis since an adversary could choose inputs where they're useless. They may help in the problems you need to solve depending on your inputs, and may help more broadly if you have some guarantees on the inputs.
Let's think about this a different way: instead of an array of numbers, consider a directed acyclic graph where each number becomes a vertex in the graph, and there is an edge from u → v if and only if u divides v. The problem is now to find a vertex with the greatest in-degree.
Note that we can't actually count the in-degree of a vertex directly in less than Θ(n) time, so the naive solution is to count the in-degree of every vertex in Θ(n2) time. To do better than Θ(n2), we need to take advantage of the fact that if there are edges u → v → w then there is also an edge u → w; we get knowledge of three edges for the price of two divisibility tests. If there are edges u → v → w → x then three divisibility tests can buy us knowledge of six edges in the graph, and so on.
However, crucially, we only get "free" information about edges if the numbers we test are divisible by each other. If we do a divisibility test and the result is negative, we get no "free" information about other possible edges. So in the worst case, there is only one edge in the graph (i.e. all the numbers in the array are not multiples of each other, except for one pair). For an algorithm to output the correct result, it must find the single number in the array which has a divisor in the array, but each divisibility test which doesn't find this pair gives no "free" information. So in the worst case, we would indeed have to test every pair to find the correct answer. This proves that a worst-case time complexity of Θ(n2) is the best you can do in the general* case.
*In case the array elements are bounded, then a pseudopolynomial algorithm could plausibly do better.

How to understand leetcode 494 Target Sum ( knapsack problem ) fastest python code using bit operation

The problem is described as follows
https://leetcode.com/problems/target-sum/
You are given a list of non-negative integers, a1, a2, ..., an, and a target, S. Now you have 2 symbols + and -. For each integer, you should choose one from + and - as its new symbol.
Find out how many ways to assign symbols to make sum of integers equal to target S.
Constraints:
The length of the given array is positive and will not exceed 20.
The sum of elements in the given array will not exceed 1000.
Your output answer is guaranteed to be fitted in a 32-bit integer.
I find this submission in leetcode submission detail Accepted Solutions Runtime Distribution
class Solution:
def findTargetSumWays(self, nums, S):
a = sum(nums) - S
if a < 0 or a%2==1: return 0
S = [((1<<(i*21))+1) for i in nums]
return reduce(lambda p,i:(p*i)&(1<<((a//2+1)*21))-1,S,1)>>(21*a//2)
Simplify reduce, it becomes
class Solution:
def findTargetSumWays(self, nums, S):
a = sum(nums) - S
if a < 0 or a%2==1: return 0
auxarr = [((1<<(i*21))+1) for i in nums]
ret=1
for i in auxarr:
ret= (ret*i)&(1<<((a//2+1)*21))-1
return ret>>(21*a//2)
It transforms the original problem into another problem that finds the number of selections that select some nums[i] that their sum is (sum(nums)-S)/2.
I know how to solve such knapsack problems with dp, but I can't understand the above code, I am very curious how such code works, please help me.
# my dp code
class Solution:
def findTargetSumWays(self, nums: List[int], S: int) -> int:
S=sum(nums)-S
if S%2!=0 or S<0: return 0
S//=2
dp=[0]*(S+1)
dp[0]=1
for c in nums:
for j in range(S,c-1,-1):
dp[j]+=dp[j-c]
return dp[S]
It seems to use characteristics of a polynomial where you multiply terms formed of (B^n+1) where B is a power of 2 large enough to avoid overlapping.
So, let's say you have 3 numbers (x,y,z) to add, it will compute:
(B^x + 1)(B^y + 1)(B^z + 1)
The exponents of these polynomials will add up in the result
B^(x+y+z) + B^(x+z) + B^(y+z) + B^z + B^(x+y) + B^x + B^y + 1
So, if any combination of exponent (i.e. numbers) adds up to the same total, the number of times B^total occurs will be the number of ways to obtain that total. Leveraging this characteristic of polynomials, we will find ways*B^total in the result. If the number of ways does not overlap with the value of B^(total+1), it can be extracted using masks and integer divisions.
For example, given 4 numbers h,i,j,k, the products will produce the sum of B raised to powers corresponding to every combination of 1 up to 4 of the numbers added together. So, if we are looking for a total of T, and h+i and j+k equal T. Then the product will contain 2*B^T formed by B^(h+i) + B^(j+k). This corresponds to two ways to form the sum T.
Given that there are 2 possibility for each number (+ or -), there is a maximum of 2^20 possible ways to combine them. To make sure that any sum of B^x does not overlap with B^(x+1), the value of 2^21 is chosen for B.
This is why the offset array (variable name S is a really poor choice here) is formed of (B^n+1) for each n in nums, where B is 2^21, so (2^21^n+1) ... (2^(21n)+1) ... (1<<(21*n))+1
To be able to use the polynomial approach, the problem needs to be converted to an Absence/Presence problem. This is done by reasoning that there has to be a combination of numbers that produces a zero sum by cancelling each other out, leaving the rest to be positive and add up to S. So, if we remove S from the total of numbers, there will be a combination that adds up to half of what remains (a//2). This will be the total we will be looking for.
The reduce function implements the polynomial product and applies a mask ((1<<((a//2+1)*21))-1) to cut off any power of B that is beyond B^(a/2). The final result cuts off the part below B^(a/2) by shifting bits.
This results in the multiple of B^(a/2) which corresponds to the number of ways to produce the sum of exponents (i.e the sum of numbers).

Efficiently Predict the Output of a Number Processing Algorithm

I am working on a bit of code that needs to be able to efficiently predict (preferably in O(1) time) the output of the following algorithm when presented with two ints m and n.
algorithm(m,n):
history = set()
while True:
if (m,n) in history:
return False
elif n == m:
return True
else:
history.add((m,n))
if m>n:
x = m-n
y = 2*n
m = x
n = y
else:
x = 2*m
y = n-m
m = x
n = y
Note that when (m,n) appears in the following algorithm's history, you've entered an infinite loop (i.e. 2,1 -> 1,2 -> 2,1...); when m==n the algorithm can proceed only one step further and must terminate (i.e. 5,5 -> 10,0 -> 10,0...). Essentially I need to be able to predict if m(current) and n(current) will ever match.
PS, if this algorithm has a name I'd love to know it. Furthermore, if there exists good reading on this topic (predicting numerical sequences, etc...) I'd love to be directed to it.
Assuming positive integer input, this algorithm will return True if and only if (m+n)/gcd(m, n) is a power of two.
Proof sketch:
Divide both m and n by gcd(m, n) at the start of the algorithm; this will not change the return value.
If the sum of m and n is divisible by an odd prime p after doing this, then both m and n need to become divisible by p for the algorithm to return True, but neither m nor n can do so.
If the sum of m and n is a power of two, then both m and n will become divisible by another factor of 2 on each iteration until both are equal.
First of all, let's reduce the update step to a single line. On each iteration, m updates to the absolute difference; n updates to twice the smaller number.
else:
history.add((m,n))
m, n = abs(m-n), 2 * min(m, n)
This highlights the non-linearity of the iteration. Each update breaks into the two classes you first programmed; the recurrence breaks into multiple classes on each further iteration.
I believe that the short answer for this is no -- you cannot predict the outcome in a time reasonably shorter than simply executing the algorithm.
The division point for switching large vs smaller is when one number is 3 times the other. In that space, the algorithm closes the gap simply: subtract the smaller form the larger, then double the smaller. Once they get within the 3x range, the system quickly turns chaotic: you cannot state that two nearby pairs will have results that remain nearby as the algorithm progresses, not for any adjacent pairs.

Categories

Resources