Sunday, December 04, 2022

My mnemonics for the guitar / bass fretboard


Chord positions

For F-shape:

F G A B are frets 1, 3, 5, and 7, roughly speaking. These are the dotted frets.

C and D are frets 8 and 10, roughly speaking. These are the ones between the dotted ones.

E is fret 0 and fret 12.


For A-shape:

Bb C D E are frets 1, 3, 5, 7. These are the dotted frets.

F and G are frets 8 and 10.

A is fret 0 and fret 12.


For C-shape:

C D E are frets 1, 3, 5.

F, G, A are fets 6, 8, 10.

B is fret 0 and fret 12.


The C-shape is ahead of the A-shape by one full tone in position.


Identifying notes


For 1st and 6th string, we know that it is always the root note of the F shaped barre chord on that fret.

For 5th string, it is the root note of the A shaped barre chord on that fret. It is also the root note of the C-shaped chord that includes it.

For 4th string, it is the root note of the F-shaped barre chord that is barred two frets earlier.

For the 3rd string, it is the root note of the A-shared barre chord that is barred two frets earlier.

For the 2nd string, it is the note that is one full tone above the root note of the A-shaped barre chord on that fret.


Identifying the mediant, dominant, etc.

Now finding 3rd, 5th, and 7th notes.

- F-shaped and A-shaped chords are really the same structure, but the A-shaped ones are off by one string towards the higher pitch.

- The 6th string is not normally played in A-shaped barre chords.

- The 6th string, 1st string, and 4th string for F-shaped barre chords has the root note.

- For F-shaped chords, the 5th stirng (5 semi-tones behind the 4th string) has the 5th note.

- For F-shaped chords, 3rd string (4 semi-tones ahead of the 4th string) has the 3rd note.

- For A-shaped chords, the 4th string (5 semi-tones behind the 3rd string) has the 5th note.

- For A-shaped chords, the 2nd string (4 semi-tones ahead of the 3rd string) has the 3rd note.

Since we can find these notes based on the positions easily, we can immediately know the notes in the chord.



Chord relationships

This means that the root note of the F-shaped chord is the 5th note of the A-shaped chord barred at the same fret.

It also means that The 3rd note of the F-shaped chord is a flat of (one semi-tone below) the root note of the A-shaped chord barred at the same fret.



Read more!

Friday, July 29, 2022

Algorithmic problem solving: The importance of boundary values, edge case, and a consistent model

 I made the title sound almost like that of a computer science paper, although confessions here are of a more mundane nature. Using a small set of problems solving which I sucked at, I want to capture gaps in my own problem solving approach, and figure out patterns.

The right boundary conditions: Thief on a circular road

This is one of those famous problems. There is a circular road or whatever lined with houses. Each house has some cash. A thief is out thieving, but he cannot break into two adjacent houses on the same night as then some automated system notifies the police or dials 111 or some such (why does it need two adjacent houses to be broken into to raise an alarm is the question that perturbed me at a far deeper level all through the course of trying to solve the problem, but ...). The actual problem isn't all that hard to solve. But we have some interesting boundary conditions. If there are no houses, the maximum loot is 0. When there is exactly one house, the maximum loot is the money in that house. When there are two, the maximum loot is the higher of the sums in the two houses. The general algorithm starts being applicable starting with three or more houses. Stated this way it sounds simple, but writing it cleanly is important. It is also worth spending a tiny bit of time trying to keep these special cases as general as possible, but more often that not it doesn't work.

Defining structure: Finding zero-sum triplets in an array of integers

Given an array of integers, find unique triplets that sum to zero. Obviously this won't happen in an array that does have a mix of integers of opposite signs (or one having all zeros). The harder part is to get unique triplets out without any sort of post-processing.

First shot

Without the unique constraint, here was my flawed first shot. We sort the array, and loop through it one element at a time. For each element, we look for two other elements that sum to its negative - and we shall have our triplet. Obviously, we have reduced the problem of finding a triplet to a slightly easier problem of finding a pair of elements matching a constraint. We start by summing two elements one from each end. If their sum is less than the target, we pick the next higher element from the left. If the sum exceeds the target, we pick the next lower element from the right. Else we have our triplet. This is good but inadequate - it does not eliminate duplicates.

