CS302 Lecture Notes - STL Review with some Topcoder Problems

In class, we went over three topcoder problems. In these lecture notes, I'll reiterate some points made in class.

SRM 347, D2, 250-pointer: CarBuyer

Read over the problem. It's on their web server at: http://community.topcoder.com/stat?c=problem_statement&pm=7652&rd=10671. I have a program that includes CarBuyer.cpp and lets you test the examples in CB-Main.cpp. Compile and test by copying your implementation to CarBuyer.cpp and then calling g++ -std=c++98 on CB-Main.cpp:
UNIX> cp CarBuyer-ss.cpp CarBuyer.cpp
UNIX> vi CB-Main.cpp
UNIX> g++ -std=c++98 CB-Main.cpp
UNIX> a.out 0
10500.000000
UNIX> a.out 1
45200.000000
UNIX> a.out 2
254122.444444
UNIX> 
The reason I did this problem was to review stringstreams. Solving this problem is pretty straightforward. First, convert all of those ints to doubles, since you'll be using floating point. Next, for each string in cars, use a stringstream to extract the price, tax and efficiency. Then calculate the cost and store the minimum.

Some smaller points to remember:

Here's the code (CarBuyer.cpp)

#include <string>
#include <vector>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

class CarBuyer {
  public:
    double lowestCost(vector <string> cars, int fuelPrice, int annualDistance, int years);
};

double CarBuyer::lowestCost(vector <string> cars, int fuelPrice, int annualDistance, int years)
{
  int i;
  istringstream ss;

  double fp, ad, y, price, tax, efficiency, cost, min;
 
  fp = fuelPrice;
  ad = annualDistance; 
  y = years;
  min = -1;

  for (i = 0; i < cars.size(); i++) {
    ss.clear();
    ss.str(cars[i]);
    ss >> price >> tax >> efficiency;
    cost = price + (y * tax) + (y * ad * fp / efficiency);
    if (min == -1 || cost < min) min = cost;
  }
  return min;
}

In class, I showed you how to use sscanf() in place of the stringstream. Here's that code. Since sscanf() does not use reference variables, you have to pass pointers that get filled in. I personally prefer sscanf() to stringstreams, but that's mostly because I'm old and have been using sscanf() for 30+ years. The return value of sscanf() tells you how many matches were made correctly. In this program, that would be three, since Topcoder guarantees that your input is correct. (This code is in CarBuyer-sscanf.cpp):

double CarBuyer::lowestCost(vector <string> cars, int fuelPrice, int annualDistance, int years)
{
  int i;

  double fp, ad, y, price, tax, efficiency, cost, min;
 
  fp = fuelPrice;
  ad = annualDistance; 
  y = years;
  min = -1;

  for (i = 0; i < cars.size(); i++) {
    sscanf(cars[i].c_str(), "%lf %lf %lf", &price, &tax, &efficiency);
    cost = price + (y * tax) + (y * ad * fp / efficiency);
    if (min == -1 || cost < min) min = cost;
  }
  return min;
}

Bottom Lines From This Program

This was just about input processing: Practice with stringstreams and sscanf().

SRM 551, D2, 250-Pointer: ColorfulBricks

Go ahead and read the description from Topcoder or http://community.topcoder.com/stat?c=problem_statement&pm=12136&rd=15173. What you should see relatively quickly that this problem amounts to counting the number of distinct letters in bricks. If that number is two, return two. If that number is one, return one. If that number is anything else, return zero.

Now, determining the number of distinct letters is an easy matter, and there are a lot of ways to write ColorfulBricks. In my opinion, the easiest is to sort bricks, and then count the number of times adjacent letters don't equal each other. That's in Colorful-Sort.cpp. Remember, you need to include algorithm to use the STL's sort() procedure.

int ColorfulBricks::countLayouts(string bricks)
{
  int i, nc;

  sort(bricks.begin(), bricks.end());

  nc =  1;
  for (i = 1; i < bricks.size(); i++) if (bricks[i] != bricks[i-1]) nc++;
  if (nc == 1) return 1;
  if (nc == 2) return 2;
  return 0;
}

To test this, copy it to ColorfulBricks.cpp and compile Colorful-Main.cpp:

UNIX> cp Colorful-Sort.cpp ColorfulBricks.cpp
UNIX> g++ -std=c++98 Colorful-Main.cpp 
UNIX> a.out 0
2
UNIX> a.out 1
1
UNIX> a.out 2
0
UNIX> 
Alternatively, you can use a set to store the characters in bricks, and the size of the set will be the number of distinct characters. This is in: Colorful-Set.cpp:

int ColorfulBricks::countLayouts(string bricks)
{
  int i;
  set <char> s;

  for (i = 0; i < bricks.size(); i++) s.insert(bricks[i]);
  if (s.size() == 1) return 1;
  if (s.size() == 2) return 2;
  return 0;
}

