Saturday, July 17, 2021

How did that thought occur?

Third para from here, i write a basic explanation of Euclid's algorithm for calculating G.C.D.
Why i write this version is because, it baffles me when i see an opening statement:
'we need to find g.c.d of numbers a and b .. consider (a - b)'
[or] 
'we need to find g.c.d of numbers a and b .. consider a = (q1)b + (r1) and b = (q2)(r1) + (r2)'
 
The 'consider this or that' version might be terse and tight to prove, but i think the motivation behind how such consideration came into being gets lost...
It is like for example, you want to understand how red-black trees are designed the way they are without knowing their evolution from the 2-3 trees.
You will keep wondering forever as how such 'considerations' occur. 
Maybe even resigning to a false notion that it just can't happen to you and so get on with the considerations that are already laid out.
But then while your ability to memorize how something works enhances, your ability to arrive at how something can work diminishes...
 
So you have two numbers a and b, and you need to find their greatest common denominator, G.C.D.
Let d be the G.C.D of a and b.
 
Now, what do we want to know? we want to know what d is... But what do we actually know? 
We only know about two numbers a and b whose G.C.D, d we want... How can we get to d from there?
 
We know  a and b ... and we also know that a and b are actually a bunch of 'd's lumped together...  [ ex: a = xd and b = yd ]
So a maybe some x number of 'd's and b maybe some y numbers 'd's ... 
But, we don't want x or y number of 'd's , we just want "a" 'd' .. 
 
We some how need a way to reduce that x or y down to one, and we will be at our prize.
If a > b, we can reduce the numbers of 'd's in a by the number of 'd's in b ... 
How can we do that? Well, just subtract b from a... and now we have (x-y) number of 'd's in a as opposed to the previous x number of  'd's .. 
 
Now that is some progress... And we can do this chopping off, till a > b... 
What if a becomes less than b ? Simple, just swap them... and repeat... 
 
So, where does this repeated chopping down of the number of 'd's from either a or b lead us to?
What happens in the end?
In the end, a will have become equal to b.
What does it mean when a equals b? It means both of them have the same number of 'd's . And when we are looking at the d as the "greatest" common denominator, both have them, exactly have one 'd'.
 
This explanation is no different* from others except for the emphasis on the thought process that leads one to the first step in problem size reduction.
 
* Or even  deficient w.r.t completeness.




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:

"We are hosting a tournament with 'n' number of participants.
If 'n' is even, we conduct n/2 matches and n/2 winners advance to next round.
If 'n' is odd, we give free pass to a random participant, and conduct (n-1)/2 matches with the remaining (n-1) participants. So the random lucky participant and (n-1)/2 winners advance to the next round.
The tournament thus proceeds with remaining participants till a champion is decided.
Calculate the total number of matches that need to be held in the tournament.
The Gist way of solving this problem: 
We need a champion. How do we get a champion? After n-1 participants lose. How do they lose? By  playing a match. So if we play n-1 matches, we eliminate n-1 participants, and we have a champion.
``But wait...what about odd, even and all that?``
What about it? Does it contradict the statement that every match conducted eliminates exactly one participant? ``Hmm, no... ``
Then, hey, its (n-1)

The Details way of solving this same problem:
``What about odd, even and all that?``
We change course here, and say ``No no, lets do some detailed analysis, we want to be sure we covered the odd/even part of the requirements``

Hang on tight as we get into the "detailed analysis :)"

Detailed Case Analysis:

Part 1: Formulate basic recursion:
Lets define matches required for a given number n as m(n).
"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.
We have base cases: m(1) = 0 and m(2) = 1.
Here, Eq.1 and Eq.2 are good, but they are discontinuous, in the sense that we have to ways to move ahead based on whether number of participants in odd or even
Eq.3 is better, but its still a one way relationship in the sense that we can go from even to next odd, but we don't have a way to go from an odd to next even.

Part 2:Derive a continuous recursion
So, lets try to formulate a recursion that is valid for all 'n' in a uniform way.
Lets consider m(2n+2)...
m(2n+2) = m(2(n+1)) = n+1+m(n+1) ---> Eq.4
 
Part 2.1: n is even:
Here we consider that n is even, which means n+1 is odd.
So, m(n+1) = m(n) + 1 based on Eq.2
Eq.4 reduces to:
m(2n+2) = (n+1 + m(n)) + 1 And from Eq.2 n+1+m(n) = m(2n+1)
So, For all even 'n', m(2n+2) = m(2n+1) + 1 -----> Eq5
 