Dedup, initial thoughts

One possible way we would be to start with a sorted array of unique elements. But this means we modify the input or make a copy. However, even that alone won't be enough. A little thinking leads us to the observation that if I found a triplet from elements at positions i, j, and k in the sorted array, then I would hit this triplet thrice - first time when I picked a[i] and looked for a pair summing to -a[i], second time when I picked a[j], etc. and third time when I picked a[k]. So if we pick a[0], and look for a pair with a matching sum of opposite sign, then we should exclude a[0] from further searches because we would have found any pairs with a matching sum of opposite sign already. This means, starting with a[i], we should always search for j and k such that j > i and of course k < size.

Dedup, the comprehensive way

The above would suffice in a sorted array of unique elements. But if we want to avoid that chore and keep the input read-only or avoid copying, we can still do it. Simply put, if an element is repeated then we should skip all copies of it after the first. If a[1] equal a[0], we should just skip the logic for a[1]. If a[j+1] equals a[j], we again skip the logic for a[j+1] and so on.

None of these ideas are difficult, but they need a bit of thinking about the structure of the array - for most people perhaps.

Establishing the above structures is important - like shrinking the search space in each successive iteration, and skipping over duplicates.


Read more!

Saturday, April 16, 2022

A smattering of bitwisdom

I remember my epiphany about bit-twiddling. It was one small insight - the fact that 1 in binary is the equivalent of both 1 and 9 in decimal, but more often than not (to speak very facetiously) the latter. So 1111 is like 9999 in the sense that adding 1 to either will result in 10000, in the respective bases. Of course, binary 1 is equivalent to decimal 1 as well, because you add 1 to 9999 (or 1111) and get 10000. Also, subtracting 1 from 10000 has the predictably opposite effect of producing 1111 or 9999 as the base may be (pun intended).

Turning off bits and counting bits

This has several interesting consequences that are often used in bit-wise computations. For one, if the lowest n bits of x are 0, then x-1 would turn them into 1, and the n+1th bit, i.e. the lowest significant bit, to 0. What this in turn would mean is that x & (x-1) would simply turn off the lowest significant bit. We could use this successively on a number to turn off each of its significant bits, and count how many it takes till the number is reduced to zero - basically giving us a quick way to count set bits in a number.

Setting bits and bit ranges

Again, x+1 also does some stuff. Just as x-1 unsets the least significant bit, x+1 sets the lowest zero bit (and unsets all lower bits). So if the lowest n bits of x are set, and n+1th bit is 0, then x+1 unsets all these n bits, and sets the n+1th bit. This means repeated application of x |= (x+1) would set all its bits. But when does one stop? At some point all the bits of x up to its most significant bit are set. At this point, if we perform x & (x+1), we would get 0. So that should be the condition to check at each iteration.

The point of this post ... a leetcode medium problem

So far, I have repeated what is common knowledge. What's the point of this post then? I came across this leetcode problem - how to compute the bitwise-and of a continuous range of integers - could be the entire range of 32 bit positive signed integers.

I liked this problem because it didn't immediately fit a pattern, and I had to think about it. Now, it's useful to remember here that the nth power of two has a binary representation of 1 followed by n 0s. That means if we bitwise-and a range of numbers, the smallest of which is less than 2^n and the largest of which is greater than 2^n, then 2^n will occur in that range and nullify all the numbers resulting in 0 (think why). So only a range that lies entirely between two successive powers of 2 could yield a non-zero bitwise-and. At this point it's relatively straightforward to figure out the rest. Here is the logic:

If the range is [a, b], find the maximum power of 2 in the a and in b. If they aren't equal, the result would be 0, so return. Else, assuming that power is 2^n, set the n+1th bit in the result. Then repeat the process for a-2^n and b-2^n.


Read more!

Saturday, April 02, 2022

