Wednesday, August 2, 2023

Leetcode M808 Soup Servings Convergence Analysis

M808 description:
There are two types of soup: type A and type B. Initially, we have $n$ ml of each type of soup. There are four kinds of operations: 1. Serve 100 ml of soup A and 0 ml of soup B, 2. Serve 75 ml of soup A and 25 ml of soup B, 3. Serve 50 ml of soup A and 50 ml of soup B, and 4. Serve 25 ml of soup A and 75 ml of soup B. When we serve some soup, we give it to someone, and we no longer have it. Each turn, we will choose from the four operations with an equal probability 0.25. If the remaining volume of soup is not enough to complete the operation, we will serve as much as possible. We stop once we no longer have some quantity of both types of soup. Note that we do not have an operation where all 100 ml's of soup B are used first. Return the probability that soup A will be empty first, plus half the probability that A and B become empty at the same time. Answers within $10^{-5}$ of the actual answer will be accepted. Constraints: $0<=n<=10^9$
It's not difficult to find the algorithm, if one notices the recursive nature of the problem: State $(A,B) \rightarrow (A-100,B)$ or $(A-75,B-25)$ or $(A-50,B-50)$ or $(A-25,B-75)$.
But the upper bound of $n$ is $10^9$. Are we supposed to build a $10^9$ by $10^9$ table? How much time would that take?

It's easy to see that we can simplify the conditions a little bit. Apparently if the quantity is not a multiple of 25, the situation is the same as when the quantity is the next multiple of 25. So let's convert $n$ to the ceiling of its ratio to 25.
Now we can change the 4 operations to: $(m,n) \rightarrow (m-4,n-0)$ or $(m-3,n-1)$ or $(m-2,n-2)$ or $(m-1,n-3)$.
So it's easy to tell that the probability to win from the state $(m,n)$ is, $p(m,n)=\frac{1}{4}(p(m-4,n-0)+p(m-3,n-1)+p(m-2,n-2)+p(m-1,n-3))$.
The base cases are, $p(m,n)=\begin{cases} 0 & \text{if $m>0,n\leq0$}\\ 1 & \text{if $m\leq 0,n>0$}\\ 0.5 & \text{if $m\leq 0,n\leq 0$}\end{cases}$.

We could apply this subroutine recursively until we reach one of the base cases, but that would be very inefficient. The first important thing is we should memoize the intermediate results. We can do that in multiple ways. I chose the bottom up approach, i.e. just build the entire table. And since the conditions don't change, once I have the table, I can simply take a value from the table for any input.

But still, that would be a $4\times 10^7$ by $4\times 10^7$ table. Can we do better than that?
Here's the second crucial thing to notice: The larger $n$ is, the higher the probability that soup A will be empty first becomes, because there's a higher chance that we will serve more soup A than soup B in each step, we would expect soup A to decrease faster than soup B. If it keeps increasing, we can stop at the $n$ such that $p(n,n)>1-10^{-5}$, because for larger inputs, the difference between the result and 1 is always less than $10^{-5}$, so we can simply return 1!

The following program, which I submitted, is based on this intuition. It took 0ms, pretty fast.
```cpp
vector<vector<double>> tab;
bool first=true;
int oneTh=INT_MAX;
double check(int i,int j){
    if(i<0) return (1.0+(j>=0))/2;
    if(j<0) return 0;
    return tab[i][j];
}
double avg(int i,int j){
    if(i==-1){if(j==-1) return 0.5;return 1.0;}
    if(j==-1) return 0.0;
    return 0.25*(check(i-4,j)+check(i-3,j-1)+check(i-2,j-2)+check(i-1,j-3));
}
void init(){
    first=false;
    for(int n=1;n<=40000000;++n){
        for(int i=0;i<n-1;++i) tab[i].emplace_back(avg(i,n-1));
        if(1.0-avg(n-1,n-1)<0.00001){oneTh=n;break;}
        vector<double> tmp;
        for(int i=0;i<n;++i) tmp.emplace_back(avg(n-1,i));
        tab.emplace_back(move(tmp));
        // cout<<"n="<<n<<endl;
        // for(auto& v:tab){
        //     for(auto e:v) cout<<e<<",";
        //     cout<<endl;
        // }
    }
}
class Solution {
public:
    double soupServings(int n) {
        if(first) init();
        if((n=(n+24)/25)>=oneTh) return 1.0;
        if(n==0) return 0.5;
        return tab[n-1][n-1];
    }
};
```
(In the submission page, all the sample submissions that take less than 100ms use some magic number like "if(n>5000)" or "if(n>10000)", but the threshold could be easily determined during the computation.)
The main reason for this article is not about the program, so let's go back to the assumption. The assumption is based on our intuition, but can we prove it rigorously? Let's think about it!
The editorial of that problem on leetcode demonstrates the law of large numbers. But the fact that the probability converges to 1 doesn't justify the statement that after we first encounter a value $\geq 1-0.00001$, all the values afterwards satisfy it too.
Consider the function $f(x)=e^{-x/100}\cos^2(x/100)$. We know that it converges to 0 as $x\rightarrow\infty$, but that doesn't mean that we can stop when we encounter a value less than the criterion for the first time, because the values will keep increasing and decreasing alternately! So what we really need is a proof that it increases monotonically, or an upper bound such that even if it starts to increase again, it will never exceed the upper bound.

The probabilities that we calculate form a table, which we can represent as a grid graph. To make it easier to see, let's rotate it by $45^{\circ}$:
The value at each cell is the average of another 4 cells, and if the cells are outside the grid, the value is 0 or 1 or 0.5 respectively in the regions shown on the figure:
(From here, immediately we find two optimizations to our program: First, it's unnecessary to calculate half of the numbers in the grid. Because we only need the values at $(n,n)$, all the elements that we'll ever use are either in $(odd,odd)$ cells or $(even,even)$ ones. Second, we only need one layer of the values to construct the next layer. So instead of saving the entire table, we could just use two vectors to store the current layer and the next layer, which saves a lot of space. That will do for all odd $n$s, then we do the same to fill in all the even ones. Another possible optimization is, to find the last $p(n,n)$, we just need the values of the cells between these two lines, so we can start to shrink the vectors after half way through, which cuts the space complexity by half again. But that's only if we already know the last value of $n$, and it's probably going to be a little more complicated to implement.)

(From here on, "next layer" means two rows downwards, e.g. from coordinate (1,1) to (3,3) is one layer down.)

What we want is, a proof that the values at the cells in the middle increases monotonically as it goes downward, or an upper bound of those values as a function of the number of steps.

Since we've seen the law of large numbers, maybe we can continue on that path?
The central limit theorem states that with i.i.d variables, their average will converge to normal distribution.
The problem can be seen as a random walk: We start at the origin, each time we may take 1 step left, or no move, or 1 step right, or 2 steps right, each with probability 1/4, and we want to find out the probability that we are at location $l$ after $n$ steps. It's easy to find the expectation of the variables $\mu_0=(-1+0+1+2)/4=1/2$, and the variance $\sigma_0^2=(1+0+1+4)/4 - 1/4=5/4$.

If we start from $(n,n)$, we will move out of the grid in at most $\frac{n}{2}$ steps. By then, the range of our final location is $[-\frac{n}{2},n]$. The expectation of the summation is $\mu=\frac{n}{4}$. Let's show them on the figure:
If you're thinking "wait a minute - once we reach either one of the boundaries, we can't keep going, so how can we map the probability distribution along the boundaries to the line?", that's good thinking! Because indeed, it's not a one on one map. But we only cares about the summation of the probabilities, and notice that if our final location is in the 0 or 1 region, we must have come through the corresponding boundary, and if we come through either one of the boundaries, we won't be able to leave that region. That means, the summation of the probabilitese from $-\frac{n}{2}$ to 0 and from 0 to $n$ corresponds to the probability of B empty and A empty respectively. There is a discrepancy, though, because if we reach the first 3 rows inside the grid, we may enter the 0.5 region. But as $n$ increases, that probability converges to zero.

So, let's compute the probability distribution of our final location on that horizontal line. The central limit theorem states that the probability distribution should converge to a normal distribution with expectation  $\mu=\frac{n}{4}$ and variance $\sigma^2=\frac{5n}{8}$. The probability of A empty is
$$P=1-\int_{-\infty}^0 \mathcal{N}\left(\frac{n}{4},\sqrt{\frac{5n}{8}}\right) dx$$
We can manipulate the variable a little to convert it to an integration on the standard normal distribution:
$$P=1-\sqrt{\frac{5n}{8}}\int_{-\infty}^{-\frac{n}{4}\sqrt{\frac{8}{5n}}} \mathcal{N}(0,1) dx$$
$$=1-\sqrt{\frac{5n}{8}}\frac{1+\text{erf}(-\sqrt{\frac{n}{20}})}{2}$$

Now we can easily find out the threshold based on this approximation, in one line:
```cpp
    for(int i=1;i<2000;++i) if((1+erf(-sqrt(i/20.0)))*sqrt(5*i/32.0)<0.00001) {cout<<"i="<<i;break;}
```
We get $i=230$, which corresponds to an original value of $230\times 25=5750$.
The threshold I got from my program is $oneTh=179$, which corresponds to an original value of $4475$.
How close is this approximation? Let's find out the differences:
```cpp
    for(int i=1;i<tab.size();++i) cout<<setw(8)<<tab[i][i]<<" - "<<setw(8)<<(f=1-(1+erf(-sqrt(i/20.0)))*sqrt(5*i/32.0))<<" = "<<setw(8)<<tab[i][i]-f<<endl;
```
Output:
   0.625 - 0.702813 = -0.0778132
 0.65625 -    0.634 = 0.0222501
 0.71875 - 0.600243 = 0.118507
0.742188 - 0.583299 = 0.158888
0.757812 - 0.576178 = 0.181635
0.785156 - 0.575349 = 0.209808
0.796875 - 0.578759 = 0.218116
0.817871 - 0.585105 = 0.232766
0.827637 - 0.593511 = 0.234126
0.844849 - 0.603362 = 0.241487
0.852173 - 0.614214 = 0.237959
0.866699 - 0.625739 =  0.24096
0.872559 -  0.63769 = 0.234868
0.884827 - 0.649881 = 0.234946
0.889633 - 0.662167 = 0.227466
0.900076 - 0.674438 = 0.225637
0.904058 - 0.686609 =  0.21745
0.913005 - 0.698613 = 0.214392
0.916344 -   0.7104 = 0.205944
0.924045 - 0.721932 = 0.202114
 0.92687 - 0.733179 =  0.19369
0.933526 - 0.744121 = 0.189404
 0.93593 - 0.754743 = 0.181187
0.941703 - 0.765035 = 0.176668
0.943762 - 0.774991 = 0.168771
0.948783 - 0.784609 = 0.164174
0.950555 - 0.793889 = 0.156667
0.954934 - 0.802832 = 0.152102
0.956464 - 0.811443 = 0.145021
0.960291 - 0.819727 = 0.140564
0.961618 -  0.82769 = 0.133927
0.964968 -  0.83534 = 0.129628
0.966121 - 0.842684 = 0.123438
0.969061 -  0.84973 = 0.119331
0.970066 - 0.856487 = 0.113579
0.972648 - 0.862964 = 0.109684
0.973525 - 0.869169 = 0.104356
0.975797 - 0.875113 = 0.100684
0.976565 - 0.880803 = 0.0957618
0.978566 - 0.886249 = 0.0923166
0.979239 -  0.89146 = 0.0877792
0.981004 - 0.896444 = 0.0845595
0.981595 - 0.901211 = 0.0803844
0.983153 - 0.905767 = 0.0773854
0.983673 - 0.910123 = 0.0735498
0.985049 - 0.914285 = 0.0707642
0.985506 - 0.918261 = 0.0672452
0.986724 -  0.92206 = 0.0646639
0.987127 - 0.925688 = 0.0614394
0.988204 - 0.929152 = 0.0590524
 0.98856 -  0.93246 = 0.0561005