Part 2.2: n is odd:
Here, we consider that n is odd, which means n = 2x+1, from some integer 'x'.
lets substitute this in m(n+1)
Then, m(n+1) = m(2x+1+1) = m(2x+2) = m(2x+1) + 1 (from Eq5 due courtesy Part2.1)
Now, lets plug back the value of n = 2x+1
We get for all odd n, m(n+1) = m(n) + 1 ---> Eq.6
So, we have proved that:
when n is even m(n+1) = m(n) + 1 (Eq.3)
when n is odd, m(n+1) = m(n) + 1(Eq.6)
And hence, 
For all n, m(n+1) = m(n) + 1 --> Eq.6
And thus we arrive at a continuous recursion of m(n) = m(n-1) + 1
 
Part 3: Arrive at the closed form from continuous recursion  
From base cases, m(1) = 0 and expanding Eq. 6:
m(n) = 1 + m(n-2) + 1 = 1 + 1 + m(n-3) + 1 = 1 + 1 + ... (n-2) times + m(1) + 1
m(n) = (n-2) + m(1) + 1 = n-2 + 0 + 1 = n -1 
m(n) = n-1 --> Eq.7 
Q.E.D
 
And here we see just the tips of devil's horns.
The claws, the thorny mace, and other gory tools of torture lie buried in the reviewing, standardizing, platforms, and ways of working  :-)

Friday, May 14, 2021

A Hare and Tortoise tale of computation

Its one of those usual days when i solved a little practice problem, and on a second look, felt like i touched my nose from around the back of my head..

The problem is this: "Given an array of positive integers, calculate the sum of all possible odd-length sub-arrays."
Below are 2 implementations to solve the problem.. 
Now place your bets on which one will be faster on a general purpose risc machine.. 
Its really like an Ingenious vs Ingenuous face-off, and who in the sane world would ever identify themself with a super brainy victor? πŸ˜‰

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;
}

The ingenuous way: total = ∑val[i] + ∑combi_sum[i]; where combi_sum[i] is defined as the sum of all the odd length sub-arrays that end at position (i) other than the single element subarray [val[i]] and computed as below:
combi_sum[0]=combi_sum[1]=0;
combi_sum[2]=val[0]+val[1]+val[2];
For all other (i):
combi_sum[i] = ∑ {
                                num_odd_arrays_ending_at[i-2] * (val[i-1] + val[i]) + 
                                combi_sum[i-2] + 
                                val[i-2] + val[i-1] + val[i];
                               }
Wondering what nasty cocktail is that summation above?
Just go with the flow, there is explanation to follow..
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;
}
Now, which version do you think is faster on a usual computer?
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

So, for each element, we have two multiplications and one divisions. That is obvious before we drew the table.
But what we see from the table is that for an array of size 'n', the multiplicands range:
a) from 1 to n for an array of size n when calculating cnt(odd).
b) and from ceil(n/2) to ceil((n*n)/8 when calculating cnt(odd)*val
So, for an array of size 100, the minimum multiplicand is 50, and maximum is 1250.
The n*n part in the multiplicand indicates, that the multiplicand grow faster than the array size.

Now, let us look at the round about way
Lets recap the definition of calculations used
1) combi_sum[0]=combi_sum[1]=0 
2) combi_sum[2]=val[0]+val[1]+val[2]
3) For all other (i) combi_sum[i] =
                                                                num_odd_arrays_ending_at[i-2] * (val[i-1] + val[i]) +
                                                                combi_sum[i-2] + 
                                                                val[i-2] + val[i-1] + val[i]
                                                            };
A bit of explanation: 
At position (i), assume that already the sum of all the odd length arrays ending at (i-2) is computed.
Now (i), together with (i-1) will add to each of these sub-arrays.
Apart from that, (i) will also form a new odd length array with (i-2), (i-1), and itself.
In the example down below, lets consider position 6. position 6 can form an odd length array in following combinations: a) 0 to 6, b) 2 to 6, c) 4 to 6. 
The sum of those odd length arrays a), b), and c) would be ∑val[0..6], ∑val[2..6], and ∑val[4..6] respectively.
But we already calculated the sum of val[0..4] =27 plus val[2..4] =16 and stored it as combi_sum[4]=27+16=43.
a) ∑val[0..6] =    val[0..4] + val[5..6];
b) ∑val[2..6] =    val[2..4] + val[5..6]
c) ∑val[4..6]
a) + b) + c) = (val[0..4] + val[5..6]) + (∑val[2..4] + val[5..6]) + ∑val[4..6]
re-arranging we get:
a) + b) + c) = val[0..4] + val[2..4] + 2 * val[5..6] + ∑val[4..6]
But we know val[0..4] + val[2..4] = combi_sum[4], so
a) + b) + c) = combi_sum[4] + 2 * val[5..6] + ∑val[4..6]
q.e.d 😁
4) num_odd_arrays_ending_at[i-2] = (i-2)/2
5) we really don't need to, but we also maintain a running sum to skip an addition.
instead of doing: val[i-2] + val[i-1] + val[i], we do running_sum[i] - running_sum[i-3]
6) total = ∑val[i] + ∑combi_sum[i]


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.