Merry ways of Binary Search

Binary search is an extremely important algorithmic technique in the armory of all programmers. It isn't typically thought of as an advanced algorithm, yet is deceptively simple, and features a few nuances to be aware of. In its simplest form, binary search tries to find a matching element from a sorted array of elements. I specifically said array, and not list, because the elements have to be contiguously stored and randomly accessible by index for binary search's O(log N) time-bound to hold.

To perform binary search, we usually define a condition that would be satisfied by some prefix of the array (could be of length zero) and not satisfied by the remaining array. This condition divides the array into two parts, and informs our algorithm which part could be discarded in the next step, thus reducing the search domain and zeroing in on a matching element if one exists. Below is a simple binary search implementation that works.

int binary_search(const vector<int>& nums, int key) {
  int low = 0, high = num.size() - 1;
  while (low < high) {
    int mid = low + (high - low)/2;

    if (nums[mid] < key) {
      // discard the left part
      low = mid + 1;
    } else if (nums[mid] > key) {
      // discard the right part
      high = mid - 1;
    } else {
      return mid; // we found a key
    }
  }
  return -1;  // we didn't find the key
}


The above implementation is about as simple as things can get. In most applications, we end up doing more with binary search. Let's modify the above problem slightly, to that of finding the location where the key would be inserted if it isn't already present - arguably a more useful variant. We really have to change what we return when we don't find a matching value:

  return low;  // or high
}

Quick summary

What's the key insight into binary search?

The first is that it's binary, meaning we divide the sorted array into two mutually exclusive sections. One section includes the target element if present, the other section decidedly doesn't.

The second is that we compute the mid-point of the array and check if it sits in the section that might include the target, or the one that doesn't. If it doesn't include the target, we update one end of the search space to exclude the mid-point, i.e one end becomes right = mid-1 or left = mid+1 as the case maybe. If on the other hand, the mid point is in the section that may include the target, then we update one end of the search space to point to the mid-point - we don't exclude it. So it becomes left = mid or right = mid as the case may be. This process continues until left and right converge inside the inclusive section, right at its boundary.

Finally, an important implementation note which can help. We can compute the mid-point in one of two ways:

   mid = left + (right - left)/2;

   mid = right - (right - left)/2;

When left and right are right next to each other, mid equals left in the first case, and right in the second case. Which form should be used depends on which update rule was used before that. If left was set equal to mid earlier, then the first form would repeatedly result in mid remaining at the same value and causing an infinite loop. If right was set equal to mid earlier, the same problem would appear with the second form.

So if we set mid = left, then use the second form, and if we set mid = right, then use the first form. It doesn't make a difference for update rules that add or subtract 1 on mid.

Finding ranges

Now, imagine that there can be repeated elements, and we'd like to find the first position where a repeated element occurs, as well as the last position. We can always run the regular binary search to find some occurrence of the element, and then do linear search backward or forward to find the first or the last matching element. But, this makes an O(log N) algorithm into a worst-case linear algorithm. There must be a better way.

One way to think about it is to observe that the first matching element in the array occurs at the border of a prefix for which nums[i] < key, and a second region for which nums[i] >= key. We are looking for the first element of the second region. This means that our search domain should always include this boundary.

We quickly modify our code above.

int find_first_of(const vector<int>& nums, int key) {
  int low = 0, high = nums.size() - 1;
  while (low < high) {
    int mid = low + (high - low)/2;

    if (nums[mid] < key) {
      // discard the left part
      low = mid + 1;
    } else { // nums[mid] >= key
      // discard the right part
      high = mid;
    }
  }
  return low;  // we didn't find the key
}

Notice the slight modifications. We no longer check specifically for equality. Instead we try to ensure that the remaining search domain after each step includes the boundary and zeroes in on the first element after the boundary. This is all good. What if we wanted to return the last element instead? Can't be too different, can it? Well in this case, the search domain must include the boundary between elements less than or equal to the target, and elements greater than the target.