0.989515 - 0.935617 = 0.0538971
0.989829 - 0.938631 = 0.0511974
0.990675 - 0.941508 = 0.0491667
0.990953 - 0.944253 = 0.0466995
0.991703 - 0.946872 = 0.0448306
0.991949 - 0.949371 = 0.0425775
0.992614 - 0.951755 = 0.0408595
0.992832 - 0.954029 = 0.0388033
0.993423 - 0.956197 = 0.0372257
0.993616 - 0.958266 = 0.0353502
0.994141 - 0.960238 = 0.0339031
0.994312 - 0.962119 = 0.0321933
0.994779 - 0.963912 = 0.0308669
 0.99493 - 0.965622 = 0.0293089
0.995346 - 0.967251 = 0.0280942
 0.99548 - 0.968805 = 0.0266751
 0.99585 - 0.970286 = 0.0255634
0.995969 - 0.971698 = 0.0242713
0.996298 - 0.973043 = 0.0232546
0.996404 - 0.974325 = 0.0220786
0.996697 - 0.975547 = 0.0211493
0.996791 - 0.976712 = 0.0200793
0.997052 - 0.977821 = 0.0192303
0.997136 - 0.978879 = 0.018257
0.997368 - 0.979886 = 0.0174819
0.997443 - 0.980846 = 0.0165967
 0.99765 - 0.981761 = 0.0158893
0.997716 - 0.982632 = 0.0150846
0.997901 - 0.983462 = 0.0144392
 0.99796 - 0.984252 = 0.0137078
0.998125 - 0.985006 = 0.0131192
0.998177 - 0.985723 = 0.0124545
0.998324 - 0.986406 = 0.011918
0.998371 - 0.987057 = 0.0113141
0.998502 - 0.987677 = 0.0108252
0.998544 - 0.988268 = 0.0102766
0.998661 -  0.98883 = 0.00983115
0.998699 - 0.989366 = 0.00933291
0.998803 - 0.989876 = 0.00892724
0.998836 - 0.990362 = 0.00847478
 0.99893 - 0.990824 = 0.00810543
0.998959 - 0.991265 = 0.00769461
0.999043 - 0.991684 = 0.0073584
0.999069 - 0.992084 = 0.00698545
0.999144 - 0.992464 = 0.00667947
0.999167 - 0.992826 = 0.00634094
0.999234 - 0.993171 = 0.00606255
0.999255 -   0.9935 = 0.0057553
0.999314 - 0.993812 = 0.00550206
0.999333 -  0.99411 = 0.00522323
0.999386 - 0.994393 = 0.00499291
0.999403 - 0.994663 = 0.00473991
0.999451 -  0.99492 = 0.00453047
0.999466 - 0.995165 = 0.00430092
0.999508 - 0.995398 = 0.00411051
0.999522 - 0.995619 = 0.00390226
 0.99956 -  0.99583 = 0.00372917
0.999572 - 0.996031 = 0.00354026
0.999606 - 0.996223 = 0.00338295
0.999616 - 0.996405 = 0.0032116
0.999647 - 0.996578 = 0.00306864
0.999656 - 0.996743 = 0.00291323
0.999684 -   0.9969 = 0.00278334
0.999692 -  0.99705 = 0.0026424
0.999717 - 0.997192 = 0.0025244
0.999724 - 0.997328 = 0.00239658
0.999746 - 0.997457 = 0.00228939
0.999753 - 0.997579 = 0.00217349
0.999772 - 0.997696 = 0.00207614
0.999779 - 0.997808 = 0.00197105
0.999796 - 0.997913 = 0.00188264
0.999802 - 0.998014 = 0.00178736
0.999817 -  0.99811 = 0.00170708
0.999822 - 0.998202 = 0.0016207
0.999836 - 0.998288 = 0.0015478
0.999841 - 0.998371 = 0.0014695
0.999853 -  0.99845 = 0.00140331
0.999857 - 0.998525 = 0.00133233
0.999868 - 0.998596 = 0.00127225
0.999872 - 0.998664 = 0.00120791
0.999882 - 0.998729 = 0.00115337
0.999885 -  0.99879 = 0.00109505
0.999894 - 0.998849 = 0.00104556
0.999897 - 0.998904 = 0.000992697
0.999905 - 0.998957 = 0.000947776
0.999908 - 0.999008 = 0.000899868
0.999915 - 0.999056 = 0.000859103
0.999917 - 0.999101 = 0.000815685
0.999924 - 0.999145 = 0.000778694
0.999926 - 0.999186 = 0.000739347
0.999932 - 0.999226 = 0.000705783
0.999933 - 0.999263 = 0.000670127
0.999939 - 0.999299 = 0.000639675
 0.99994 - 0.999333 = 0.000607365
0.999945 - 0.999365 = 0.000579738
0.999946 - 0.999396 = 0.00055046
0.999951 - 0.999425 = 0.000525398
0.999952 - 0.999453 = 0.000498869
0.999956 -  0.99948 = 0.000476135
0.999957 - 0.999505 = 0.000452098
 0.99996 - 0.999529 = 0.000431477
0.999961 - 0.999552 = 0.000409699
0.999964 - 0.999573 = 0.000390995
0.999965 - 0.999594 = 0.000371264
0.999968 - 0.999614 = 0.0003543
0.999969 - 0.999632 = 0.000336424
0.999971 -  0.99965 = 0.00032104
0.999972 - 0.999667 = 0.000304845
0.999974 - 0.999683 = 0.000290893
0.999975 - 0.999699 = 0.000276222
0.999977 - 0.999713 = 0.00026357
0.999977 - 0.999727 = 0.000250279
0.999979 -  0.99974 = 0.000238807
 0.99998 - 0.999753 = 0.000226767
0.999981 - 0.999765 = 0.000216365
0.999982 - 0.999776 = 0.000205459
0.999983 - 0.999787 = 0.000196027
0.999984 - 0.999798 = 0.000186147
0.999985 - 0.999807 = 0.000177596
0.999985 - 0.999817 = 0.000168647
0.999986 - 0.999826 = 0.000160895
0.999987 - 0.999834 = 0.000152789
0.999988 - 0.999842 = 0.00014576
0.999988 -  0.99985 = 0.000138418
0.999989 - 0.999857 = 0.000132047
0.999989 - 0.999864 = 0.000125396

Indeed, the numeric evidence of convergence is very convincing.

But wait a minute... Does this count as a proof? Is it rigorous?

The simple answer is, no.
Because we can tell that it converges to normal distribution as $n$ goes to infinity, but we still don't know how fast it converges to that! We can tell that for the normal distribution we can stop at a certain $n$, but we don't have proof that the normal distribution is close enough to the actual distributon!
Wikipedia states that
If the third central moment $E[(X_1-\mu)^3]$ exists and is finite, then the speed of convergence is at least on the order of $1/\sqrt{n}$ (see Berry–Esseen theorem). Stein's method can be used not only to prove the central limit theorem, but also to provide bounds on the rates of convergence for selected metrics.
Apparently the third central moment is finite for our distribution, but a convergence speed of $1/\sqrt{n}$ is too slow for our problem. The criterion is $10^{-5}$, that would require $10^{10}$ steps, which corresponds to $n=2\times 10^{10}$, larger than the maximum of the constraint! I haven't looked into this "Stein's method", I wonder if it provides a higher lower bound on the speed. Also notice that, we not only need to bound the difference between the probability densities, we also need to bound the difference between the cumulative distribution function, since what we need is the value of the integration. We can also tell that the value approximated by normal distribution seems to be a lower bound of the actual value, except for the first one. But unless we can prove it, it's just an observation. What if we don't use an approximation? What if we just sum up the discrete probabilities? If we use the recursive relationship twice, we get: $p(m,n)=\frac{1}{4}(p(m-4,n)+p(m-3,n-1)+p(m-2,n-2)+p(m-1,n-3))$ $=\left(\frac{1}{4}\right)^2(p(m-8,n)+2p(m-7,n-1)+3p(m-6,n-2)+4p(m-5,n-3)+3p(m-4,n-4)+2p(m-3,n-5)+p(m-2,n-6))$ And the pattern continues. The coefficients goes like this: 1 1 1 1 1 2 3 4 3 2 1 1 3 6 10 12 12 10 6 3 1 1 4 10 20 31 40 44 40 31 20 10 4 1 (more rows up to the 11th: 1,5,15,35,65,101,135,155,155,135,101,65,35,15,5,1, 1,6,21,56,120,216,336,456,546,580,546,456,336,216,120,56,21,6,1, 1,7,28,84,203,413,728,1128,1554,1918,2128,2128,1918,1554,1128,728,413,203,84,28,7,1, 1,8,36,120,322,728,1428,2472,3823,5328,6728,7728,8092,7728,6728,5328,3823,2472,1428,728,322,120,36,8,1, 1,9,45,165,486,1206,2598,4950,8451,13051,18351,23607,27876,30276,30276,27876,23607,18351,13051,8451,4950,2598,1206,486,165,45,9,1, 1,10,55,220,705,1902,4455,9240,17205,29050,44803,63460,82885,100110,112035,116304,112035,100110,82885,63460,44803,29050,17205,9240,4455,1902,705,220,55,10,1, 1,11,66,286,990,2882,7282,16302,32802,59950,100298,154518,220198,291258,358490,411334,440484,440484,411334,358490,291258,220198,154518,100298,59950,32802,16302,7282,2882,990,286,66,11,1,) On each row, each number is the sum of 4 numbers of the previous row: 2 position to the left of the one above it, 1 position to the left, directly above it, and one to the right, and if it goes out of the boundary, it's considered 0. Over all, there's an extra coefficient of $\frac{1}{4^t}$ to normalize the probability to one. Each row has one more element on the left and two more on the right. The vertical line in our figure corresponds to the vertical line of {1,3,10,31...}. If we take the 4th layer of the even cells (coordinate (8,8) in the table below) as an example, the total probability is $$\frac{1}{4^4}(31\times 0.5 + 40 + 44 + 40 + 31 + 20 + 10 + 4 + 1)=0.802734375$$ (For odd layers, it will recurse back to the row above the line in the figure, so there will be 3 elements multiplied by 0.5. Just a little more complicated.) This is larger the actual result 0.796875. The reason is simple: When we enter the 0 region, there's a chance that we land at the boundary (coordinate (4,-1)), then there's a 1/4 chance that we enter 0.5, which is impossible since we should already stop once we're outside the grid. There are 3 ways to get to that cell, this gives an extra value of $0.5(3\times \frac{1}{4^3} \times \frac{1}{4})$. Removing this discrepancy gives the correct result. But as $n$ increases, the discrepancy should converge to 0 exponentially. So, if we can prove that the sum of the numbers before the middle line converges to zero monotonically, then we're done. The first few sums are $$(1 + 0.5)/4 =0.375$$ $$(1+2+3\times 0.5)/4^2=0.28125$$ $$(1+3+6+10\times 0.5)/4^3=0.234375$$ $$(1+4+10+20+31\times 0.5)/4^4=0.197265625$$ If we can find a closed form of this summation, it would be very helpful. It's easy to find the equation for the first 4 terms from the recurrence relationship. The first is always 1, the second is $t$, the third is $1+\dots+t = t(t+1)/2$, the fourth is $t(t+1)(t+2)/6$ based on the fact $f_4(t)-f_4(t-1)=t(t+1)/2$, the fifth has this recurrence relation $f_5(t)-f_5(t-1)=(t-1)(t^2+4t+6)/6$ and increases as $t^4/24$. So in general, the $k$th term is a polynomial of degree $k-1$ where the coefficient of the leading term is $\frac{1}{(k-1)!}$. It's quite similar to the binomial coefficients, just more complicated. Maybe the techniques to bound the partial sum of binomial coefficients can be applied here, too. We're basically trying to bound the sum of the first $t+1$ (or $t+2$ for odd layers) terms of these unnamed coefficients, where the length of the row is $3t+1$. Based on the fact that it's less than the Gaussian distribution approximation, maybe the Chernoff bound can be applied here, too. This seems to be the most promising direction. I'll take a look at these when I have more time...
[2023 08 03 update]

