Optimal solution for this interview question - optimization

Can anyone provide optimal solution with Time Complexity? Very appreciated!
Determine bandwidth required to run prescheduled video streams.
There can be 10s of thousands of streams and all the scheduling data is available at the start.
There may be time intervals when no streams are running
Inputs
int startTime; //start time for the stream
int duration; //how long the stream runs
int bandwidth; // consumed bandwidth from start to end
Output:
list[int] totalBW; // bandwidth needed at every time instant
Example:
input (list may not sorted)
[ [0,2,10], [1,2,10], [2,1,20] ]
output
[10, 20, 30]
Explanation
At time 0: only the first element needs bandwidth 10 => [10, ...]
At time 1: first two elements need bandwidth 10 + 10 => [10, 20, ...]
At time 2: the second and third element need bandwidth 10 + 20 => [10, 20, 30]
The brute force approach using python:
def solution(streams):
_max = max(streams, key=lambda x: (x[0]+x[1]))
_max_time = _max[0] + _max[1]
res = [0] * _max_time
for s, d, bw in streams:
for i in range(d):
res[s+i] += bw
return res
Is there any more efficient approach?

Is there any more efficient approach?
Yes.
The first step would be to transform the original data into a set of "at time = T, bandwidth changes by N" events, sorted in chronological order, and simplified by merging events that happen at the same time.
For your example, if the input is [ [0,2,10], [1,2,10], [2,1,20] ] then it would be broken up into:
** [ [0,2,10] **
At 0, bandwidth += 10
At 2, bandwidth += -10
** [1,2,10] **
At 1, bandwidth += 10
At 3, bandwidth += -10
** [2,1,20] **
At 2, bandwidth += 20
At 3, bandwidth += -20
..then sorted and simplified (merging events that happen at the same time - e.g. bandwidth += -10, bandwidth += 20 becomes a single bandwidth += 10) to get:
At 0, bandwidth += 10
At 1, bandwidth += 10
At 2, bandwidth += 10
At 3, bandwidth += -30
From there it's a simple matter of generating the final array from the sorted list:
10, 20, 30, 0
To understand why this is more efficient, imagine what happens if the time is tracked with higher precision (e.g. maybe milliseconds instead of seconds) and the input is [ [0,2000,10], [1000,2000,10], [2000,1000,20] ] instead. For my approach generating the final array would become an outer loop with 4 iterations and an inner loop that can be a highly optimized memset() (C) or rep stosd (80x86 assembly) or np.full() (Python with NumPy); and for your approach the outer loop needs 30000 iterations where the inner loop wastes a huge amount of time repeating a linear search that (for most iterations of the outer loop) finds the same answer as the previous iteration of the outer loop.

Related

How to add sequential (time series) constraint to optimization problem using python PuLP?

A simple optimization problem: Find the optimal control sequence for a refrigerator based on the cost of energy. The only constraint is to stay below a temperature threshold, and the objective function tries to minimize the cost of energy used. This problem is simplified so the control is simply a binary array, ie. [0, 1, 0, 1, 0], where 1 means using electricity to cool the fridge, and 0 means to turn of the cooling mechanism (which means there is no cost for this period, but the temperature will increase). We can assume each period is fixed period of time, and has a constant temperature change based on it's on/off status.
Here are the example values:
Cost of energy (for our example 5 periods): [466, 426, 423, 442, 494]
Minimum cooling periods (just as a test): 3
Starting temperature: 0
Temperature threshold(must be less than or equal): 1
Temperature change per period of cooling: -1
Temperature change per period of warming (when control input is 0): 2
And here is the code in PuLP
from pulp import LpProblem, LpMinimize, LpVariable, lpSum, LpStatus, value
from itertools import accumulate
l = list(range(5))
costy = [466, 426, 423, 442, 494]
cost = dict(zip(l, costy))
min_cooling_periods = 3
prob = LpProblem("Fridge", LpMinimize)
si = LpVariable.dicts("time_step", l, lowBound=0, upBound=1, cat='Integer')
prob += lpSum([cost[i]*si[i] for i in l]) # cost function to minimize
prob += lpSum([si[i] for i in l]) >= min_cooling_periods # how many values must be positive
prob.solve()
The optimization seems to work before I try to account for the temperature threshold. With just the cost function, it returns an array of 0s, which does indeed minimize the cost (duh). With the first constraint (how many values must be positive) it picks the cheapest 3 cooling periods, and calculates the total cost correctly.
obj = value(prob.objective)
print(f'Solution is {LpStatus[prob.status]}\nThe total cost of this regime is: {obj}\n')
for v in prob.variables():
print(f'{v.name} = {v.varValue}')
output:
Solution is Optimal
The total cost of this regime is: 1291.0
time_step_0 = 0.0
time_step_1 = 1.0
time_step_2 = 1.0
time_step_3 = 1.0
time_step_4 = 0.0
So, if our control sequence is [0, 1, 1, 1, 0], the temperature will look like this at the end of each cooling/warming period: [2, 1, 0, -1, 1]. The temperature goes up 2 whenever the control input is 1, and down 1 whenever the control input is 1. This example sequence is a valid answer, but will have to change if we add a max temperature threshold of 1, which would mean the first value must be a 1, or else the fridge will warm to a temperature of 2.
However I get incorrect results when trying to specify the sequential constraint of staying within the temperature thresholds with the condition:
up_temp_thresh = 1
down = -1
up = 2
# here is where I try to ensure that the control sequence would never cause the temperature to
# surpass the threshold. In practice I would like a lower and upper threshold but for now
# let us focus only on the upper threshold.
prob += lpSum([e <= up_temp_thresh for e in accumulate([down if si[i] == 1. else up for i in l])]) >= len(l)
In this case the answer comes out the same as before, I am clearly not formulating it correctly as the sequence [0, 1, 1, 1, 0] would surpass the threshold.
I am trying to encode "the temperature at the end of each control sequence must be less than the threshold". I do this by turning the control sequence into an array of the temperature changes, so control sequence [0, 1, 1, 1, 0] gives us temperature changes [2, -1, -1, -1, 2]. Then using the accumulate function, it computes a cumulative sum, equal to the fridge temp after each step, which is [2, 1, 0, -1, 1]. I would like to just check if the max of this array is less than the threshold, but using lpSum I check that the sum of values in the array less than the threshold is equal to the length of the array, which should be the same thing.
However I'm clearly formulating this step incorrectly. As written this last constraint has no effect on the output, and small changes give other wrong answers. It seems the answer should be [1, 1, 1, 0, 0], which gives an acceptable temperature series of [-1, -2, -3, -1, 1]. How can I specify the sequential nature of the control input using PuLP, or another free python optimization library?
The easiest and least error-prone approach would be to create a new set of auxillary variables of your problem which track the temperature of the fridge in each interval. These are not 'primary decision variables' because you cannot directly choose them - rather the value of them is constrained by the on/off decision variables for the fridge.
You would then add constraints on these temperature state variables to represent the dynamics. So in untested code:
l_plus_1 = list(range(6))
fridge_temp = LpVariable.dicts("fridge_temp", l_plus_1, cat='Continuos')
fridge_temp[0] = init_temp # initial temperature of fridge - a known value
for i in l:
prob += fridge_temp[i+1] == fridge_temp[i] + 2 - 3*s[i]
You can then sent the min/max temperature constraints on these new fridge_temp variables.
Note that in the above I've assumed that the fridge temperature variables are defined at one more intervals than the on/off decisions for the fridge. The fridge temperature variables represent the temperature at the start of an interval - and having one extra one means we can ensure the final temperature of the fridge is acceptable.

Optimization, time complexity and flowchart (Scilab)

I tried to optimize this code, but it’s impossible to optimize anymore.
Please help with building a flowchart for this algorithm.
A = [-1,0,1,2,3,5,6,8,10,13,19,23,45];
B = [0,1,3,6,7,8,9,12,45];
N1 = length(A);
N2 = length(B);
t = 1;
m = 10;
C = [];
for i=1:N1
for j=1:N2
if A(i)==B(j)
break
else
if j==N2
C(t)=A(i);
t=t+1;
end
end
end
end
disp(C);
N3=length(C);
R = [];
y = 1;
for l=1:N3
if C(l)>m
R(y)=C(l);
y=y+1;
end
end
disp(R);
How to find time complexity of an algorithm
I think it should be O(n).
Dominant (elementary) operation:
comparison A(i)== B(j)
But I am not sure yet.
And I can't do
Complexity function (worst case)
and
Worst Computing Complexity:
𝐹(𝑁)
"Optimization" depends of your goal for exmple you may want to minimize the number of floating point operation or to minimize the number of Scilab instruction or minimize the execution time of the algorithm.
As Scilab is an intepreted langage it is possible to reduce the execution time ans code length applying vectorization.
For example your code
N3=length(C);
R = [];
y = 1;
for l=1:N3
if C(l)>m
R(y)=C(l);
y=y+1;
end
end
may be rewritten:
R=C(C>m)
Here the number of computer operations is more or less the same as in the original code, but the execution time is many times faster:
Let C=rand(1,1000);m=0.5;
--> timer();R=C(C>0.5);timer()
ans =
0.000137
--> timer();
--> N3=length(C);
--> R = [];
--> y = 1;
--> for l=1:N3
> if C(l)>m
> R(y)=C(l);
> y=y+1;
> end
> end
--> timer()
ans =
0.388749
This seems like it's probably homework ;p
As for time complexity, it looks more like it would be O(n²) since you have a for loop, inside another for loop.
I recently started watching this course about Algorithms and data structures on Udemy that I highly recommend. He explains BigO notation quite well :)
Link to course. (No affiliations to the author)
As far as the optimization is concerned you should consider that Scilab (and its fellow mathematical programming languages MATLAB and Octave) are intrinsically vectorized. It means if you are using too many for loops, you should probably go back and read some documentation and tutorials. The canonical version of the code you wrote can be implemented as:
A = [-1, 0, 1, 2, 3, 5, 6, 8, 10, 13, 19, 23, 45];
B = [0, 1, 3, 6, 7, 8, 9, 12, 45];
C = setdiff(A, B);
disp(C);
m = 10;
R = C(C > m);
disp(R);
Result:
-1. 2. 5. 10. 13. 19. 23.
23.

