StringsAndTabs topcoder SRM412 div2 level3

Introduction

If you have not tried to solve this problem by yourself, please do so. Otherwise, this post will make little sense to you.

This problem has a very interesting background: translate a tablature (tab) from one string instrument to another, i.e. guitar to ukulele or balalaika and vise versa. Knowing the tab system definitely helps, but even if one has no background on tab, the explanation is very clear. 🎸

The solution is straightforward, but to write a bug-free code in a short period is challenging.

A Greedy Approach

For each time step, get all notes from the input tab. Raise the pitch by , which can be negative. Then, look for strings on the output instrument to play all notes for this time step. There are some rules to deal with degeneracy. If a note can be played on more than one strings, play it on the one with highest base pitch; and if two strings have the same base pitch, play it on the one with highest index (bottom-most). So, it might be a good idea to sort the output instrument strings int such order, and we can do binary search to look for such string. At the end, we can sort them again according to their indices. Therefore, it is a good idea to pack the base pitch, index and note sequence into a class, called Str in my code.

Another difficulty is to manage an infeasible cord correctly.

  • a note is too high or too low for the output instrument;
  • a note cannot be played anymore because all possible strings have been occupied.

I did this by a bool flag, and a vector to track which strings have been played. If all notes can be played, attach a '-' to all unvisited strings. Otherwise, pop the last notes from the visited strings and attach 'x' to all strings.

The default comparator of lower_bound() is less and it assumes the vector is in ascending order. It will return the first element which does not compare less than the query value. However, in our case, the strings are in descending order with respect to the base pitch. We can still use lower_bound()by passing a (customized) greater comparator. It will return the first element which does not compare greater than the query value. Here is a little demo.

int main( int argc, char * argv[] ){
  vector<int> v= { 1, 2, 3, 3, 3, 5, 5, 7};
  auto itr = lower_bound(v.begin(),v.end(),3);
  cout << itr-v.begin() << '\n'; // return 2, the index of the first '3'
  sort(v.begin(),v.end(), std::greater<int>()); // {7, 5, 5, 3, 3, ...}
  for( auto x : v ) cout << x << ' '; cout << '\n';
  itr = lower_bound(v.begin(),v.end(),4, greater<int>()); 
  cout << itr-v.begin() << '\n'; // return 3, the first '3'. as '3'<='4'
  itr = lower_bound(v.begin(),v.end(),3, greater<int>());
  cout << itr-v.begin() << '\n'; // return 3, the first '3'. as '3'<='4'
  return 0;
}

Time Complexity

If there are time steps, strings of input instrument, and strings of output instrument, the running time of worst case is .

class StringsAndTabs {
  private:
    struct Str {
      int idx;
      int pitch;
      string notes;
    };
    vector<Str> v;

    void assign_all( const char c ){
      for( auto & x : v ){
        x.notes += c;
      }
    }

    void translate( vector<int> & notes ){
      if( notes.size() == 0 ){
        assign_all( '-' );
        return;
      }

      vector<int8_t> is_visited( v.size(), false );
      bool can_assign = true;
      for( auto x : notes ){
        Str tmp; tmp.pitch = x;
        auto itr = lower_bound( all(v), tmp, [](
            const Str & lhs, const Str & rhs){
            return lhs.pitch > rhs.pitch;
            });
        // too high or too low
        if ( (itr == v.begin() && itr->pitch + 35 < x) || // too high
            ( itr == v.end() && (itr-1)->pitch > x )  ){ // too low 
          can_assign = false;
        }else{
          if( itr == v.end() || itr->pitch > x )  -- itr;
          while( is_visited[ itr - v.begin() ] ){ // find next string to accomodate
            ++ itr;
            if( itr == v.end() ){
              can_assign = false;
              break; // cannot assign anymore
            }
          }

          if( can_assign ){
            int y = x - itr->pitch;
            if( y <= 9 ){
              itr->notes += to_string( y );
              is_visited[itr-v.begin()] = true;
            }else if( y <= 35 ){
              itr->notes += 'A'+(y-10);
              is_visited[itr-v.begin()] = true;
            }else{ // y is too large
              can_assign = false;
            }
          }
        }
        if( !can_assign ) break;
      }

      if( !can_assign ){
        for( int i = 0 ; i < is_visited.size(); ++i ) {
          if( is_visited[i] ){
            v[i].notes.pop_back();
          }
        }
        assign_all( 'x' );
      }else{
        for( int i = 0 ; i < is_visited.size(); ++i ) {
          if( ! is_visited[i] ){
            v[i].notes.push_back( '-' );
          }
        }
      }

    }
  public: 

    vector<string> transformTab( 
        vector<string> & tab, 
        vector<int> stringsA, 
        vector<int> stringsB, 
        int d ){
      
      // initialize strings of output instrument
      int n = stringsB.size();
      this->v.resize(n);
      for( int i = 0; i < stringsB.size(); ++i ){
        v[i].idx = i;
        v[i].pitch = stringsB[i];
        v[i].notes = "";
      }
      // sort all strings based on pitch and then index
      sort( v.begin(), v.end(), [](const Str & lhs,
            const Str & rhs ){
          if( lhs.pitch > rhs.pitch ) return true;
          else if ( lhs.pitch == rhs.pitch && lhs.idx > rhs.idx )
          return true;
          else return false;
          } );

      const int M = tab[0].size();
      const int K = tab.size();
      for( int i = 0; i < M; ++i ){ // for each time step
        vector<int> notes;
        notes.reserve(K);
        for( int j = 0; j < K; ++j ){
          char c = tab[j][i];
          if( c != '-' ){
            if( isdigit(c) ){
              notes.push_back( c - '0' + d + stringsA[j] );
            }else{ // c is A-Z
              notes.push_back( c - 'A' + 10 + d + stringsA[j] );
            }
          }
        }
        sort( all(notes), std::greater<int>());
        translate( notes );
      }

      // sort by index, to output
      sort( all( v ), []( const Str & lhs,
            const Str & rhs ){
          return lhs.idx < rhs.idx ;
          } );

      vector<string> rst( v.size() );

      for( int i = 0; i < rst.size(); ++i ){
        rst[i] = move( v[i].notes );
      }
      return rst;
    }

};
/* the main() for testing.
int main( int argc, char * argv[] ){
  std::ios::sync_with_stdio(false);
  int n,m,d; cin >> n >> m >> d;
  vector<string> tab(n);
  vector<int> stringsA(n);
  vector<int> stringsB(m);
  input( tab );
  input( stringsA );
  input( stringsB );
  StringsAndTabs s;
  auto rst = s.transformTab( tab, stringsA, stringsB, d );
  for( auto x : rst ) cout << x << '\n'; 
  return 0;
}
*/
Written on April 1, 2017