I figured out how to prove the upper bound after giving it a litte more thought!
So, basically the idea is (Thanks to user2575jO on leetcode), we can write one step as two steps merged together: choosing from $\{-1,0,1,2\}$ each with $p=1/4$ is equivalent to first choosing from $\{0,2\}$ with $p=1/2$ and then choosing from $\{-1,0\}$ with $p=1/2$, then add the result together. The distributions are exactly the same.
Now if we move $t$ steps, the probability that we're at a location $\leq 0$ is
$$P(t)=\frac{1}{4^t}\sum_{l=0}^t\left(\binom t l\sum_{r=0}^{l/2}\binom t r\right)$$
where $l$ is the number of $-1$ moves and $r$ is the number of $+2$ moves. $l$ can be from 0 to $t$, and $r$ must be between $0$ and $\frac{l}{2}$, so that the final location $-l+2r\leq 0$.

Knowing that $r\leq \frac{t}{2}$, we can apply the Chernoff bound directly (with $p=\frac{1}{2}$):
$$Pr\left(r<\frac{l}{2}\right)=Pr\left(r<\frac{t}{2}-\left(\frac{t}{2}-\frac{l}{2}\right)\right)\leq\exp{\left(-2t\left(\frac{\frac{t}{2}-\frac{l}{2}}{t}\right)^2\right)}$$
$$Pr\left(r<\frac{l}{2}\right)\leq\exp{\left(-\frac{(t-l)^2}{2t}\right)}$$

Replace $2^{-t}\sum_{r=0}^{l/2}\binom t r$ with the upper bound above, now we have
$$P(t)\leq 2^{-t}\sum_{l=0}^t\binom t l\exp{\left(-\frac{(t-l)^2}{2t}\right)}$$
To simplify this expression, noticing the binomial coefficient, we really want to write the last term as a power of $l$. Let's expand it:
$$P(t)\leq 2^{-t}\sum_{l=0}^t\binom t l\exp{\left(-\frac{t^2-2lt+l^2}{2t}\right)}=2^{-t}e^{-t/2}\sum_{l=0}^t\binom t l e^l e^{-\frac{l^2}{2t}}$$
We are almost there, except for the annoying $e^{-\frac{l^2}{2t}}$ term. It's a normal distribution, centered at 0. If we can bound it by some function like $a e^{-bl}$, then we can write the term as a power of $l$. And that seems doable!
$$e^{-\frac{l^2}{2t}}\leq ae^{-bl}$$
$$e^{-\frac{l^2}{2t}+bl}\leq a$$
$$e^{-\frac{1}{2t}(l-bt)^2+b^2t/2}\leq a$$
If we choose $a=e^{b^2t/2}$, the inequality is always true. Apply this inequality with an arbitrary $b$, we get
$$P(t)\leq 2^{-t}e^{-t/2}\sum_{l=0}^t\binom t l e^l e^{b^2t/2} e^{-bl}$$
$$=2^{-t}e^{-t/2}e^{b^2t/2}\sum_{l=0}^t\binom t l e^l e^{-bl}$$
$$=2^{-t}e^{\frac{b^2-1}{2}t}\left(1+e^{1-b}\right)^t$$
$$=\left(\frac{1}{2}\left(e^{\frac{b^2-1}{2}}+e^{\frac{(b-1)^2}{2}}\right)\right)^t$$
Now we want to choose a $b$ to minimize this expression. Taking the derivative and we find this equation
$$be^{b-1}+b-1=0$$
Solving it numerically, we find
$$b=0.598942$$
Putting it back to the inequality above, we get
$$P(t)\leq 0.9047174^t$$
The threshold is at
$$0.9047174^t=10^{-5}$$
$$t=114.97\dots$$
Remember that $t$ is the number of steps, so this corresponds to $n=2t=230$, the same result as the Gaussian approximation above! (Probably not a coincidence, because even though they are very different for small $n$s, when I change $10^{-5}$ to $5\times 10^{-6}$, they still give the same result $n=244$. I think this has the same asymptotic behavior as the Gaussian approximation.) But this time the bound is proved.

