Saturday, July 17, 2021
How did that thought occur?
Sunday, May 16, 2021
Gist vs Details
All of us have heard that 'the devil is in the detail', and is intended to mean that we should bother about the details.
But should we really?
Lets check a counter example, which would rather re-interpret 'the devil is in the detail' as don't get into the details or you will awaken the devil π
It is a major efficiency issue in many 'structured' organizations, several people focusing on details and the details going ping-pong, and finally what one could implement in a day taking months.
End result is a robust bullet proof museum worthy product crafted by hundreds. Why museum worthy? Oh its market is already taken by a product quickly patched together by a dozen
Just to clear out the air, this is metaphoric writing and 'details' is quite overloaded in its meaning.
Here is the counter example problem statement:
Hang on tight as we get into the "detailed analysis :)"
Detailed Case Analysis:
"n teams need n/2 matches if n is even" translates to
m(2n) = n + m(n) ------> Eq.1
"n teams need (n-1)/2 matches if n is odd, and one gets a free pass" translates to
m(2n+1) = n + m(n) + 1 ------> Eq.2
From Eq.1 and 2:
m(2n+1) = m(2n) + 1 ------> Eq.3
Now, this is great that we can express m(2n+1) in terms of m(2n).
If we can calculate m(2n) in closed form, we can easily calculate m(2n+1) by adding a 1 to it.
Friday, May 14, 2021
A Hare and Tortoise tale of computation
The problem is this: "Given an array of positive integers, calculate the sum of all possible odd-length sub-arrays."
The ingenious way: total= ∑ count[i]*val[i] where count[i] defines the number of odd length arrays that can be formed with val[i]. For indices beginning at 0, count[i] = ceil( (i+1)*(n-i ) / 2 )
The smarter code
1 2 3 4 5 6 7 8 9 10 | int sumOddLengthSubarrays(vector<int>& arr) { int total = 0; int n = arr.size(); for(int i = 0; i < n; i++){ int count = (i+1)*(n-i); int count_odd = count/2 + count%2; total += count_odd*arr[i]; } return total; } |
combi_sum[i] = ∑ {
The round about code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | int sumOddLengthSubarrays(vector<int>& arr) { vector<int> running_sum; running_sum.reserve(arr.size()); vector<int> combination_sum; combination_sum.reserve(arr.size()); int total = 0; int i = 0; int k = arr.size() > 1 ? 2 : arr.size(); for(i = 0; i < k; i++){ total += arr[i]; running_sum.push_back(total); combination_sum.push_back(0); } if(i < arr.size()){ total += arr[i]; running_sum.push_back(total); combination_sum.push_back(total); total += total; i++; } for(; i < arr.size(); i++){ total += arr[i]; running_sum.push_back(running_sum[i-1] + arr[i]); combination_sum.push_back( (((i/2) - 1) * (arr[i-1] + arr[i])) + combination_sum[i-2] + running_sum[i] - running_sum[i-3] ); total += combination_sum[i]; } return total; } |
i for sure thought the smarter and concise version was also the faster... but was in for a surprise trying to cheerfully test its performance.
And that too, after a recovery from the rather laborious and clumsy looking round about version.
Let me lay it out for you.. the fact is that the 'round about way', beats the 'smarter way'..
And how is it so? if you wonder, it seems to come from the cost of multiplications... not only the number of them, but also from the value of the multiplicand.
So, lets draw a table tracing through the calculations of each approach.
Lets start with the smarter and concise way first.
| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| example data | 6 | 5 | 8 | 7 | 1 | 2 | 4 | 3 | 9 | 11 | 10 |
| cnt(all) | 1*11= 11 | 2*10= 20 | 3*9 = 27 | 4*8 = 32 | 5*7 = 35 | 6*6 = 36 | 7*5 = 35 | 8*4 = 32 | 9*3 = 27 | 10*2 = 20 | 11*1= 11 |
| cnt(odd) = ceil(cnt(all)/2) | 6 | 10 | 14 | 16 | 18 | 18 | 18 | 16 | 14 | 10 | 6 |
| cnt(odd) * val | 6*6= 36 |
10*5= 50 |
14*8= 112 |
16*7= 112 |
18*1= 18 |
18*2= 36 |
18*4= 72 | 16*3= 48 |
14*9= 126 |
10*11= 110 |
6*10= 60 |
Total = ∑(cnt(odd)[i]*val[i]) = 780
| index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| example data | 6 | 5 | 8 | 7 | 1 | 2 | 4 | 3 | 9 | 11 | 10 |
| running sum | 6 | 11 |
19 |
26 | 27 | 29 | 33 | 36 | 45 | 56 | 66 |
| combi sum | 0 | 0 | 19 |
20= 0*15+0 +26-6 | 43= 1*8+19 +27-11 | 33= 1*3+20 +29-19 |
62= 2*6+43 +33-26 |
56= 2*7+33 +36-27 |
114= 3*12+62 +45-29 |
139= 3*20+56 +56-33 |
228= 4*21+114 +66-36 |
Total = running_sum[10] + ∑(combination_sum[i]) = 780
Here, in the round about way, we surely have additional memory to buffer up the combi_sum and also running_sum, but we have only one multiplication per element, though we have a few more additions.
The highest multiplicand is (n-2)/2 for an array of size n... compare it to n*n/8 in case of the smarter solution.
And hence, sometimes, the operations we assume wont have any noticeable performance impact, can indeed do, or can it not? Especially since the multiplication operation within wordsize is guaranteed to have same asymptotic cost as addition..
While the hare was hopping element to element without looking back much swiftly, it was kept busy in multiplication..
The tortoise was looking back at at least 3 elements but as a result kept its multiplication simpler..
And in the end it went past the finish line without losing much of its breath.
But on a third look, its not the multiplication that's the problem with the smarter way, its just the calculation of odd_count as (count/2+count%2) or ceil(count/2.0)
Do it as odd_count = (count+1)/2. Now, both hare and rabbit are happy, they crossed the line at the same time, though they took different routes...
Time to think, whats the cost of modulo π±
Thursday, May 06, 2021
Obsession Outlook Originality = Output
Yea right, the three Os of Obsession, Outlook, and Originality together can read OOO and mean that you are Out Of Office for Others π
Can be on anything, but when one leads to the other in the order, it can be good, and when it feeds back, can probably be better after each cycle.
Journaling here one such cycle while working on a leet problem.
Thursday, April 15, 2021
little joy of sorting
/** * Return the number of specially colored things. * Each thing has only one color on it. * 'thing' s are comparable based on their color * with a 'color' or another 'thing'. * 'color' s can be compared to each other as well. */ int find(vector special_colors, vector things)
Now we can implement it in two ways
1) The so mixed up way :-)
int find(vector<color> special_colors, vector <thing> things) { int special_things = 0; for (auto thing: things) { for (auto special_color: special_colors) { if (thing == special_color) { special_things++; } } } return special_things; }
It gets the job done, but errr, has a time complexity of O(m*n) where m = special_colors.size() and n = things.size()
(OR)
int find(vector special_colors, vector things) { sort(special_colors.begin(), special_colors.end()); sort(things.begin(), things.end()); auto thing = things.begin(); auto special_color = special_colors.begin(); int special_things = 0; while (thing != things.end() && special_color != special_colors.end()) { if ( *thing == *special_color) { special_things++; thing++; } else if ( *thing < *special_color) { thing++; } else { special_color++; } } return special_things; }
*average* - because, most often a hybrid algorithm to achieve average optimal performance is used to implement sort()
Thursday, September 03, 2020
Now this may be silly..
But yes, i didn't think its safe to swap two numbers numbers represented in fixed bits without using a temporary variable
We have this famous trick from college days,
Saturday, March 14, 2020
Why only 2 Weighted Medians at most?
Whereas, if the number of elements is even, i.e $2n$, then $n^{th}$ and $(n+1)^{th}$ elements form the median.
Apart from median, there is also a concept called "weighted median".
The Definition
In a given distinct ordering of pairs $\{(i_1 ,w_1) , (i_2, w_2) .. , (i_n, w_n)\}$:------> Let $i$ represent any arbitrary identity of a given element; $w$ represents the weight of that element in the sequence; and $W$ the sum of weights of all the elements
------> Then, an element $i_m$ is a weighted median if $\displaystyle\sum_{1 \le j \lt m} w_j \le \frac{W}{2}$ and $\displaystyle\sum_{m \lt j \le n} w_j \le \frac{W}{2}$
The Interpretation
One good use of weighted median is to compute the median of all the values when there are multiples/repetitions of same values in the sampled data.So if you have $n$ values with average repetition $w_{avg}$ unordered, the median could be computed at $O(n.w_{avg})$.
But if you organize or have the data as distinct values and their repetitions, the complexity to find the median could come down to $O(n)$
Let's see why this is so:
After expanding an ordering of the form: (Lets refer to this as abridged ordering)And if its even, the median comprises of $(\frac{W}{2})^{th}$and $(\frac{W}{2} + 1)^{th}$elements.
Let a given repetition of $i_m$ be the median in the above expanded ordering, and lets also consider $W$ to be odd.
Then, the number of elements in the expanded ordering before and after the selected instance of $i_m$ must exactly be $W/2$.
Note that, for any element other than median, this property will not hold good.
Now if we extract out the copies of $i_m$ on its left, and right, if any, we can establish that the number of non $i_m$ elements before and after $i_m$ is at most $W/2$.
Hence, if we calculate the weighted median of abridged ordering, it would be same as the median of the expanded ordering.
Similar argument can be made for when $W$ is even, and also the special case when such ordering has two distinct medians $i_m$ and $i_{m+1}$
From this analysis on using weights as repetitions, it is easy to see, that like median, the weighted medians can also be at most two.
The observation
But what if we focus on just the plain simple definition of weighted median without interpreting the weights as repetitions or such, and were to "observe" that at most, there can just be two weighted medians.... How would we establish that fact? Well, its not too hard.Let $i_m$ be a weighted median in a given abridged ordering, then,
For me this understanding proved to be more fun than actually writing the R-Select code to find out the weighted median itself, and hence come these notes πThough, i actually ended up describing the algorithm here π
Thursday, March 05, 2020
2-D Peak-A-Proof
Ahem ahem... course correction.. back to the utility of proofs...
Take for example the rather simple 2-D Peak finding problem.
The 2-D Peak finding problem:
| 60 | 100 | 103 | 80 | 95 |
| 125 | 120 | 108 | 101 | 45 |
| 130 | 30 | 93 | 90 | 156 |
| 115 | 40 | 98 | 65 | 125 |
| 75 | 50 | 96 | 55 | 25 |
Well, i guess it varies from person to person. But for some reason, i have this inherent discomfort with anything more than one dimension.
And i get the urge to write about it as i have a feeling there maybe a few more confused souls like me who feel dazed thinking in more than one dimension π.
i tried for several hours to come up with a less than $O(n^2)$ algorithm, but just could not.
Started looking up for hints and tried the....
1-D Peak finding problem:
A peak in an array A means any A(i) such that both A(i-1) and A(i+1) are less than or equal to A(i).
For the beginning and end elements of the array, A(0) is a peak if A(1) ≤ A(0) and A(end) is a peak if A(end-1) ≤ A(end). Find such a peak from the array A.
1-D Peak finding algorithm
"A" (i.e. any) peak in this array can be found in O(logn) time using the following strategy:
1. Take a middle element, A(m) and check if it is a peak.If so, return A(m).
2. If not, either A(m+1) or A(m-1) or both would be greater than A(m)
3. Continue with step 1 on the right half, if A(m+1) >A(m)
Continue with step 1 on the left half, if A(m-1) > A(m)
If both A(m-1), and A(m+1) are greater than A(m), either half is fine.
Is this algorithm correct? Seems trivial enough to prove so.
[proof:1-D Peak finding algorithm]
Suppose A(m) is not a peak and A(m+1) > A(m).
In this case, A(m+1) will be a peak unless its own right neighbor, A(m+2) > A(m+1).
Lets assume A(m+2) meets this condition. Inductively, for A(m+2) to not be a peak, A(m+3) > A(m+2) and it can go on and on, only till the end of the array where the end element will turn out to be a peak.
Hence, when we follow the half where A(m+1) > A(m), we are guaranteed to find a peak.
We can argue similarly for the case when A(m-1) > A(m).
[proof:end]
Surprisingly so, this new found wisdom on the 1-D case got me no where with the 2-D case, and finally i looked up the algorithm π
However, i still could not convince myself very easily that the 2-D algorithm i looked up is actually correct.
Guess this is always the case with borrowed analysis, because it is less of an analysis and more of an assurance.
Anyways putting a stop to the sob, here is the most simple of the divide and conquer algorithms for the 2-D case:
O(nlogn) 2-D Peak finding algorithm
1. Find the maximum element of the middle column, let it be A(i, m).
If its a peak, return it and terminate.
2.a) If A(i, m-1) >= A(i, m) and A(i, m+1) ≤ A(i, m); then recurse in the left half of matrix.
2.b) Else If A(i, m+1) > A(i, m) and A(i, m+1) ≤= A(i, m); then recurse in the right half of matrix.
2.c) Else If A(i, m-1) >= A(i, m) and A(i, m+1) >= A(i, m); then recurse in either left or right half. (i.e., either of the halves is guaranteed to have a peak)
3. If the recursion reduces the matrix to a single column, just return the max of that column and terminate.
Sounds neat and simple to code up, but is the algorithm correct?
How do we go about its correctness? It was a bit of mind muddling experience for me, because for once in the evening i thought i got the proof but couldn't really write it down. And when i tried to write it down before bed, i couldn't really come up with the proof again.. Wonder if the evening was a moment of self deceit or a volatile intuition π€. Well, so i decided to give it a go in the morning instead when luckily i guess i could reason the correctness correctly.
So here is a proof for the above binary search algorithm for the 2-D peak:
i changed my original proof a bit starting with the base case first. This way, it is simpler to understand and takes fewer words i think.
[proof:O(nlogn) 2-D Peak finding algorithm]
Consider the base case (Step 3), a single column and n rows of elements. Lets call this column as c.
Its easy to see that the max of that column is a "peak" in that 1 column matrix. Lets call this peak as p.
Now, lets unwind the recursion, and add another column, c+1 of n elements to this matrix.
p ceases to be a peak in the new 2 column matrix only if column c+1 has an element greater than p in the same row as p.
But, Step 2.a) in the above algorithm ensures that not only the one next to p, but, all of the elements in column c+1 are less than p if column c+1 was skipped out of the search.
Fine, lets unwind a bit more, and add another column, c-1 of n elements to this matrix.
p ceases to be a peak in the new 3 column matrix only if column c-1 has an element greater than p in the same row as p.
However, Step 2.a) ensured that all the elements in column c-1 are indeed less than p if column c-1 was skipped out of the search.
Hence, inductively we can prove that when the algorithm hits the base case, it always finds a peak which is valid for the undivided input matrix
[proof:end]
If you look at the design of this algorithm, you will notice, the main ingredient is to use the middle column as a virtual valley with the knowledge that the half next to this column has at least one element greater than any of the elements in this virtual valley, and hence there is a guarantee to find a peak in that particular half.
O(n) 2-D Peak finding algorithm
The algorithm goes as below:
1. Find the maximum element among all the elements of the middle row, $m_r$ and middle column, $m_c$
If the maximum is at the intersection of the row and column, it is definitely a peak, so return it and terminate.
2. If the maximum is a peak, return it and terminate.
3. If the maximum is not a peak, recurse is the quadrant where adjacent neighbor of the maximum is greater than the maximum.
4. When recursion is left with only one row, return the max of that row and terminate. Or, if the recursion is left with only one column, return that max of that column and terminate. Or, if the recursion is only left with one element, that one element must be the peak, return it, and terminate.
Bit more details on step3
i am just writing more details on step3 to clarify and maybe also to aid the proof which is coming up.
3.a. If the maximum A($m_r$, j) is in the middle row, and j > $m_c$, and A($m_r$ + 1, j) > A($m_r$, j), then recurse in the quadrant bounded by rows $m_r$ +1 to end, and columns $m_c$ + 1 to end.Below 7 by 7 table gives an example of this case:
$m_c$ = $m_r$ = 4, maximum is A(4, 5) = 4520.
But A(5, 5) = 4610 > A(4, 5), Hence we recurse in the bottom right quadrant.
| 1923 | 3317 | 1784 | 4236 | 126 | 526 | 2583 |
| 4060 | 1068 | 1667 | 1263 | 3989 | 2461 | 4342 |
| 2764 | 139 | 4552 | 4029 | 502 | 2764 | 653 |
| 1347 | 2172 | 1614 | 2852 | 4520 | 4305 | 2199 |
| 4780 | 353 | 2606 | 1504 | 4610 | 3475 | 4515 |
| 1581 | 1132 | 719 | 38 | 1396 | 4285 | 4456 |
| 2607 | 3458 | 1651 | 3426 | 1078 | 4388 | 559 |
| 3328 | 2332 | 42 | 3865 | 378 | 3989 | 3959 |
$m_c$ = 2, $m_r$ = 2, maximum is A(2, 3) = 4456.
But A(1, 3) = 4515 > A(2, 3), Hence we recurse in the top right quadrant.
| 4610 | 3475 | 4515 |
| 1396 | 4285 | 4456 |
| 1078 | 4388 | 559 |
| 378 | 3989 | 3959 |
3.c. Else If the maximum A($m_r$, j) is in the middle row and j < $m_c$ , and A($m_r$ - 1, j) > A($m_r$, j), then recurse in the quadrant bounded by rows 0 to $m_r$ -1, and columns 0 to $m_c$ -1
3.d. Else if the maximum A($m_r$, j) is in the middle row and j < $m_c$ , and A($m_r$ + 1, j) > A($m_r$, j), then recurse in the quadrant bounded by rows $m_r$ +1 to end, and columns 0 to $m_c$ -1
3.e. Else if the maximum A(i, $m_c$) is in the middle column and i < $m_r$, and A(i, $m_c$ - 1) > A(i, $m_c$), then recurse in the quadrant bounded by rows 0 to $m_r$ -1 and columns 0 to $m_c$ -1
3.f. Else if the maximum A(i, $m_c$) is in the middle column and i > $m_r$, and A(i, $m_c$ - 1) > A(i, $m_c$), then recurse in the quadrant bounded by rows $m_r$ + 1 to end, and columns 0 to $m_c$ -1
3.g. Else if the maximum A(i, $m_c$) is in the middle column and i < $m_r$, and A(i, $m_c$ + 1) > A(i, $m_c$), then recurse in the quadrant bounded by rows 0 to $m_r$ -1 and columns $m_c$ + 1 to end.
3.h. Else if the maximum A(i, $m_c$) is in the middle column and i > $m_r$, and A(i, $m_c$ + 1) > A(i, $m_c$), then recurse in the quadrant bounded by rows $m_r$ + 1 to end, and columns $m_c$ + 1 to end.