int find_last_of(const vector<int>& nums, int key) {
  int low = 0, high = nums.size() - 1;
  while (low < high) {
    int mid = low + (high - low)/2;

    if (nums[mid] <= key) {
      // discard the left part
      low = mid;
    } else { // nums[mid] > key
      // discard the right part
      high = mid-1;
    }
  }
  return low;  // we didn't find the key
}

The update rules changed slightly, but guess what - the above code doesn't work. It goes into an infinite loop! The reason is that mid is computed as low+(high-low)/2, but when low and high are next to each other, i.e. the range is only two elements, then (high-low)/2 is 0, and so mid equals low. This means in successive iterations, the value of low doesn't change when ideally it should converge to high. Why hasn't this hit us so far? Notice that in the earlier examples, we never assigned mid to low, like we did here. What's the solution? Well, there is no mid for a two-element range. So use mid=high-(high-low)/2. This will mean that mid would become equal to high eventually. Since we assign mid to low, eventually low and high would converge. Conversely, if we used this way of computing mid in find_first_of, that one would go into an infinite loop.

Searching in a rotated array

An array is said to be rotated by k positions, if the original elements of the array have been shifted by k index positions. And spillover elements wrap around from zero, hence called rotation. If an array is sorted but rotated, and we don't know by how much, can binary search be of any help? For that matter, can binary search determine the amount of rotation? Turns out that it can, without much need to resort to anything linear.

If a sorted array is rotated, then the largest and smallest elements would be neighbours somewhere in the array. How do we find this location? In an array with a large number of duplicates, this might be tricky to do. But in an array with unique elements this is simple. In the rotated array, the beginning would be larger than the end, and the max element would lie somewhere between the beginning and the end, either in the first half, or in the second half. We call this the pivot point, and following is the code to find it.

int find_pivot(vector<int> nums) {
  int low = 0, high = nums.size()-1;

  while (low < high) {
    int mid = high - (high-low)/2;

    if (nums[low] < nums[mid]) {
      low = mid;
    } else {
      high = mid-1;
    }
  }
  return low;
}

Notice how we use the appropriate form for calculating mid, based on the update rules - to prevent infinite loops. This would terminate and return the position of the largest element.

Searching an element in the rotated array is also similar, and requires just a few changes, as shown below.

int find_elem_in_rotated(vector<int> nums, int key) {
  int low = 0, high = nums.size()-1;

  while (low < high) {
    int mid = high - (high-low)/2;

    if (nums[low] < nums[mid]) {
      if (nums[low] <= key && key < nums[mid]) {
        high = mid - 1;
      } else {
        low = mid;
      }

    } else {
      if (nums[mid] <= key && key <= nums[high]) {
        low = mid;
      } else {
        high = mid - 1;
      }
    }
  }
  return low;
}

This would return the location of the key if present, or the position where it can be inserted, if not present.



Read more!

Monday, March 14, 2022

vim as an IDE for C++

I found a relatively quick way to setup vim / neovim as an IDE for C++. Here is how it works.

  1. vim loads a plugin called Coc (I pronounce it coque which is a family-friendly approach).
  2. Coc runs a nodejs process to perform runtime checks.
  3. This process starts a configured language server (such as ccls or clangd) and then communicates with it.

 Here is what we need to set it up.

  1. Install ccls.
  2. Configure ccls by using a .ccls file at the root of the project, or by generating a compile_commands.json file as with the set(CMAKE_EXPORT_COMPILE_COMMANDS on) option.
  3. Install a relatively new version of nodejs (I installed 16.14.0 LTS).
    1. curl -sL https://deb.nodesource.com/setup_16.x -o nodesource_setup.sh
    2. sudo bash nodesource_setup.sh
    3. sudo apt install nodejs
  4. Install the Plug plugin management system in vim.
    1. curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
  5. Edit .vimrc to add the neoclide/coc plugin, the long list of key bindings from coc for convenience, and then reopen vim, running the command :PlugInstall.
  6. Configure coc using :CocConfig and editing the JSON that shows up. Refer here.
  7. Open a C++ source file in your project in vim / neovim.

Read more!