The Obsession:
 Write good code that got translated to get that "faster than 100.00%" report on submission :-) 
The problem was very simple, shuffle a std::string 's' based on vector<int> 'indices'.
ex: for input -- std::string s ("rat"); vectot<int> indices{1,0,2}; -- the output std::string t = "art"
The easiest solution for this is:
string t(s.length(), 0);
for(int i = 0; i < s.length(); i++){
    t[indices[i]] = s[i];
return t;

But here is where obsession bit because of  'faster than 95.12%' feedback... and then what crazy things i tried to get that execution speed up πŸ™ˆ..
Read the constraints again, tried to take advantage of the fact that 100 is the maximum size of the input string...  switched to cycle sort kind of inplace shuffle, even tried to pre-allocate memory for the output string, and somehow hack std::string to wrap around it.. unfortunately, or should i say, fortunately, none of that worked..

The Outlook:
Then i took a pause and asked myself, what am i doing here? what does 100% faster mean? 100% faster has tuned into an obsession because it means well written code. i choose 100% faster as a parameter to measure the quality of code, but here i am obviously destroying it for the sake of 100% faster... so i stepped back and re-asserted that no, the operation is not a success if the patient is dead... Then i was like, ok lets shake the obsession away and think slowly case by case of various approaches and possible areas of improvement..

The Originality:
The simple approach is good, and only goes through the string once, but because we construct an output std:string and there is no interface that gets it to own precreated memory, we will end up initializing the output string once, and hence it takes 2n steps of assignment and O(n) extra memory.. 
The cycle sort style inplace shuffle is good, but it can also take 2n steps of comparisons though it does only n assignments.
So, what if we can cut the number of comparisons?? 
Time to do some case analysis of cycle sort style shuffle..
1) Worst case input order:
    The worst case occurs when we need to swap n/2 elements individually with each other..
    In this case, there are n/2 cycles and after each cycle we step back to the starting point of that cycle and move past it... So it takes at least 2n comparisons no matter what.
2) Best case input order:
    The best case order is when we have a perfect cycle, where we start at the beginning and all the elements are chained together neatly ending up at the starting point... 
    This is a case that can be exploited to avoid additional n comparisons.
3) Average case input order:
   It may have some split cycles, but last cycle usually ends somewhere in the middle of the string.
   Even, this case can be exploited to avoid at least n/2 additional comparisons.. 
And that kind of thinking felt like the purpose of practicing code being met and why not don the grin if it also fetches a 'faster than 100%' on the side πŸ˜‰without forgetting to ask 'can we do better'?
 
The Output:


Thursday, April 15, 2021

little joy of sorting