How to Understand Time Complexity of Happy Number Problem Solution from Leetcode

I have some difficulties in understanding the time complexity analysis for one solution for the Happy Number Question from Leet code, for my doubts on complexity analysis, I marked them in bold and really appreciate your advice
Here is the question:
Link: https://leetcode.com/problems/happy-number/
Question:
Write an algorithm to determine if a number is "happy".
A happy number is a number defined by the following process: Starting with any positive integer, replace the number by the sum of the squares of its digits, and repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1. Those numbers for which this process ends in 1 are happy numbers.
Example:
Input: 19
Output: true
Explanation:
1^2(square of 1) + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
Here is the code:
class Solution(object):
def isHappy(self, n):
#getnext function will compute the sum of square of each digit of n
def getnext(n):
totalsum = 0
while n>0:
n,v = divmod(n,10)
totalsum+=v**2
return totalsum
#we declare seen as a set to track the number we already visited
seen = set()
#we stop checking if: either the number reaches one or the number was visited #already(ex.a cycle)
while n!=1 and (n not in seen):
seen.add(n)
n = getnext(n)
return n==1
Note: feel free to let me know if I need to explain how the code works
Time Complexity Analysis:
Time complexity : O(243 * 3 + logN + loglogN + log loglog N)...=O(logN).
Finding the next value for a given number has a cost of O(log n)because we are processing each digit in the number, and the number of digits in a number is given by logN.
My doubt: why the number of digits in a number is given by logN? what is N here? the value of a specific number or something else?
To work out the total time complexity, we'll need to think carefully about how many numbers are in the chain, and how big they are.
We determined above that once a number is below 243, it is impossible for it to go back up above 243.Therefore, based on our very shallow analysis we know for sure that once a number is below 243, it is impossible for it to take more than another 243 steps to terminate.
Each of these numbers has at most 3 digits. With a little more analysis, we could replace the 243 with the length of the longest number chain below 243, however because the constant doesn't matter anyway, we won't worry about it.
My doubt: I think the above paragraph is related to the time complexity component of 243*3, but I cannot understand why we multiply 243 by 3
For an n above 243, we need to consider the cost of each number in the chain that is above 243. With a little math, we can show that in the worst case, these costs will be O(log n) + O(log log n) + O(log log log N)... Luckily for us, the O(logN) is the dominating part, and the others are all tiny in comparison (collectively, they add up to less than logN), so we can ignore them. My doubt: what is the reasoning behind O(log log n) O(log log log N) for an n above 243?
Well, my guess for the first doubt is that the number of digits of a base 10 number is given by it's value (N) taken to the logarithm at base 10, rounded down. So for example, 1023 would have floor(log10(1023)) digits, which is 3. So yes, the N is the value of the number. the log in time complexity indicates a logarithm, not specifically that of base 2 or base e.
As for the second doubt, it probably has to do with the work required to reduce a number to below 243, but I am not sure. I'll edit this answer once I work that bit out.
Let's say N has M digits. Than getnext(N) <= 81*M. The equality happens when N only has 9's.
When N < 1000, i.e. at most 3 digits, getnext(N) <= 3*81 = 243. Now, you will have to call getnext(.) at most O(243) times to figure out if N is indeed happy.
If M > 3, number of digits of getnext(N) must be less than M. Try getnext(9999), getnext(99999), and so on [1].
Notes:
[1] Adding a digit to N can make it at most 10*N + 9, i.e. adding a 9 at the end. But the number of digits increases to M+1 only. It's a logarithmic relationship between N and M. Hence, the same relationship holds between N and 81*M.
Using the Leetcode solution
class Solution {
private int getNext(int n) {
int totalSum = 0;
while (n > 0) {
int d = n % 10;
n = n / 10;
totalSum += d * d;
}
return totalSum;
}
public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n);
n = getNext(n);
}
return n == 1;
}
}
}
O(243*3) for n < 243
3 is the max number of digits in n
e.g. For n = 243
getNext() will take a maximum of 3 iterations because there are 3 digits for us to loop over.
isHappy() can take a maximum of 243 iterations to find a cycle or terminate, because we can store a max of 243 numbers in our hash set.
O(log n) + O(log log n) + O(log log log N)... for n > 243
1st iteration + 2nd iteration + 3rd iteration ...
getNext() will be called a maximum of O(log n) times. Because log10 n is the number of digits.
isHappy() will be called a maximum of 9^2 per digit. This is the max we can store in the hash set before we find a cycle or terminate.
First Iteration
9^2 * number of digits
O(81*(log n)) drop the constant
O(log n)
+
Second Iteration
O(log (81*(log n))) drop the constant
O(log log n)
+
Third Iteration
O(log log log N)
+
ect ...