Remember, sets don't store duplicate values and maps don't store duplicate keys -- use multisets and multimaps if you want that functionality. Here, we don't want duplicates.

You can use a map, too, instead of a set, which allows you to leverage the syntax where you can treat the map like an associative array. That's in Colorful-Map.cpp

int ColorfulBricks::countLayouts(string bricks)
{
  int i;
  map <char, int> s;

  /* This treats the map like an associate array, inserting
     bricks[i] into the map if it is not there already. */

  for (i = 0; i < bricks.size(); i++) s[bricks[i]]++;

  if (s.size() == 1) return 1;
  if (s.size() == 2) return 2;
  return 0;
}

You can instead leverage the fact that there are only 26 potential values of bricks[i], to use a vector. This is in Colorful-Vec1.cpp:

int ColorfulBricks::countLayouts(string bricks)
{
  int i, nc;
  vector <int> s;

  s.resize(26, 0);
  nc = 0;
  for (i = 0; i < bricks.size(); i++) s[bricks[i]-'A'] = 1;
  for (i = 0; i < 26; i++) if (s[i] != 0) nc++;
  if (nc == 1) return 1;
  if (nc == 2) return 2;
  return 0;
}

You can tweak the above to use the characters themselves as indices into s (Colorful-Vec2.cpp). The maximum character that you'll see is 'Z', so that's why we resize s to be ('Z'+1).

int ColorfulBricks::countLayouts(string bricks)
{
  int i, nc;
  vector <int> s;

  s.resize('Z'+1, 0);
  nc = 0;
  for (i = 0; i < bricks.size(); i++) s[bricks[i]] = 1;
  for (i = 'A'; i <= 'Z'; i++) if (s[i] != 0) nc++;
  if (nc == 1) return 1;
  if (nc == 2) return 2;
  return 0;
}

I'm not sure which is easier to read -- this program of the last one. They are both about the same to me.

Finally, you can simply use one variable to keep track of the first letter and another to keep track of the second. This is in Colorful-L1L2.cpp:

int ColorfulBricks::countLayouts(string bricks)
{
  int i, l1, l2, n;
  vector <int> s;

  l1 = bricks[0];
  n = 1;
  
  for (i = 1; i < bricks.size(); i++) {
    if (n == 1 && bricks[i] != l1) {
      n = 2;
      l2 = bricks[i];
    } else if (n == 2 && bricks[i] != l1 && bricks[i] != l2) return 0;
  }

  if (n == 1) return 1;
  if (n == 2) return 2;
  return 0;
}

They all work, so which is the best? I'll hedge a bit. First, let's think about running time complexity. Suppose that there are n elements in bricks. Then the first three implementations are O(n log n), while the remaining three are O(n). That would argue for one of the last three. In terms of raw speed, the last one will be the fastest. So is that the best?

Well, I'm hedging because you need to consider multiple factors when you ask about the "best." Speed is one thing. Memory is another (the last one wins there too). Readability is a third (I think the set solution is the most readable). And of course, in Topcoder, speed of programming and disinclination for bugs is also important. Since the constraints ensure that all implementations will be super-fast (n is less than or equal to 50), these last considerations may be the most important, in which case the one that used sorting is the best. That's what I used when I solved this one for fun (it took 3 minutes for 246.70 points).

Regardless of which one is "best," you should be able to tell me the running time of all of these.

Bottom Lines From This Program

There are many ways to solve a problem. The ``best'' way depends on what is important for the problem at hand, such as:

SRM 353, D1, 250-pointer: Glossary

Go ahead and read the description, either on Topcoder or http://community.topcoder.com/stat?c=problem_statement&pm=7838&rd=10710. I know from experience that y'all hate these problems, because there's so much detail in the formatting. However specifications are specifications. What I'm going to do is build this one up and show you how I'd attack it. There's a temptation to do everything at once -- build a big data structure and then traverse it and print. That really doesn't work well here. Instead, it's better to go small, and build up, testing as you go.

The first thing to notice is that you have to sort the output, ignoring case. So, the first thing that I'm going to do is read all the terms, convert them to uppercase, and then store them in a map. The key will be the uppercase term, and the val is the original term. Then, I'll print it out to make sure I've got it right. This is in Glossary-1.cpp:

vector <string> Glossary::buildGlossary(vector <string> I)
{
  int i, j;
  string s;
  map <string, string> g;
  map <string, string>::iterator git;
  SVec rv;

  /* Create the map g, which has the strings converted 
     to upper-case as keys, and the strings themselves as vals. */

  for (i = 0; i < I.size(); i++) {
    s = I[i];
    for (j = 0; j < s.size(); j++) {
      if (s[j] >= 'a' && s[j] <= 'z') s[j] += ('A'-'a');
    }
    g[s] = I[i];
  }

  for (git = g.begin(); git != g.end(); git++) {
    cout << git->first << " " << git->second << endl;
  }
  return rv;
}