What good is sorting unless you really need to sort? Take a look at this problem:
/**
 * 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)

 2) The all nice and sorted way ;-)

 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;
 } 
Now, this has an *average* time complexity of O(m*logm + n*logn) for sorting, and O(m+n) for counting (i.e., everything we do after sorting both the vectors), which is much better than O(m*n) 
 
Goes on to say, "Hey, better be sorted and save yourself some free time" ;-)

Quiz: For what kind of input, will the counting complexity be the most?

*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,

$a = a + b$
$b = a - b$ # a + b - b = a
$a = a - b$ # a + b -a = b

Which i practically never needed to use, up until in a recent hobby project where the number of registers available are only two.
well my college days happened some 20 years back, so maybe i understood it at that time but not any more... use it or lose it you see.. 
When i wanted to use this trick now, what really concerned me was the overflow, and turns out the overflow would not matter. i just try to explain in simple terms as below.. 
 
For an $n$ bit width field, the largest unsigned number that can be represented is $2^n - 1$.
So for an overflow to occur on adding numbers $a$ and $b$, we would need $a$ as:
$a = 2^{n-1} + x$
and $b$ such that $x + b = 2^{n-1} + y$ where $y \ge 0$ 
From above, we can re-express $b$ as $b = 2^{n-1} + y - x$ Lets remember this as $eq(1)$ 
When we do
$a := a + b$, we have
$a := 2^{n-1} + x + b = 2^{n-1} + 2^{n-1} + y = 2^n + y$
But because we only have $n$ bits, the value stored at a would be:
$a = y$
And then we set out to do 
$b : = a - b = y - b$, so how would we do '$a - b$'?
We would first take 2's compliment of $b$ and then add it to $a$
2's compliment of $b$ is $2^n - b$
Let's now substiture $b$ from $eq(1)$, then we have,
$b := a - b = y - b = y + 2^n - b = y + 2^n - (2^{n-1} + y - x)$
Thus we get
$b := \cancel{y} + 2^n - 2^{n-1} - \cancel{y} + x = 2^{n-1} + x$
Now this last expression is exactly the same as the value originally at $a$.

Similary, we can convince ourselves for the step $a = a - b$ that follows 

But does it mean i understand the elegance of the modular arithmetic involved here?? No way :-)
 

 
 
 

Saturday, March 14, 2020

Why only 2 Weighted Medians at most?


It is intuitive to observe that in a distinct ordering of numbers, there are at most two medians.If the number of elements is odd, i.e. $2n-1$, then $n^{th}$ element is the median.
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)
 $\{(i_1 ,w_1) , (i_2, w_2) .. , (i_n, w_n)\}$ .....
to an ordering of the form: (lets call this expanded ordering)
 $\{i_1, i_1,...(w_1times), i_2, i_2, ....(w_2 times), ...., i_n, i_n, ... (w_n times) \}$
The total number of elements in the ordering would be:
$\displaystyle\sum_{1\le j \le n} w_j = W$.

Now we can easily say that the median of such an ordering would be $(\frac{W+1}{2})^{th}$ element if $W$ turns out to be odd.
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,

$\displaystyle\sum_{1 \le j \lt m} w_j + w_m + \displaystyle\sum_{m \lt j \le n} w_j = W$

Let $A = \displaystyle\sum_{1 \le j \lt m} w_j$, and $B = \displaystyle\sum_{m \lt j \le n} w_j$

  Then $A + w_m + B = W$ and by definition of weighted median, $A \le W/2$ and $B \le W/2$

Now, if we assume that: $w_m + B \le W/2$,  then we can claim that $w_{m-1}$ is also a weighted median.

This is because: $A \le W/2 \implies A - w_{m-1} \le W/2$ and of course by our assumption, we have $w_m + B \le W/2$

We have three relations to satisfy here: $1: A + w_m + B = W; 2: w_m + B \le W/2;$ and $3: A \le W/2$. 

All the three can be satisfied if and only if $4: A = W/2$ and $5: w_m + B = W/2$

Given that we already have weighted medians $w_{m-1}$ and $w_m$, the requirements in 4: and 5: above make it impossible to have any other weighted median.
Similar argument can also be made assuming that $A + w_m \le W/2$

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


Hmm 😡.. Capacity for proofs... how important is that?
No logic is sound unless it is proved. The ability to prove is a testament to one of the three things:
 a) clarity of thought,
 b) loudness of mouth,
 c) willingness to self deceive πŸ˜›.
But more often than not, i guess its useful when its a); c) is not bad either, for it counter balances its other opposite, which is self doubt πŸ˜‰
Somewhere between self doubt and self deceit lies self confidence, right? Not sure of b) but who knows, its probably used pretty widely, or? πŸ˜†


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:
From a given n-by-n matrix A, find a (i.e., any) number A(i, j) such that all valid elements in A at positions A(i-1, j), A(i+1, j), A(i, j-1), A(i, j+1) are less than or equal to A(i, j).
Valid element means that the position computed is a valid legal position in the matrix.
For example: A(-1, 0) is NOT a valid element.
Simply put, an element is a peak, when the elements if any, immediately above, below, to the left, and to the right of it, are less than or equal to the given element.

In the example 5-by-5 matrix below, the peak elements are highlighted.
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

The Divide and Conquer algorithm(s) to efficiently find "a" peak is(are) rather simple, but, is the proof of their correctness as simple?
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]
The complexity of this algorithm is O(nlogn).
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.

Now lets looks at the O(n) algorithm:
O(n)  2-D Peak finding algorithm
The O(n) algorithm extends this valley building effort both across the middle column as well as the middle row, and recurses into one of the quadrants where a peak is guaranteed to be found.
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
3.b. 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 $m_c$ + 1 to end. Below table gives an example of this case:
$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
Similarly it follows for other cases as below...
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.
       
Now that we stated the algorithm, rather elaborately, lets try to prove it by starting with base case.
[proof: O(n)  2-D Peak finding algorithm]
Consider the base case when only one row $r$ and columns $c_x$ to $c_y$ are left.
The maximum of that row, say, A(r,j) is the peak of that one row, r.
Lets unwind the recursion and add back the row r-1 to the array.
A(r, j) ceases to be a peak only if A(r-1, j) > A(r, j)
And steps 3.a, 3.d, 3.f, 3.h are the ones responsible for peeling off rows above.
Because of preconditions in the those steps, not only  A(r-1, j), but for all c such that $c_x$ < c < $c_y$; A(r-1, c) ≤= A(r, j)

On similar lines, the proof can be extended to the case when we add back row r+1, as well as when the base case is a column.
[proof:end]