(The $l$ should be replaced by $l+1$ or $l+2$ somewhere, because the value in the middle is 0.5, not 1, and in odd layers it recurses back to the layer of three 0.5s, so we must shift it one more step. But these are just small details, it shouldn't change the answer by much...)

[update end]
What else can we do, then? In the previous calculations, we're trying to find an upper bound. If we have an upper bound, we can use that to estimate where to stop, e.g. if we can prove that the normal distribution is an upper bound, we cau use the threshold $n=230$. But if we can prove the monotonicity, we can use the threshold at exactly where the value exceeds the criterion i.e. $n=179$, which is a stronger statement.

Let's go back to the grid, maybe.
The proof would be trivial if all the values increase monotonically along any vertical line. But unfortunately that is not the case. The elements in the top $10 \times 10$ grid are

     0.625,      0.75,     0.875,         1,         1,         1,         1,         1,         1,         1,
       0.5,     0.625,      0.75,   0.90625,    0.9375,   0.96875,         1,         1,         1,         1,
     0.375,       0.5,   0.65625,    0.8125,     0.875,    0.9375,  0.976562,  0.984375,  0.992188,         1,
      0.25,   0.40625,    0.5625,   0.71875,    0.8125,  0.890625,    0.9375,  0.960938,  0.984375,  0.994141,
   0.15625,    0.3125,   0.46875,     0.625,  0.742188,  0.828125,  0.890625,    0.9375,  0.966797,  0.980469,
     0.125,      0.25,     0.375,   0.53125,   0.65625,  0.757812,   0.84375,  0.902344,    0.9375,  0.960938,
   0.09375,    0.1875,  0.304688,  0.453125,  0.578125,    0.6875,  0.785156,  0.851562,  0.900391,  0.941406,
    0.0625,  0.140625,      0.25,  0.382812,       0.5,  0.617188,   0.71875,  0.796875,  0.863281,  0.912109,
 0.0390625,  0.109375,  0.203125,    0.3125,  0.429688,  0.546875,  0.652344,  0.742188,  0.817871,   0.87207,
   0.03125, 0.0859375,   0.15625,  0.253906,  0.367188,  0.480469,  0.585938,  0.683594,  0.763672,  0.827637,
   
The diagonal line is indeed increasing, but the third diagonal line has elements 0.875, 0.90625, 0.875, 0.890625, 0.890625, 0.902344, 0.900391, 0.912109... which increases and decreases alternately. And that is not the only diagonal line that shows this behavior. Let's take the 5th diagonal line as an example, the first few elements are

       1,       1,0.984375,0.984375,0.980469,0.980469,0.978516,0.979492,0.978271,0.979492,
0.979004,0.980286,0.980179,0.981461,0.981567,0.982796, 0.98303, 0.98418,0.984484,0.985547,
0.985884,0.986855,0.987203,0.988086, 0.98843,0.989229, 0.98956,0.990281,0.990595,0.991244,
0.991538, 0.99212,0.992393,0.992915,0.993167,0.993635,0.993865,0.994284,0.994495, 0.99487,
0.995062,0.995397,0.995572,0.995872, 0.99603,0.996298,0.996441,0.996681, 0.99681,0.997024,

It's not easy to tell if it increases or decreases. Let's see their differences:

           0,   -0.015625,           0, -0.00390625,           0, -0.00195312, 0.000976562,  -0.0012207,   0.0012207,-0.000488281,
  0.00128174,-0.000106812,  0.00128174, 0.000106812,  0.00122833, 0.000234604,  0.00115013, 0.000303984,  0.00106215, 0.000337005,
 0.000971675, 0.000347495, 0.000883162, 0.000343844, 0.000799075, 0.000331543, 0.000720632, 0.000314187,  0.00064834,  0.00029413,
 0.000582274, 0.000272915, 0.000522257, 0.000251551,  0.00046797,  0.00023068, 0.000419023, 0.000210703, 0.000374994, 0.000191853,

It alternates for about 12 times, then it keeps increasing.
Remembering that there are two independent sets of layers, we should find the differences within each set. The differences within the odd layers are

   -0.015625, -0.00390625, -0.00195312,-0.000244141, 0.000732422,  0.00117493,  0.00138855,  0.00146294,  0.00145411,  0.00139916,
  0.00131917,  0.00122701,  0.00113062,  0.00103482,  0.00094247,  0.00085519, 0.000773808, 0.000698651, 0.000629726, 0.000566848,
  
and the even layers

   -0.015625, -0.00390625,-0.000976562,           0, 0.000793457,  0.00117493,  0.00133514,  0.00138474,  0.00136614,  0.00130868,
  0.00123066,  0.00114292,  0.00105218, 0.000962528, 0.000876404, 0.000795173, 0.000719521, 0.000649703, 0.000585698, 0.000527314,

Now they no longer alternate.
Is it possible to prove that it can only decrease for a finite amount of times before it starts to increase monotonically?
Let's take a look at a diagonal line that's further away. Here are the differences of the elements on the 10th diagonal:
odd layers:
           0,-0.000488281,-0.000488281,-0.000366211,-0.000267029,-0.000171661,-9.20296e-05, -3.0756e-05,   1.508e-05, 4.81904e-05,
 7.11586e-05, 8.62759e-05, 9.54153e-05, 0.000100072, 0.000101421, 0.000100375,  9.7634e-05, 9.37348e-05,  8.9083e-05, 8.39835e-05,
even layers:
           0,-0.000366211,-0.000427246,-0.000335693,-0.000244141,-0.000161171,-9.08375e-05,-3.57032e-05, 5.90086e-06, 3.63067e-05,
  5.7715e-05, 7.20862e-05, 8.10546e-05, 8.59372e-05, 8.77771e-05, 8.73906e-05, 8.54098e-05, 8.23208e-05, 7.84945e-05, 7.42116e-05,

It seems that the further away from the middle, the more times it would decrease.

Let's see if we can find out more about this pattern! In the following two figures (first the odd layers, second the even layers), a cell $(m,n)$ is a black square if $p(m,n)-p(m-2,n-2)$ is negative, a white square if positive, and a dash if zero.
There is definitely a pattern here. The boundary between increasing cells and decreasing cells shifts further and further away from the middle diagonal line. If this can be proved, then it's obvious that the diagonal line doesn't have decreasing cells. But I haven't found a proof of that.

How about we consider something more solid - something that we can actually prove?
If we look at the table again, it's easy to see that the values increase rightward and decrease downward. Can we do better than that?
It's easy to see that the number on the left boundary decrease to $1/4$ of the value 4 steps (or 1 layer) above it. We can also tell that there are more and more 1s on the right boundary. Let's mark them on the figure:
The value of the light green cell is 1/4 of the green cell, whose value is 1/4 of the deep green cell.
With the same reasoning, we can tell that there's a similar pattern on the right boundary. There is one more cell of value 1 after each layer, and also the value of the cell next to the 1 (the blue cells) increases monotonically downward. The difference between 1 and the value of the blue cell is 1/4 of the difference between 1 and the light blue cell, and the difference between 1 and and the deep blue cell is 1/4 of the previous one. So the value of the cells along this line converges to 1. By induction, it's easy to show that any cell increases value along that line downward.

So we have two lines that guarantee to increase or decrease downward. Is there a line of equal values, then? If so, any angle between the increasing line and equal line should be increasing, and the other angles should be decreasing. But the angle of the equal line depends on where the cell is, because if the cell already has value 1, the equal line is the same as the increasing line. So as the location of the cell changes from the right most to the left most, the angle between the equal line and the increasing line increases from 0 to some positive value. The two figures above seems to support this statement. If we can prove that when the cell is in the middle, the equal line always points to the lower left, then we can tell that along the middle diagonal, the value increases monotonically. 

Another thing that I noticed is, the summation of the values on the next layer is always 2.5 larger than the current layer, since all the values will appear exactly 4 time in the average, the extra 0s don't contribute, and the 1s contribute exactly $(4+3+2+1)/4=2.5$ to the summation of the next layer. I don't know how to use this fact in a proof, though.

Anyway, this summarizes my thoughts on this problem so far. If anyone finds a rigorous proof of this problem, I'd be glad to hear it!

Friday, June 9, 2023

Leetcode M1318 Minimum Flips to Make a OR b Equal to c, E1351 Count Negative Numbers in a Sorted Matrix

M1318 description:
  Given 3 positives numbers a, b and c. Return the minimum flips required in some bits of a and b to make ( a OR b == c ). (bitwise OR operation).
  Flip operation consists of change any single bit 1 to 0 or change the bit 0 to 1 in their binary representation.
Constraints:
  1 <= a <= $10^9$
  1 <= b <= $10^9$
  1 <= c <= $10^9$
This problem is very easy, a typical algorithm is to go through each bit position of the three numbers. If $c$ at that position is 1 but $a|b$ at that position is 0, we add one flip, and if $c$ is 0 we add one if $a$ is 1 and another one if $b$ is 1 at that position. (Since the numbers are positive, we don't need to check the 32nd bit.)

But putting these together, let's make a table:
c 1, a|b 0 : add 1
c 0, a&b 1 : add 2
c 0, a|b 1 : add 1
We can do the bitwise operations to the numbers beforehand, instead of checking each digit in the loop. So here's the first version that I submitted:
```cpp
class Solution {
public:
    int minFlips(int a, int b, int c) {
        int r=0;
        int t=1;
        int abc=(a|b)^c,aAb=a&b;
        for(int i=0;i<31;++i){
            if(abc&t){
                r++;
                if(aAb&t) r++;
            }
            t<<=1;
        }
        return r;
    }
};
```
Then I noticed that, I can also combine the '&' operation to get rid of the branching. The version without the branching reads,
```cpp
class Solution {
public:
    int minFlips(int a, int b, int c) {
        int r=0;
        int abc=(a|b)^c,aAb=a&b&abc;
        for(int i=0;i<31;++i) r+=(abc>>i&1)+(aAb>>i&1);
        return r;
    }
};
```
At this point, it's obvious that we are simply counting the number of '1's in the two numbers in binary, which is known as the Hamming weight or population count. There is a built-in function in GCC compiler for that, "__builtin_popcount(int)", so we can write it like this:
```cpp
class Solution {
public:
    int minFlips(int a, int b, int c) {
        uint abc=(a|b)^c,aAb=a&b&abc;
        #ifdef __GNUG__
        return __builtin_popcount(abc)+__builtin_popcount(aAb);
        #else
        int r=0;
        for(int i=0;i<31;++i) r+=(abc>>i&1)+(aAb>>i&1);
        return r;
        #endif
    }
};
```
I did some research and noticed that llvm also supports the function "__builtin_popcount", and MSVC has these functions "__popcnt16, __popcnt, __popcnt64". We can use these functions according to the compiler.
(Update: starting from C++20, there's an "std::popcount()" function in the header <bit>.)

Then I found this webpage, which lists the best bit hacks currently known for many computation tasks. I'm fascinated by the esoteric beauty of the algorithms and magic numbers. Using the algorithm in the list, we can write the solution as
```cpp
#ifndef __GNUG__
int popC_32(uint v){
    v = v - ((v >> 1) & 0x55555555);
    v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
    return ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
}
#endif
class Solution {
public:
    int minFlips(int a, int b, int c) {
        ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
        uint abc=(a|b)^c,aAb=a&b&abc;
        #ifdef __GNUG__
        return __builtin_popcount(abc)+__builtin_popcount(aAb);
        #else
        return popC_32(abc)+popC_32(aAb);
        #endif
    }
};
```
But since many CPUs support the instruction "POPCNT", it's probably better to use the built-in functions if they are available.
---
E1351 description:
  Given a m x n matrix grid which is sorted in non-increasing order both row-wise and column-wise, return the number of negative numbers in grid.
Constraints:
  m == grid.length
  n == grid[i].length
  1 <= m, n <= 100
  -100 <= grid[i][j] <= 100
An O(m+n) algorithm is easy to find,
```cpp
class Solution {
public:
    int countNegatives(vector<vector<int>>& grid) {
        const int m=grid.size(),n=grid[0].size();
        int k=n-1,r=0;
        for(int i=0;i<m;++i){
            while(k>=0&&grid[i][k]<0) k--;
            if(k<0){r+=(m-i)*n;return r;}
            r+=n-1-k;
        }
        return r;
    }
};
```
We may try to use binary search to increase speed,
```cpp
class Solution {
public:
    int countNegatives(vector<vector<int>>& grid) {
        const int m=grid.size(),n=grid[0].size();
        int k=n,r=0;
        for(int i=0;i<m;++i){
            k=upper_bound(grid[i].begin(),grid[i].begin()+k,0,greater())-grid[i].begin();
            if(k==0){r+=(m-i)*n;return r;}
            r+=n-k;
        }
        return r;
    }
};
```
But now it takes O(m*log(n)), which may be slower than O(m+n).
Also, if $m>n$, it would be better to do binary search in the columns, in which case we must write our own upper bound function. Also, because we'll access elements in different vectors, the overhead may be larger.

Let's consider the complexity. For m*log(n) to be less than m+n, we need $n>m*(log(n)-1)$. Considering that $n<100$, it can be satisfied if $n>6m$. But also considering the overhead and the constant coeffecient, we may try to use a larger criterion, something like this:
```cpp
int upperB(vector<vector<int>>& grid,int i,int k){
    if(grid[0][i]<0) return 0;
    int l=0,r=k;
    while(r-l>1){
        k=(l+r)/2;
        if(grid[k][i]>=0) l=k;
        else r=k;
    }
    return r;
}
class Solution {
public:
    int countNegatives(vector<vector<int>>& grid) {
        const int m=grid.size(),n=grid[0].size();
        if(n>8*m){
            int k=n,r=0;
            for(int i=0;i<m;++i){
                k=upper_bound(grid[i].begin(),grid[i].begin()+k,0,greater())-grid

[i].begin();
                if(k==0){r+=(m-i)*n;return r;}
                r+=n-k;
            }
            return r;
        }
        else if(m>64*n){
            int k=m,r=0;
            for(int i=0;i<n;++i){
                k=upperB(grid,i,k);
                if(k==0){r+=(n-i)*m;return r;}
                r+=m-k;
            }
            return r;
        }
        else{
            int k=n-1,r=0;
            for(int i=0;i<m;++i){
                while(k>=0&&grid[i][k]<0) k--;
                if(k<0){r+=(m-i)*n;return r;}
                r+=n-1-k;
            }
            return r;
        }
    }
};
```
But considering that the size of the input is quite small, it won't show much difference in this case, unless there're a lot of test cases where the input has one dimension much larger than the other.

Then I thought, instead of doing binary search vertically or horizontally, what if we do it... diagonally?
The good thing about it is, we can be sure that we can get rid of at least half of the inputs after each step, because if we choose any point on the diagonal and cut the rectangle into four smaller ones, the total area of the top left and the bottom right must be at least half of the area of the original rectangle.

The following code implements this algorithm:
```cpp
int negCount(int tl_r,int tl_c,int br_r,int br_c,vector<vector<int>>& grid){
    //cout<<"tl is {"<<tl_r<<","<<tl_c<<"}, br is {"<<br_r<<","<<br_c<<"},";
    if(tl_r==br_r||tl_c==br_c) return 0;
    if(br_r-tl_r==1){
        if(grid[tl_r][tl_c]<0){
            //cout<<"\nadded col count="<<br_c-c1<<endl;
            return br_c-tl_c;
        }
        int c1=tl_c,c2=br_c,cm;
        while(c2-c1>1){
            cm=(c1+c2)/2;
            if(grid[tl_r][cm]>=0)c1=cm;
            else c2=cm;
        }
        //cout<<"\nadded col count="<<br_c-c2<<endl;
        return br_c-c2;
    }
    if(br_c-tl_c==1){
        if(grid[tl_r][tl_c]<0){
            //cout<<"\nadded row count="<<br_r-r1<<endl;
            return br_r-tl_r;
        }
        int r1=tl_r,r2=br_r,rm;
        while(r2-r1>1){
            rm=(r1+r2)/2;
            if(grid[rm][tl_c]>=0)r1=rm;
            else r2=rm;
        }
        //cout<<"\nadded row count="<<br_r-r2<<endl;
        return br_r-r2;
    }
    int r1=tl_r,r2=br_r,c1=tl_c,c2=br_c,rm,cm;
    if(grid[r1][c1]<0){r2=r1;c2=c1;}
    else{
        while(r2-r1>1&&c2-c1>1){
            rm=(r1+r2)/2;
            cm=(c1+c2)/2;
            if(grid[rm][cm]>=0){r1=rm;c1=cm;}
            else{r2=rm;c2=cm;}
        }
        while(r2-r1>1){
            rm=(r1+r2)/2;
            if(grid[rm][cm]>=0)r1=rm;
            else r2=rm;
        }
        while(c2-c1>1){
            cm=(c1+c2)/2;
            if(grid[rm][cm]>=0)c1=cm;
            else c2=cm;
        }
    }
    //cout<<"r1 is "<<r1<<",c1 is "<<c1<<"\n";
    //cout<<"r2 is "<<r2<<",c2 is "<<c2<<"\nadded count="<<(br_r-r2)*(br_c-c2)<<endl;
    return (br_r-r2)*(br_c-c2)+negCount(tl_r,c2,r2,br_c,grid)+negCount(r2,tl_c,br_r,c2,grid);
}
class Solution {
public:
    int countNegatives(vector<vector<int>>& grid) {
        const int m=grid.size(),n=grid[0].size();
        return negCount(0,0,m,n,grid);
    }
};
```
What's the complexity of this algorithm? Let's take a look. Without loss of generality, let's assume $m>n$. The first function call will take log(n) first, when c1 and c2 meet, then it will take log(m/n) to let r1 and r2 meet, so it will take O(log(m)). Next, the complication starts. The time it takes depends on the shapes of the two rectangles that result from the first call, so it's hard to tell. If the index of the point where the positive and negative meet on the diagonal is (m1,n1), then the complexity of the next function call is log(max(m-m1,n1))+log(max(m1,n-n1)). The maximum it can get is log(m)+log(n), which is when the point is at the lower right corner - but that also means, we only has two rows left and we just need to do two more 1D upper-bound search. Considering this, the worse case is probably when the point is at the center. Let's find out the complexity in this scenario.

Each time the rectangle will be cut into 4 equal smaller ones, two of them will be the input of the next call. The complexity is log(m)+2log(m/2)+4log(m/4)+8log(m/8)+..., but it doesn't go down to 2^log(m)*log(m/m), instead it stops at 2^log(n)*log(m/(2^n)) because at that point we only have 1D arrays to search.

Summing up the terms, I got the complexity O(f(m,n)) where $f(m,n)=2n\log(m/n)+2n-\log m -2$ and $m\geq n$. When $m>>n$, it's approximately O(nlog(m/n)), and when $m=n$, it becomes O(m+n). I think it's a pretty good algorithm, although it's hard to tell with such small input sizes.

A variation of this algorithm is, instead of finding the exact cell that separates the positive and negative values, we can stop at the separation line of the smaller dimension, so it takes O(log(min(m,n))) instead of max(m,n). Then the two rectangles that would be the input of the next call will have an overlap in the larger dimension of size around m/n. The complexity analysis of this one would be even more complicated. Anyway, I wonder how the performance would be on a larger scale.

Monday, June 5, 2023

Leetcode M1091 Shortest Path in Binary Matrix, M2101 Detonate the Maximum Bombs, E1232 Check If It Is a Straight Line

M1091 description:
  Given an n x n binary matrix grid, return the length of the shortest clear path in the matrix. If there is no clear path, return -1.
  A clear path in a binary matrix is a path from the top-left cell (i.e., (0, 0)) to the bottom-right cell (i.e., (n - 1, n - 1)) such that:
  All the visited cells of the path are 0.
  All the adjacent cells of the path are 8-directionally connected (i.e., they are different and they share an edge or a corner).
  The length of a clear path is the number of visited cells of this path.
Constraints:
  n == grid.length
  n == grid[i].length
  1 <= n <= 100
  grid[i][j] is 0 or 1
Apparently this is a bfs problem. The concept is simple, but there're a few places we can optimize. This code breaks both the speed and memory record at the same time:
```cpp
#pragma GCC optimize("Ofast","inline","-ffast-math")
#pragma GCC target("avx,mmx,sse2,sse3,sse4")
static const int SZMAX=10000;
int sz;
struct Co{
    int8_t x;
    int8_t y;
    Co operator+(const Co& other){
        return Co{int8_t(x+other.x),int8_t(y+other.y)};
    }
};
Co q[SZMAX];
Co* q2;
const Co dir[8]={{0,1},{0,-1},{1,0},{-1,0},{1,1},{1,-1},{-1,1},{-1,-1}};
class Solution {
    int checkCo(Co co,const vector<vector<int>>& grid){
        if(co.x<0||co.x>=sz||co.y<0||co.y>=sz) return 1;
        else return grid[co.x][co.y];
    }
    int q1l,q1r,q2l,q2r;
    int l1,l2;
    int processQ(vector<vector<int>>& grid,int which){
        if(which==1){
            int qsz=q1r-q1l;
            while(qsz--){
                auto& cur=q[q1l++];
                for(int i=0;i<8;++i){
                    auto nxt=cur+dir[i];
                    auto v=checkCo(nxt,grid);
                    if(v>1) return -l1+v-2;
                    if(v==0){q[q1r++]=nxt;grid[nxt.x][nxt.y]=l1;}
                }
            }
            l1--;
        }
        else{
            int qsz=q2r-q2l;
            while(qsz--){
                auto& cur=q2[q2r--];
                for(int i=0;i<8;++i){
                    auto nxt=cur+dir[i];
                    auto v=checkCo(nxt,grid);
                    if(v<0) return l2-2-v;
                    if(v==0){q2[q2l--]=nxt;grid[nxt.x][nxt.y]=l2;}
                }
            }
            l2++;
        }
        return 0;
    }
public:
    int shortestPathBinaryMatrix(vector<vector<int>>& grid) {
        static auto _=[](){ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0);return 0;}();
        sz=grid.size();
        if(grid[0][0]||grid[sz-1][sz-1]) return -1;
        if(sz==1) return 1;
        grid[0][0]=-1;grid[sz-1][sz-1]=2;
        l1=-2;l2=3;
        q1l=0;q1r=1;q2l=-1;q2r=0;
        q2=&q[0]+SZMAX-1;
        q[0]=Co{0,0};*q2=Co{int8_t(sz-1),int8_t(sz-1)};
        while(q1l<q1r&&q2l<q2r){
            int r=(q1r-q1l)<=(q2r-q2l)?processQ(grid,1):processQ(grid,2);
            if(r) return r;
        }
        return -1;
    }
};
```
It combines a few tricks to increase speed. One of them is the return value for out of range inputs to "checkCo()" matches the value of blocked cells, so it only takes one condition check instead of two, as I mentioned before. Another is, we can do bfs on either side, but if we start from both sides and proceed with the one that has less elements in the queue, it may save space and time. The extreme case would be a tree, starting from a leaf would be much more efficient than starting from the root. We can use two queues for that, but considering that the maximum total elements in the two queues is known, we can fit both of them in one array, where one queue moves in increasing address direction while the other moves in the opposite direction.
---
M2101 description:
  You are given a list of bombs. The range of a bomb is defined as the area where its effect can be felt. This area is in the shape of a circle with the center as the location of the bomb.
  The bombs are represented by a 0-indexed 2D integer array bombs where bombs[i] = [xi, yi, ri]. xi and yi denote the X-coordinate and Y-coordinate of the location of the ith bomb, whereas ri denotes the radius of its range.
  You may choose to detonate a single bomb. When a bomb is detonated, it will detonate all bombs that lie in its range. These bombs will further detonate the bombs that lie in their ranges.
  Given the list of bombs, return the maximum number of bombs that can be detonated if you are allowed to detonate only one bomb.
Constraints:
  1 <= bombs.length <= 100
  bombs[i].length == 3
  $1 <= xi, yi, ri <= 10^5$
So we are given a directed graph, and our task is to find the largest number of vertices that can be reached from a single vertex. At first I thought there must be an $O(|V|+|E|)$ algorithm, because I thought the difficulty is dealing with cycles, and there are algorithms that can find strongly connected components in $\Theta(|V|+|E|)$. After that we have a DAG, which should be easier to deal with, right? But the answer is no, which is pointed out in this paper which is posted in the discussion. A DAG doesn't make it any easier. The only possible direction one may take to lower the complexity is probably to utilize the property of circles on 2D Euclidean plane, which is not an arbitrary directed graph. But I haven't found any exploit of that. So we may just simply do a dfs on every vertex to find the maximum.
```cpp
#pragma GCC optimize("Ofast","inline","-ffast-math")
#pragma GCC target("avx,mmx,sse2,sse3,sse4")
class Solution {
    int8_t adj[100][100];
    int8_t by[100];
    int8_t dfs(int8_t i,int8_t boom){
        int8_t r=1;
        by[i]=boom;
        for(int j=0;j<adj[i][99];j++){
            if(by[adj[i][j]]!=boom) r+=dfs(adj[i][j],boom);
        }
        return r;
    }
public:
    int maximumDetonation(const vector<vector<int>>& bombs) {
        static auto _=[](){ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0);return 0;}();
        const int8_t sz=bombs.size();
        for(int8_t i=1;i<sz;++i){
            long long x1=bombs[i][0],y1=bombs[i][1],r1=bombs[i][2];
            r1*=r1;
            for(int8_t j=0;j<i;++j){
                long long dx=x1-bombs[j][0],dy=y1-bombs[j][1];
                long long dis=dx*dx+dy*dy;
                long long r2=bombs[j][2];
                if(dis<=r1) {auto& _=adj[i][99];adj[i][_++]=j;}
                if(dis<=r2*r2) {auto& _=adj[j][99];adj[j][_++]=i;}
            }
        }
        int8_t m=0;
        memset(by,-1,sz);
        for(int8_t i=0;i<sz;++i){
            if(by[i]==-1) m=max(m,dfs(i,i));
        }
        return m;
    }
};
```
Here I used a 2d array instead of a 2d vector to store the adjacent list, just to make it a little faster. The last element in each row stores the size. The "by[]" array stores which bomb the current one is detonated by.
---
E1232 description:
  You are given an array coordinates, coordinates[i] = [x, y], where [x, y] represents the coordinate of a point. Check if these points make a straight line in the XY plane.
Constraints:
  2 <= coordinates.length <= 1000
  coordinates[i].length == 2
  $-10^4 <= coordinates[i][0], coordinates[i][1] <= 10^4$
  coordinates contains no duplicate point.
The problem is indeed very easy, but I found quite a few bad codes on the submission page. Let's take a look:
Sample 0 ms submission
```cpp
class Solution {
public:
    bool checkStraightLine(vector<vector<int>>& coordinates) {
        float m,c;
        int v=0;
        for(int i =0;i<coordinates.size();i++){  
            int k=coordinates[0][0];
            int t=coordinates[i][0];
            if(k==t)
              v++;
        }
        if(v==coordinates.size())
        return true;
        m=float(coordinates[0][1]-coordinates[1][1])/float(coordinates[0][0]-coordinates[1][0]);      
        for(int i=1;i<coordinates.size()-1;i++){                
            c=float(coordinates[i][1]-coordinates[i+1][1])/float(coordinates[i][0]-coordinates[i+1][0]);
            cout<<c<<" ";
            if(m==c){                    
             continue;
            }
            else{                
             return false;
            }
        }    
        return true;
     
    }
};
```
First, multiplication is better than division, and addition or subtraction is better than multiplication in most scenarios, as long as it doesn't take too much more operations. So if it can be done with multiplications, we should do that, because 1. it avoids converting integers to floating point numbers, 2. it avoids dividing zero problems, 3. it's faster, even though we may need to convert the integer type to a larger one.
Second, using "==" to check equality is a terrible idea due to roundoff errors. It may not present in this example, since it passed all tests, probably because the computation is just a single division. But if there's one more step, e.g. multiplication or addition/subtraction, it may result in errors. Here's an example, try this line "cout<<((double)38/100+(double)65/200);", what do you get? 0.705. Now try this line "cout<<((double)38/100+(double)65/200==0.705);". What do you get? 0! It's false!
Just how bad is it? Let's test it with a simple program:
```cpp
  int cnt=0;
  for(int i=0;i<100;++i){
      for(int j=0;j<200;++j){
          if((double)i/100+(double)j/200!=double(10*i+5*j)/1000) cnt++;
      }
  }
  cout<<cnt;
```
I got 4239 on multiple platforms. So out of 20000 operations, 4239 of the answers are wrong, more than 20%.
So how do you check if two floating point numbers are equal? You may either 1. Express them as simplest form of fractions, but it's only applicable to rational numbers and could be complicated, or 2. Use a criterion $\epsilon$, if the difference between two floating point numbers is less than $\epsilon$, they are considered equal. But numeric errors can still cause false positive or false negative in certain cases. So this is another reason of the first rule of thumb: use integer multiplications instead of floating point divisions if possible.

The third one is a small thing, instead of checking each pair of the last two points, it would be faster to use the first point and the last point to find the slope, because the first point doesn't change, and it's one less addition operation because when we read the $i$th index of an array we add the address and $i$, it would be faster to just read a number that has a fixed address.

Putting all these together, this is the code that I submitted:
```cpp
class Solution {
public:
    bool checkStraightLine(const vector<vector<int>>& coordinates) {
        ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
        const int sz=coordinates.size();
        const long long x0=coordinates[0][0],y0=coordinates[0][1],dy01=coordinates[1][1]-y0,dx01=coordinates[1][0]-x0;
        for(int i=2;i<sz;++i){
            if((coordinates[i][1]-y0)*dx01!=dy01*(coordinates[i][0]-x0)) return false;
        }
        return true;
    }
};
```
It's probably the most reliable and optimized way to solve this problem. If the last constraint, "coordinates contains no duplicate point.", is loosened, the only extra step we need to take is to find a point that has different coordinate than the first one, then we start from there.

Sunday, May 28, 2023

Tuscaridium cygneum and the maximum 8 vertex polyhedron -Follow up

Previously...

So I wrote a short program to calculate the shapes that correspond to those problems that were mentioned in the previous post. Actually, the first one that I ran did not correspond to any of them. I was thinking, maybe the mechanism is on the surface, so instead of using a function of the distances between the points, maybe I should use a function that corresponds to the distance on the surface - or equivalently, the angle between them? Suppose that the capsules keep releasing a certain chemical on the surface that repels other capsules, so that they will find the spot that has the least concentration of the chemical, and the chemical degrades in time. The concentration from each capsule would be inversely proportional to the distance on the surface. If I assume the "force" is proportional to the concentration, the energy would be log of the angle.

I thought the solution would be similar to the cases with the Euclidean distance - but it's totally different. I was surprised by the result!
function=$-\sum\ln\delta_{ij}$, 8 vertices:
 0.0      , 0.0     , 1.0     ,
 0.646104 , 0.621228,-0.443424,
-0.614415 ,-0.575319, 0.539911,
-0.0590085,-0.946303,-0.317851,
 0.0283015, 0.968172, 0.248679,
 0.923682 ,-0.269269, 0.272589,
-0.951041 , 0.240249,-0.194424,
 0.0052322, 0.000739,-0.999986,
It looks like two perpendicular rectangles, nothing like the solutions to the other problems.
The difference actually starts with $n=4$. The solutions to all the Euclidean distance problems are a regular tetrahedron, which is not the solution to this problem - it's a square! Indeed, the product of the vertex-center-vertex angles that a regular tetrahedron makes is about 48.6478, and the one for a square is $\left(\frac{\pi}{4}\right)^4\pi^2\approx 60.0868$. This is probably not the right model for this problem, I suppose.
The result of Thomson problem - i.e., vertices are equal charges and the force is inverse-square to the distance, is
function=$\sum 1/r_{ij}$ (Thomson problem), 8 vertices:
 0.0     , 0.0     , 1.0     ,
 0.67198 ,-0.675175,-0.304273,
 0.90106 , 0.297473, 0.315596,
 0.341148, 0.451193,-0.824647,
-0.970893, 0.172403, 0.166266,
-0.292125,-0.901176, 0.320226,
-0.506278,-0.341556,-0.791847,
-0.145709, 0.982264, 0.118012,
Which is indeed a square antiprism.

I also tried the Whyte's problem (I didn't find any reference about this problem, except that the name was mentioned on wikipedia), where the force is inversly proportional to the distance. The result is
function=$\sum -\ln r_{ij}$ (Whyte's problem), 8 vertices:
 0.0     , 0.0     , 1.0     ,
-0.96761 , 0.171449, 0.185302,
-0.082313, 0.97464 , 0.208092,
 0.482057, 0.538236,-0.69132 ,
 0.50466 ,-0.715044,-0.483767,
 0.933948, 0.019279, 0.356889,
-0.530202,-0.085225,-0.843577,
-0.337356,-0.901919, 0.269692,
It looks very similar to the max volume shape, but doesn't seem to be exactly the same.
And when I compared it with the shape of Tuscaridium cygneum, it seemed closer to the shape than the maximum volume shape.
I wonder if this is the actual shape, and how the organism builds it!

Friday, May 26, 2023

Tuscaridium cygneum and the maximum 8 vertex polyhedron -A curious shape

I saw this thing on a youtube video, and I thought I recognized the shape. Here is my original comment:
Wait... This shape... I think I've seen this shape, the interesting symmetry. The voice in the video says that it has "7 arms", but that's not true, there're actually 8 arms. And it looks surprisingly symmetric. If we take each "arm" as a vertex, It has 8 vertices and all of its faces are triangles. At first I thought of a regular octahedron whose faces are all equilateral triangles, but it has 6 vertices. It's definitely not a cube either. I'm pretty sure I've seen this shape before in my random research on polyhedrons, so I googled "eight vertices polyhedron on a sphere maximum". And there it is: https://mathoverflow.net/questions/73899/optimal-8-vertex-isoperimetric-polyhedron https://math.stackexchange.com/questions/979660/largest-n-vertex-polyhedron-that-fits-into-a-unit-sphere I believe this is the same shape. I wonder how this naturally happens. Maybe this shape is also the solution to "if I put 8 equal charges on a sphere and they can move freely on the sphere, what shape do they make in equilibrium?". Maybe that's exactly how it's formed: 8 "bud"s were released to the surface, and they repelled each other... eventually they ended up in this shape. We should give this polyhedron a name. How about "Tuscaridron"? (What does "Tuscaridium" mean anyway?) This is such an astonishing finding, I'm deeply intrigued by the beauty of the nature...
And it got deleted immediately. Probably because it was long and had links. But still, youtube's spam detection is crap. It happened quite a few times before, too, so I had the habbit to copy my comment somewhere else before posting them. After all, if you want to talk about something serious, youtube comment is not the best place to post it anyway.
I tried to see if the shapes actually match. The data was from Spherical Codes, graphed on GeoGebra.
Light color means the vertex is in the front, dark color means it's in the back.
I also found more images on the internet (Credit to Steven Haddock and MBARI), and I tried to match those, too:
They seem to line up in some degree, but not perfectly.

I also noticed that there could be more than 8 capsules, as the following picture shows:
I'm not sure what that shape is. From what I've read, it seems that the number of capsules is often a multiple of 8, so there're probably 16 in this one. Also, I don't think the shape of the bottom one matches the 8-vertex maximum volume polyhedron, even though there're exactly 8 capsules. I wonder what the shape is - the 4 on the bottom seem to form a square.

On the other hand, I also thought about the other problem: If I put $n$ point charges on a sphere and they can move freely on the sphere, what shape do they make in equilibrium? While I was writing a program to find out, I took a look at the links again and noticed that in another question they mentioned the Thomson problem, which is exactly what I thought about. Apparently the solution for 8 vertices is a square antiprism - which doesn't seem to match any of the shapes above (except that there seem to be a square at the bottom in the last picture above). A closely related problem, Tammes problem, has exactly the same solution to 8 vertices. The proof can be found here.

I wonder if the shape is indeed the one that maximizes the volume, and since the shape is not a square antiprism, I wonder what mechanism drives it to become this shape.

Leetcode M1140 Stone Game II - Push it to the limit

M1140 description:
  Alice and Bob continue their games with piles of stones.  There are a number of piles arranged in a row, and each pile has a positive integer number of stones piles[i].  The objective of the game is to end with the most stones. 
  Alice and Bob take turns, with Alice starting first.  Initially, M = 1.
  On each player's turn, that player can take all the stones in the first X remaining piles, where 1 <= X <= 2M.  Then, we set M = max(M, X).
  The game continues until all the stones have been taken.
  Assuming Alice and Bob play optimally, return the maximum number of stones Alice can get.

Example 1:
  Input: piles = [2,7,9,4,4]
  Output: 10
  Explanation:  If Alice takes one pile at the beginning, Bob takes two piles, then Alice takes 2 piles again. Alice can get 2 + 4 + 4 = 10 piles in total. If Alice takes two piles at the beginning, then Bob can take all three piles left. In this case, Alice get 2 + 7 = 9 piles in total. So we return 10 since it's larger. 
Example 2:
  Input: piles = [1,2,3,4,5,100]
  Output: 104

Constraints:
  $1 <= piles.length <= 100$
  $1 <= piles[i] <= 10^4$
The problem is not trivial, so we most likely need to iterate through all possibilities. But there are a lot of sub-problems that we will encounter over and over again, so we should record the intermediate results. We can keep a table as the maximum points that a player can get at an index $i$ and given the current $M$. Considering the two approaches in DP, the top-down recursive approach is probably easier to write, since it's not easy to tell what the range of $M$ is if we use the bottom-up approach. But bottom-up is usually a little faster, so let's take the challenge. 
```cpp
#pragma GCC optimize("Ofast","inline","-ffast-math")
#pragma GCC target("avx,mmx,sse2,sse3,sse4")
class Solution {
    int tab[100][49];
    int v[100];
    void generate(){
        memset(v,0,sizeof(v));
        v[0]=1;
        for(int i=0;i<100;++i){
            int x=1;
            for(;x<=v[i];++x){
                if(i+x>=100) break;
                v[i+x]=max(v[i+x],v[i]);
            }
            for(;x<=2*v[i];++x){
                if(i+x>=100) break;
                v[i+x]=max(v[i+x],x);
            }
        }
    }
public:
    Solution(){
        generate();
        for(int i=0;i<100;++i){
            tab[i][48]=v[i];
        }
    }
    int stoneGameII(const vector<int>& piles) {
        static auto _=[](){ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0);return 0;}();
        const int sz=piles.size();
        int sum=0;
        for(int i=sz-1;i>=0;--i){
            sum+=piles[i];
            for(int j=0;j<tab[i][48];++j){
                int canTake=2*(j+1);
                if(canTake>=sz-i){
                    tab[i][j]=sum; continue;
                }
                int minE=INT_MAX;
                for(int x=1;x<=j+1;++x){
                    minE=min(minE,tab[i+x][j]);
                }
                for(int x=j+2;x<=canTake;++x){
                    if(tab[i+x][48]<x) continue;
                    minE=min(minE,tab[i+x][x-1]);
                }
                tab[i][j]=sum-minE;
            }
        }
        return tab[0][0];
    }
};
```
The code above has Runtime 3 ms Beats 100% and Memory 8.3 MB Beats 91.12%, and the "Sample 5 ms submission" now takes 16ms. It breaks the record and I think it is pretty much optimized.

You may wonder where does the magic number "49" come from. Well, I ran the "generate" function and got the ranges of $M$ at each index from 0 to 99, and the largest is 48. Since I use index $j$ to represent $M-1$, I use the index 48 to store the range of $M-1$. If we want to have a more flexible way to write it, we could write
```cpp
static const int maxSize=100;
static const int mRange=(maxSize+1)/2;
//...
int tab[maxSize][mRange];
```
because the range of $M$ satisfies $v[n]<=(n+2)/2$. The proof is left as an exercise.

The code above can be easily modified to work as a C function,
```c
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
int tab[100][49];
int v[100];
bool first=true;
void generate(){
    memset(v,0,sizeof(v));
    v[0]=1;
    for(int i=0;i<100;++i){
        int x=1;
        for(;x<=v[i];++x){
            if(i+x>=100) break;
            v[i+x]=max(v[i+x],v[i]);
        }
        for(;x<=2*v[i];++x){
            if(i+x>=100) break;
            v[i+x]=max(v[i+x],x);
        }
    }
}
int stoneGameII(int* piles,const int sz){
    if(first){
        generate();
        for(int i=0;i<100;++i){
            tab[i][48]=v[i];
        }
        first=false;
    }
    int sum=0;
    for(int i=sz-1;i>=0;--i){
        sum+=piles[i];
        for(int j=0;j<tab[i][48];++j){
            int canTake=2*(j+1);
            if(canTake>=sz-i){
                tab[i][j]=sum; continue;
            }
            int minE=INT_MAX;
            for(int x=1;x<=j+1;++x){
                minE=min(minE,tab[i+x][j]);
            }
            for(int x=j+2;x<=canTake;++x){
                if(tab[i+x][48]<x) continue;
                minE=min(minE,tab[i+x][x-1]);
            }
            tab[i][j]=sum-minE;
        }
    }
    return tab[0][0];
}
```
Runtime 0 ms Beats 100%, Memory 5.9 MB Beats 85.71%.

Tuesday, May 23, 2023

Leetcode M934 Shortest Bridge Optimization

M934 Shortest Bridge description:
  You are given an n x n binary matrix grid where 1 represents land and 0 represents water.
  An island is a 4-directionally connected group of 1's not connected to any other 1's. There are exactly two islands in grid.
  You may change 0's to 1's to connect the two islands to form one island.
  Return the smallest number of 0's you must flip to connect the two islands.
Constraints:
  $n == grid.length == grid[i].length$
  $2 <= n <= 100$
  $grid[i][j]$ is either 0 or 1.
  There are exactly two islands in grid.
The first step is, of course, finding a '1'. Then the typical approach is, to record the first island of '1's, then do a bfs from there and look for another '1'.

The concept is actually quite simple, but there're a few things that we can modify to increase efficiency.
```cpp
#pragma GCC optimize("Ofast","inline","-ffast-math")
#pragma GCC target("avx,mmx,sse2,sse3,sse4")
class Solution {
    int sz;
    void findB(const vector<vector<int>>& grid,int& x,int& y){
        for(x=0;x<sz;++x){
            for(y=0;y<sz;++y){
                if(grid[x][y]==1) return;
            }
        }
    }
    int checkCo(const vector<vector<int>>& grid,int x,int y){
        if(x<0||x>=sz||y<0||y>=sz) return 3;
        else return grid[x][y];
    }
    const array<array<int,2>,4> directions={ { {-1,0},{0,-1},{0,1},{1,0} } };
    void dfs(vector<vector<int>>& grid,int x,int y){
        int val=checkCo(grid,x,y);
        if(val>=2) return;
        grid[x][y]=2;
        if(val==0){q[qM++]=128*x+y;return;}
        for(auto &dir:directions) {
            dfs(grid,x+dir[0],y+dir[1]);
        };
    }
    int q[10000];
    int qM=0;
    int bfs(vector<vector<int>>& grid) {
        int l=0;
        int level=1;
        while(qM>l){
            int lay=qM-l;
            while(lay--){
                int p=q[l++];
                for(auto &dir:directions) {
                    int t=p+128*dir[0]+dir[1];
                    int x=t/128;
                    int y=t%128;
                    int val=checkCo(grid,x,y);
                    if(val>=2) continue;
                    if(val==1) {qM=0;return level;}
                    grid[x][y]=2;
                    q[qM++]=t;
                }
            }
            level++;
        }
        qM=0;
        return level;
    }
    // void printG(const vector<vector<int>>& grid){
    //     for(auto& v:grid){
    //         for(auto i:v) cout<<i<<", ";
    //         cout<<endl;
    //     }
    // }
public:
    int shortestBridge(vector<vector<int>>& grid) {
    static auto _=[](){ios_base::sync_with_stdio(false);cin.tie(0);cout.tie(0);return 0;}();
        sz=grid.size();
        int x,y;
        findB(grid,x,y);
        dfs(grid,x,y);
        return bfs(grid);
    }
};
```
To save space and time, we can use the original grid to record the visited cells by changing the value to 2. It's easy to modify the program to record the information on a hashset if we are not allowed to modify the original grid.
The first small trick is, instead of returning -1 from "checkCo()" when the coordinates are out of range, it returns 3. This way, a single check is enough to determine if we need to proceed, instead of two.

Another one is, we can add the first layer of '0's outside the island into the queue while we do the dfs. Since we change its value to 2 right away, there will be no duplicates. So there is no need to add the entire island to the bfs queue.

Also, since the grid is at most 100*100, I decided to use 128*x+y to store the two coordinates. It can't deal with out of range cases, so I must make sure that only use this form when the coordinates are valid. Not sure if it's faster than pair<int,int>, though.

The shortest time I got from the code above is 23ms which breaks the record. I think it's pretty much optimized.

The story doesn't end here. I was thinking about if there is a more efficient algorithm for the first part - to find the surface of the island. I thought, we just needed to find the surface of the island, not the entire island. And as long as the island has a "normal" shape - i.e, not hyperbolic (which was a big part of my research) or fractal, the number of elements on the perimeter should be much less than the area. So, if I just follow the boundary of the island, it would be much more efficient when the island is large.

The following code implements this algorithm:
```cpp
class Solution {
    void printG(const vector<vector<int>>& grid){
        for(auto& v:grid){
            for(auto i:v) cout<<i<<", ";
            cout<<endl;
        }
        cout<<"----------------------"<<endl;
    }
    int sz;
    struct Edge{
        int d;//0=up,1=right,2=down,3=left
        int x;//above(h) or to the left of(v), range [0,sz)
        int y;//range [0,sz)
        bool operator!=(const Edge& a){
            return this->d!=a.d||this->x!=a.x||this->y!=a.y;
        }
    };
    void findB(const vector<vector<int>>& grid,int& x,int& y){
        for(x=0;x<sz;++x) for(y=0;y<sz;++y) if(grid[x][y]==1) return;
    }
    int q[10000];
    int qM=0;
    int checkCo(const vector<vector<int>>& grid,int x,int y){
        if(x<0||x>=sz||y<0||y>=sz) return 3;
        else return grid[x][y];
    }
    void up(Edge& edg,vector<vector<int>>& grid){
        int &x=edg.x,&y=edg.y,&d=edg.d;
        if(x==0){++d; return;}
        if(grid[x-1][y]==0){++d;grid[x][y]=2;q[qM++]=(x-1)*128+y; return;}
        if(y==0){--x;return;}
        if(grid[x-1][y-1]==0){--x;grid[x][y]=2;q[qM++]=x*128+y-1;return;}
        d+=3;
        x--;
        y--;
        grid[x][y]=2;
    }
    void right(Edge& edg,vector<vector<int>>& grid){
        int &x=edg.x,&y=edg.y,&d=edg.d;
        if(y==sz-1){++d; return;}
        if(grid[x][y+1]==0){++d;grid[x][y]=2;q[qM++]=x*128+y+1; return;}
        if(x==0){++y;return;}
        if(grid[x-1][y+1]==0){++y;grid[x][y]=2;q[qM++]=(x-1)*128+y;return;}
        d--;
        x--;
        y++;
        grid[x][y]=2;
    }
    void down(Edge& edg,vector<vector<int>>& grid){
        int &x=edg.x,&y=edg.y,&d=edg.d;
        if(x==sz-1){++d; return;}
        if(grid[x+1][y]==0){++d;grid[x][y]=2;q[qM++]=(x+1)*128+y; return;}
        if(y==sz-1){++x;return;}
        if(grid[x+1][y+1]==0){++x;grid[x][y]=2;q[qM++]=x*128+y+1;return;}
        d--;
        x++;
        y++;
        grid[x][y]=2;
    }
    void left(Edge& edg,vector<vector<int>>& grid){
        int &x=edg.x,&y=edg.y,&d=edg.d;
        if(y==0){d=0; return;}
        if(grid[x][y-1]==0){d=0;grid[x][y]=2;q[qM++]=x*128+y-1; return;}
        if(x==sz-1){--y;return;}
        if(grid[x+1][y-1]==0){--y;grid[x][y]=2;q[qM++]=(x+1)*128+y;return;}
        d--;
        x++;
        y--;
        grid[x][y]=2;
    }
public:
    int shortestBridge(vector<vector<int>>& grid) {
        sz=grid.size();
        cout<<"before:\n";
        printG(grid);
        Edge edg;
        edg.d=0;
        findB(grid,edg.x,edg.y);//pointing up, '1' is on the right, so island will always be on the right
        q[qM++]=edg.x*128+edg.y;
        Edge lastE=edg;
        do{
            switch(edg.d){
                case 0:up(edg,grid); break;
                case 1:right(edg,grid); break;
                case 2:down(edg,grid); break;
                case 3:left(edg,grid); break;
            }
            //cout<<"d is "<<edg.d<<",x is "<<edg.x<<", y is "<<edg.y<<endl;
        }while(edg!=lastE);
        cout<<"after:\n";
        printG(grid);
        
        
        
        return 0;
    }
};
```
Unfortunately, it doesn't work in certain cases. I noticed that in the third test case, 
```
1, 1, 1, 1, 1, 
1, 0, 0, 0, 1, 
1, 0, 1, 0, 1, 
1, 0, 0, 0, 1, 
1, 1, 1, 1, 1, 
```
The edge we find is on the left of the first '1', and after we go around the entire island, we don't find anything to continue to work with. So, the algorithm doesn't work when the second island is inside the first island.

In this particular case, we may cross the island and look for a '0' and start from there. But it doesn't work in general. The island can be in any strange shape, and the algorithm doesn't work if it's not singly connected.
![image](https://yjian012.github.io/Yi-blog/img/2023-05-23-Leetcode_M934-Island.png)
It works nicely when the islands are singly connected, though. E.g,
```
before:
0, 1, 
1, 0, 
----------------------
after:
0, 2, 
1, 0, 
----------------------
before:
0, 1, 0, 
0, 0, 0, 
0, 0, 1, 
----------------------
after:
0, 2, 0, 
0, 0, 0, 
0, 0, 1, 
----------------------
before:
1, 1, 1, 1, 1, 
1, 0, 0, 0, 1, 
1, 0, 1, 0, 0, 
1, 0, 0, 0, 1, 
1, 1, 1, 1, 1, 
----------------------
after:
1, 2, 2, 2, 1, 
2, 0, 0, 0, 2, 
2, 0, 1, 0, 0, 
2, 0, 0, 0, 2, 
1, 2, 2, 2, 1, 
----------------------
```
It may be useful somewhere else.

Leetcode M347 Top K Frequent Elements Optimization

M347. Top K Frequent Elements description:
  Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order.

Constraints:
  $1 <= nums.length <= 10^5$
  $-10^4 <= nums[i] <= 10^4$
  $k$ is in the range [1, the number of unique elements in the array].
  It is guaranteed that the answer is unique.
(Update: Apparently using "nth_element" would be the most efficient algorithm, which is O(n), better than the algorithms below.)

A very easy problem for a medium. But I did a few modifications to the typical approach. The code below is the "fastest" 0ms submission, which takes 12~20ms currently.
```cpp
struct myComp {
    constexpr bool operator()(
        pair<int, int> const& a,
        pair<int, int> const& b)
        const noexcept
    {
        return a.second < b.second;
    }
};
class Solution {
public:   
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> mp;
        for(int i:nums) mp[i]++;
        
        priority_queue<pair<int,int>, vector<pair<int,int>>, myComp> pq;
        for(auto i:mp) pq.push(i);
        
        vector<int> ans;
        for(int i = 0; i < k; i++) {
            auto top = pq.top();
            ans.push_back(top.first);
            pq.pop();
        }
        return ans;
    }
};
```
First we count the frequencies, then we find the top $k$ ones with the highest frequency. The code above used a priority queue (pq) to record the order, but adding a number to a pq is $O(\log n)$, and adding $n$ numbers is $O(n\log n - n)$, which is not much faster than using a vector and sorting it. We can do much better with the following code:
```cpp
class Solution {
public:   
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int,int> mp;
        for(int i:nums) mp[i]--;
        
        priority_queue<array<int,2>> pq;
        int i=0;
        auto it=mp.begin();
        for(;i<k;++i,++it) pq.emplace(array<int,2>{it->second,it->first});
        for(;it!=mp.end();++it){
            if(it->second>pq.top()[0]) continue;
            pq.emplace(array<int,2>{it->second,it->first});
            pq.pop();
        }
        vector<int> ans;
        while(!pq.empty()){
            ans.emplace_back(pq.top()[1]);
            pq.pop();
        }
        reverse(ans.begin(),ans.end());
        return ans;
    }
};
```
The trick is, since we only need the top $k$ ones, we can just store those in the pq. And each time we find a value that is larger than the smallest one in the pq, we remove the smallest one and add the new one. So we actually need a pq that implememts a minimum heap. We can do that by writing a custom comparator, but that's unnecessary - if we negate the numbers, the result of comparison would be inverted. So in the first step, instead of adding the frequency count, we subtract it.

Then we add the first $k$ elements, then each time if we encounter an element that has a higher frequency then the minimum frequency that we have, we remove the minimum frequency one and add the new one. The operation is only $O(\log k)$. In the end, we extract the corresponding values, but they will be in the order of small to large, so we need to reverse the order, which is $O(k)$. It's a much better algorithm when $n>>k$.

Leetcode M399 Evaluate Division

M399. Evaluate Division description:
  You are given an array of variable pairs equations and an array of real numbers values, where $equations[i] = [A_i, B_i]$ and $values[i]$ represent the equation $A_i / B_i = values[i]$. Each $A_i$ or $B_i$ is a string that represents a single variable.
  You are also given some queries, where $queries[j] = [C_j, D_j]$ represents the jth query where you must find the answer for $C_j / D_j = ?$.
  Return the answers to all queries. If a single answer cannot be determined, return $-1.0$.

Constraints:
  $1 <= equations.length <= 20$
  $equations[i].length == 2$
  $1 <= A_i.length, B_i.length <= 5$
  $values.length == equations.length$
  $0.0 < values[i] <= 20.0$
  $1 <= queries.length <= 20$
  $queries[i].length == 2$
  $1 <= C_j.length, D_j.length <= 5$
  $A_i, B_i, C_j, D_j$ consist of lower case English letters and digits.
There are quite a few graph related problems recently, this is an interesting one. The first intuition would be finding the shortest path, or just any path, and then multiplying the numbers. But come to think about it, it would be a waste of resources if we find the path for each query. Instead, we can store the previous results for future use.
Actually, there's an even easier approach: After we build the graph, we can simply assign a value for each variable based on the equations. Here is an example:
```cpp
class Solution {
    int parent[40];
    int size[40];
    int find_set(int v) {
        if (v == parent[v]) return v;
        return parent[v] = find_set(parent[v]);
    }
    void union_sets(int a, int b) {
        a = find_set(a);
        b = find_set(b);
        if (a != b) {
            if (size[a] < size[b])
                swap(a, b);
            parent[b] = a;
            size[a] += size[b];
        }
    }
    unordered_map<string,int> mp;
    double value[40];
    int q[40];
    double tab[40][40];
    int adj[40][40];
    int adjLen[40];
    void init(const vector<vector<string>>& equations,const vector<double>& values){
        memset(size,1,sizeof(size));
        //memset(tab,0.0,sizeof(tab));
        memset(adj,-1,sizeof(adj));
        memset(adjLen,0,sizeof(adjLen));
        memset(value,0.0,sizeof(value));
        mp.clear();
        int cnt=0;
        for(int i=0;i<equations.size();++i){
            auto it0=mp.find(equations[i][0]),it1=mp.find(equations[i][1]);
            int n0,n1;
            if(it0==mp.end()){
                if(it1==mp.end()){
                    it1=mp.insert({equations[i][1],cnt++}).first;
                    n1=it1->second;
                    parent[n1]=n1;
                }
                it0=mp.insert({equations[i][0],cnt++}).first;
                n0=it0->second;
                parent[n0]=n1;
            }
            else if(it1==mp.end()){
                n0=it0->second;
                it1=mp.insert({equations[i][1],cnt++}).first;
                n1=it1->second;
                parent[n1]=parent[n0];
            }
            else{
                n0=it0->second;
                n1=it1->second;
                union_sets(n0,n1);
            }
            adj[n0][adjLen[n0]++]=n1;
            adj[n1][adjLen[n1]++]=n0;
            tab[n0][n1]=values[i];
            tab[n1][n0]=1/values[i];
        }
        for(int i=0;i<cnt;++i){
            if(parent[i]!=i) continue;
            value[i]=1;
            q[0]=i;
            int l=0,r=1;
            while(l<r){
                int curr=q[l++];
                for(int j=0;j<adjLen[curr];++j){
                    int nei=adj[curr][j];
                    double& v=value[nei];
                    if(v==0.0){
                        v=value[curr]/tab[curr][nei];
                        q[r++]=nei;
                    }
                }
            }
        }
        // for(auto it=mp.begin();it!=mp.end();++it){
        //     cout<<it->first<<"("<<parent[it->second]<<")"<<"=";
        //     cout<<value[it->second]<<",";
        // }
        // cout<<endl;
    }
public:
    vector<double> calcEquation(const vector<vector<string>>& equations, const vector<double>& values, const vector<vector<string>>& queries) {
        init(equations,values);
        const int qsz=queries.size();
        vector<double> res(qsz);
        for(int i=0;i<qsz;++i){
            if(mp.find(queries[i][0])==mp.end()||mp.find(queries[i][1])==mp.end()){res[i]=-1;continue;}
            auto v0=mp[queries[i][0]],v1=mp[queries[i][1]];
            if(v0==v1){res[i]=1;continue;}
            if(find_set(v0)!=find_set(v1)){res[i]=-1;continue;}
            res[i]=value[v0]/value[v1];
        }
        return res;
    }
};
```
First, we initialize the adjacency list and the table of quotients. We read the equations and use disjoint set union algorithm to find the connected components. After that, we find the root of each component and assign $1$ to its value, then update the values of all the other variables in the component based on the table with bfs.
Then we process the queries, which we simply check if the variables are both known and if they are in the same component, if so we divide their values to get the result.

It's hard to tell how the performance is because the test cases are quite small, all the submissions are 0ms. But I think this algorithm is a good balance between code complexity and efficiency. There are a few places that we can improve, though. First, if there are few queries, we don't need to assign a value to all the variables. Second, imagine a different problem: The input format is {"string1","string2","number"}, where "number==0.0" means it's a query at the current input, otherwise it means it gives the value of "string1" divided by "string2". The algorithm above would be very inefficient in this case. Can we modify the algorithm, so that it updates the values when a new input is given?

It turns out that we can modify the disjoint sets union algorithm to achieve this goal:
```cpp
class Solution {
    pair<int,double> parent_value[40];
    int size[40];
    pair<int,double> find_set(int v) {
        auto& pv=parent_value[v];
        if (v == pv.first) return pv;
        auto rv=find_set(pv.first);
        return {pv.first=rv.first,pv.second*=rv.second};
    }
    void union_sets(int a, int b,double a_b) {
        auto pa = find_set(a);
        auto pb = find_set(b);
        if (pa.first != pb.first) {
            if (size[pa.first] < size[pb.first]){
                swap(pa, pb);
                a_b=1/a_b;
            }
            parent_value[pb.first]={pa.first,pa.second/pb.second/a_b};
            size[pa.first] += size[pb.first];
        }
    }
    unordered_map<string,int> mp;
    void init(const vector<vector<string>>& equations,const vector<double>& values){
        memset(size,1,sizeof(size));
        memset(parent_value,0.0,sizeof(parent_value));
        mp.clear();
        int cnt=0;
        for(int i=0;i<equations.size();++i){
            auto it0=mp.find(equations[i][0]),it1=mp.find(equations[i][1]);
            int n0,n1;
            if(it0==mp.end()){
                if(it1==mp.end()){
                    it1=mp.insert({equations[i][1],cnt++}).first;
                    n1=it1->second;
                    parent_value[n1]={n1,1};
                }
                it0=mp.insert({equations[i][0],cnt++}).first;
                n0=it0->second;
                parent_value[n0]={n1,parent_value[n1].second*values[i]};
            }
            else if(it1==mp.end()){
                n0=it0->second;
                it1=mp.insert({equations[i][1],cnt++}).first;
                n1=it1->second;
                parent_value[n1]={parent_value[n0].first,parent_value[n0].second/values[i]};
            }
            else{
                n0=it0->second;
                n1=it1->second;
                union_sets(n0,n1,values[i]);
            }
        }
    }
public:
    vector<double> calcEquation(const vector<vector<string>>& equations, const vector<double>& values, const vector<vector<string>>& queries) {
        init(equations,values);
        const int qsz=queries.size();
        vector<double> res(qsz);
        for(int i=0;i<qsz;++i){
            if(mp.find(queries[i][0])==mp.end()||mp.find(queries[i][1])==mp.end()){res[i]=-1;continue;}
            auto v0=mp[queries[i][0]],v1=mp[queries[i][1]];
            if(v0==v1){res[i]=1;continue;}
            if(find_set(v0).first!=find_set(v1).first){res[i]=-1;continue;}
            res[i]=parent_value[v0].second/parent_value[v1].second;
        }
        return res;
    }
};
```
This way, we can update the value upon each query.

Sunday, May 14, 2023

Leetcode M2466 Optimization

M2466 description:
Given the integers zero, one, low, and high, we can construct a string by starting with an empty string, and then 
at each step perform either of the following:
    Append the character '0' zero times.
    Append the character '1' one times.
This can be performed any number of times.
A good string is a string constructed by the above process having a length between low and high (inclusive).
Return the number of different good strings that can be constructed satisfying these properties. Since the answer 
can be large, return it modulo $10^9$ + 7.
Constraints:
    1 <= low <= high <= $10^5$
    1 <= zero, one <= low
An optimization to the typical solution is, noticing that if gcd(zero,one) is not one, we can skip a lot of unnecessary computations.
(And different combinations of ("zero","one")s may conform to the same entry in the table.)
We can save what we've calculated for future inputs.

# Code
```
class Solution {
    const int mo=1000000007;
    unordered_map<string,vector<int>> lookUp;
    unsigned int gcd(unsigned int a,unsigned int b){//make sure a,b>0
        while(a>0){
            b=b%a;
            swap(a,b);
        }
        return b;
    }
public:
    int countGoodStrings(int low, int high, int zero, int one) {
        int s=min(zero,one),l=max(zero,one);
        int g=gcd(s,l);
        s/=g;l/=g;low=(low-1)/g+1;high=high/g;
        string st=to_string(s)+','+to_string(l);
        auto it=lookUp.find(st);
        if(it==lookUp.end()){
            vector<int> table(high+1,0);
            for(int i=s;i<=l;i+=s){
                table[i]=1;
            }
            table[l]++;
            for(int i=l+1;i<=high;++i){
                table[i]=(table[i-s]+table[i-l])%mo;
            }
            int r=0;
            for(int i=low;i<=high;++i){
                r+=table[i];
                r%=mo;
            }
            table[0]=high;
            lookUp[st]=move(table);
            return r;
        }
        else{
            auto& table=it->second;
            if(high>table[0]){
                table.resize(high+1);
                for(int i=table[0]+1;i<=high;++i){
                    table[i]=(table[i-s]+table[i-l])%mo;
                }
                table[0]=high;
            }
            int r=0;
            for(int i=low;i<=high;++i){
                r+=table[i];
                r%=mo;
            }
            return r;
        }
    }
};
```
In this case we may change it to
```
unordered_map<long long,vector<int>> lookUp;
long long st=(long long)s*100000+l;
```
which may be faster than converting to a string? I'm just too lazy to write a hash for pair<int,int>...

IBM Ponder this 2025 06 solution

Problem description can be found here . You can play the games below with a standard keyboard. Choose main game or bonus game. The cur...