Remember, the line "g[s] = I[i]" is equivalent to "g.insert(make_pair(s, I[i]))." I return an empty vector so that everything compiles nicely. When we run it, it works as anticipated:

UNIX> cp Glossary-1.cpp Glossary.cpp
UNIX> g++ -std=c++98 Glossary-Main.cpp
UNIX> a.out 0
CANADA Canada
FRANCE France
GERMANY Germany
ITALY Italy
JAPAN Japan
RUSSIA Russia
UNITED KINGDOM United Kingdom
UNITED STATES United States
UNIX> a.out 1
ALPHA alpha
BETA beta
DELTA delta
GAMMA gamma
OMEGA omega
UNIX> 
Next, I'm going to add a second map. It has single characters as its keys, and vectors of strings as vals. These are the glossary entries. Since we create this by traversing g, we can be guaranteed that the vectors are in the proper order. We print it out in a format similar to what we need, and test again. It's in Glossary-2.cpp:

vector <string> Glossary::buildGlossary(vector <string> I)
{
  int i, j;
  string s;
  map <string, string> g;
  map <string, string>::iterator git;
  map <char, SVec> bychar;
  map <char, SVec>::iterator bit;
  char f;
  SVec rv;

  /* Create the map g, which has the strings converted 
     to upper-case as keys, and the strings themselves as vals. */
  
  for (i = 0; i < I.size(); i++) {
    s = I[i];
    for (j = 0; j < s.size(); j++) {
      if (s[j] >= 'a' && s[j] <= 'z') s[j] += ('A'-'a');
    }
    g[s] = I[i];
  }

  /* Now, create the map "bychar", which has the starting
     letters as keys, and vectors of the original strings as vals. */

  for (git = g.begin(); git != g.end(); git++) {
    f = git->first[0];
    bychar[f].push_back(git->second);
  }

  /* Print out "bychar." */

  for (bit = bychar.begin(); bit != bychar.end(); bit++) {
    printf("%c\n", bit->first);
    printf("-----\n");
    for (i = 0; i < bit->second.size(); i++) {
      printf("  %s\n", bit->second[i].c_str());
    }
  }
    
  return rv;
}

Now, instead of printing out those entries, let's create strings from them that are in the proper format, and then put them into one of two vectors -- a vector for A-M, and a vector for N-Z. This is in Glossary-3.cpp:

vector <string> Glossary::buildGlossary(vector <string> I)
{
  int i, j;
  string s;
  map <string, string> g;
  map <string, string>::iterator git;
  map <char, SVec> bychar;
  map <char, SVec>::iterator bit;
  char f;
  SVec atm, ntz, rv, *v;
  char cs[100];

  /* Create the map g, which has the strings converted 
     to upper-case as keys, and the strings themselves as vals. */

  for (i = 0; i < I.size(); i++) {
    s = I[i];
    for (j = 0; j < s.size(); j++) {
      if (s[j] >= 'a' && s[j] <= 'z') s[j] += ('A'-'a');
    }
    g[s] = I[i];
  }

  /* Now, create the map "bychar", which has the starting
     letters as keys, and vectors of the original strings as vals. */

  for (git = g.begin(); git != g.end(); git++) {
    f = git->first[0];
    bychar[f].push_back(git->second);
  }

  /* Create the formatted strings for each column.  Put the
     strings onto a vector for each column. */

  for (bit = bychar.begin(); bit != bychar.end(); bit++) {
    v = (bit->first < 'N') ? &atm : &ntz;
    s.clear();
    s.resize(19, ' ');
    s[0] = bit->first;
    v->push_back(s);
    s.clear();
    s.resize(19, '-');
    v->push_back(s);

    for (i = 0; i < bit->second.size(); i++) {
      sprintf(cs, "  %-17s", bit->second[i].c_str());
      s = cs;
      v->push_back(s);
    }
  }

  /* Print the vectors to error check. */

  for (i = 0; i < atm.size(); i++) cout << atm[i] << endl;
  cout << endl;
  for (i = 0; i < ntz.size(); i++) cout << ntz[i] << endl;
    
  return rv;
}

A few things -- first, note how I use a pointer to an SVec to make sure that I'm putting the strings onto the correct list. Second, I'm using sprintf() to create the strings for the terms. I do that because it's easier than using an ostringstream. The only sublety is that I need to make sure the memory is allocated for cs. It could be only 20 characters (19 for the string and one for the null character, but I'm using 100 just to be safe. The string " %-17s" says to create the string with two spaces, and then pad the argument to 17 characters, left justified. That makes the resulting string 19 characters. Saying "s = cs" creates a C++ string from cs.