Understanding time complexity: iterative algorithm

I'm new with getting time complexities and I can't seem to understand the logic behind getting this at the end:
100 (n(n+1) / 2)
For this function:
function a() {
int i,j,k,n;
for(i=1; i<=n; i++) {
for(j=1; j<=i; j++) {
for(k=1; k<=100; k++) {
print("hello");
}
}
}
}
Here's how I understand its algorithm:
i = 1, 2, 3, 4...n
j = 1, 2, 3, 4...(dependent to i, which can be 'n')
k = 1(100), 2(100), 3(100), 4(100)...n(100)
= 100 [1, 2, 3, 4.....]
If I'll use this algorithm above to simulate the end equation, I'll get this result:
End Equation:
100 (n(n+1) / 2)
Simulation
i = 1, 2, 3, 4... n
j = 1, 2, 3, 4... n
k = 100, 300, 600, 10000
I usually study these in youtube and get the idea of Big O, Omega & Theta but when it comes to this one, I can't figure out how they end with the equation such as what I have given. Please help and if you have some best practices, please share.
EDIT:
As for my own assumption of answer, it think it should be this one:
100 ((n+n)/2) or 100 (2n / 2)
Source:
https://www.youtube.com/watch?v=FEnwM-iDb2g
At around: 15:21
I think you've got i and j correct, except that it's not clear why you say k = 100, 200, 300... In every loop, k runs from 1 to 100.
So let's think through the inner loop first:
k from 1 to 100:
// Do something
The inner loop is O(100) = O(1) because its runtime does not depend on n. Now we analyze the outer loops:
i from 1 to n:
j from 1 to i:
// Do inner stuff
Now lets count how many times Do inner stuff executes:
i = 1 1 time
i = 2 2 times
i = 3 3 times
... ...
i = n n times
This is our classic triangular sum 1 + 2 + 3 + ... n = n(n+1) / 2. Therefore, the time complexity of the outer two loops is O(n(n+1)/2) which reduces to O(n^2).
The time complexity of the entire thing is O(1 * n^2) = O(n^2) because nesting loops multiplies the complexities (assuming the runtime of the inner loop is independent of the variables in the outer loops). Note here that if we had not reduced at various phases, we would be left with O(100(n)(n+1)/2), which is equivalent to O(n^2) because of the properties of big-O notation.
SOME TIPS:
You asked for some best practices. Here are some "rules" that I made use of in analyzing the example you posted.
In time complexity analysis, we can ignore multiplication by a constant. This is why the inner loop is still O(1) even though it executes 100 times. Understanding this is the basis of time complexity. We are analyzing runtime on a large scale, not counting the number of clock cycles.
With nested loops where the runtime is independent of each other, just multiply the complexity. Nesting the O(1) loop inside the outer O(N^2) loops resulted in O(N^2) code.
Some more reduction rules: http://courses.washington.edu/css162/rnash/quarters/current/labs/bigOLab/lab9.htm
If you can break code up into smaller pieces (in the same way we analyzed the k loop separately from the outer loops) then you can take advantage of the nesting rule to find the combined complexity.
Note on Omega/Theta:
Theta is the "exact bound" for time complexity whereas Big-O and Omega are upper and lower bounds respectively. Because there is no random data (like there is in a sorting algorithm), we can get an exact bound on the time complexity and the upper bound is equal to the lower bound. Therefore, it does not make any difference if we use O, Omega or Theta in this case.

Maximum contigous subsequence sum of x elements

So I came up with a question that I've looked and searched but with no answer found... What's the best (and by saying the best, I mean the fastest) way to get the maximum contigous subsequence sum of x elements?
Imagine that I've: A[] = {2, 4, 1, 10, 40, 50, 22, 1, 24, 12, 40, 11, ...}.
And then I ask:
"What is the maximum contigous subsequence on array A with 3 elements?"
Please imagine this in a array with more than 100000 elements... Can someone help me?
Thank you for your time and you help!
I Googled it and found this:
Using Divide and Conquer approach, we can find the maximum subarray sum in O(nLogn) time. Following is the Divide and Conquer algorithm.
The Kadane’s Algorithm for this problem takes O(n) time. Therefore the Kadane’s algorithm is better than the Divide and Conquer approach
See the code:
Initialize:
max_so_far = 0
max_ending_here = 0
Loop for each element of the array
(a) max_ending_here = max_ending_here + a[i]
(b) if(max_ending_here < 0)
max_ending_here = 0
(c) if(max_so_far < max_ending_here)
max_so_far = max_ending_here
return max_so_far