When we compile and run it, all looks good. We can pipe the output to cat -A to make sure that all of the strings are 19 characters:

UNIX> cp Glossary-3.cpp Glossary.cpp
UNIX> g++ -std=c++98 Glossary-Main.cpp 
UNIX> a.out 0 | cat -A
C                  $
-------------------$
  Canada           $
F                  $
-------------------$
  France           $
G                  $
-------------------$
  Germany          $
I                  $
-------------------$
  Italy            $
J                  $
-------------------$
  Japan            $
$
R                  $
-------------------$
  Russia           $
U                  $
-------------------$
  United Kingdom   $
  United States    $
UNIX> 
Finally, let's create rv. The first thing we do is make sure that both atm and ntz are the same size, by adding 19-character strings to the smaller one. Then we simply traverse them and create the strings in rv using string concatenation. The final program is in Glossary-4.cpp. I only include the loop that creates rv:

vector <string> Glossary::buildGlossary(vector <string> I)
{
  .......

  /* Make atm and ntz the same size. */

  s.clear();
  s.resize(19, ' ');
  while (atm.size() < ntz.size()) atm.push_back(s);
  while (ntz.size() < atm.size()) ntz.push_back(s);

  s = "  ";
  for (i = 0; i < atm.size(); i++) {
    rv.push_back(atm[i] + s + ntz[i]);
  }
  return rv;
}

UNIX> cp Glossary-4.cpp Glossary.cpp
UNIX> g++ -std=c++98 Glossary-Main.cpp
UNIX> a.out 0 | cat -A
C                    R                  $
-------------------  -------------------$
  Canada               Russia           $
F                    U                  $
-------------------  -------------------$
  France               United Kingdom   $
G                      United States    $
-------------------                     $
  Germany                               $
I                                       $
-------------------                     $
  Italy                                 $
J                                       $
-------------------                     $
  Japan                                 $
UNIX> a.out 1 | cat -A
A                    O                  $
-------------------  -------------------$
  alpha                omega            $
B                                       $
-------------------                     $
  beta                                  $
D                                       $
-------------------                     $
  delta                                 $
G                                       $
-------------------                     $
  gamma                                 $
UNIX> a.out 2 | cat -A
A                                       $
-------------------                     $
  array                                 $
  AVL tree                              $
B                                       $
-------------------                     $
  backtracking                          $
  balanced tree                         $
  binary search                         $
UNIX> a.out 3 | cat -A
                     X                  $
                     -------------------$
                       XXXXXXXXXXXXXXXXX$
                     Y                  $
                     -------------------$
                       YYYYYYYYYYYYYYYYY$
                     Z                  $
                     -------------------$
                       ZZZZZZZZZZZZZZZZZ$
UNIX> a.out 4 | cat -A
A                    O                  $
-------------------  -------------------$
  Asteria              Oceanus          $
  Astraeus             Ophion           $
  Atlas              P                  $
C                    -------------------$
-------------------    Phoebe           $
  Clymene              Prometheus       $
  Coeus              R                  $
  Crius              -------------------$
  Cronus               Rhea             $
D                    T                  $
-------------------  -------------------$
  Dione                Tethys           $
E                      Theia            $
-------------------    Themis           $
  Epimetheus                            $
H                                       $
-------------------                     $
  Helios                                $
  Hyperion                              $
I                                       $
-------------------                     $
  Iapetus                               $
L                                       $
-------------------                     $
  Leto                                  $
M                                       $
-------------------                     $
  Mnemosyne                             $
UNIX> 
Time to submit!!

Bottom Lines From This Program


Review of STL Running Times

vector deque list set map
Accessing with an index
v[i]
O(1) O(1) Not supported Not supported Not supported
Appending
push_back()
O(1) O(1) O(1) Not supported Not supported
Prepending
push_front()
v.insert(v.begin(),...)
O(n) O(1) O(1) Not supported Not supported
General Insertion O(n) O(n) O(1) O(log n) O(log n)
Deleting from the back
pop_back()
v.erase(v.rbegin())
O(1) O(1) O(1) O(log n) O(log n)
Deleting from the front
pop_front()
v.erase(v.begin())
O(n) O(1) O(1) O(log n) O(log n)
General Deletion O(n) O(n) O(1) O(log n) O(log n)
Finding an element O(n) O(n) O(n) O(log n) O(log n)
Traversing O(n) O(n) O(n) O(n) O(n)
Clearing
v.clear()
O(1) O(1) O(n) O(n log n) O(n log n)
Creating from n elements Using a loop with
push_back():
O(n)
Using a loop with
push_back():
O(n)
Using a loop with
push_back():
O(n)
Using a loop with
insert():
O(n log n)
Using a loop with
insert():
O(n log n)