11: Santa and `Map`s

Announcements

Programming assignment 05 due tomorrow.

Quiz makeups require an explanation and documentation. You must get in touch with me, and I must by email to you and the TAs OK a makeup. You can take one without the OK, but it won’t be entered into the gradebook until it’s OKed.

Withdraw deadline is next Wednesday. I’ll send out notes to those of you who might want to consider it sometime tomorrow.

Today’s agenda

In class today, we’re going to walk through solving another toy problem that’s made relatively straightforward with the Set abstraction. Then we’ll talk about an extension to this problem that’s harder to solve, and introduce a new abstraction – the Map – to solve it.

The problem is called “Santa’s little helper” and is adapted from http://adventofcode.com/2015/day/3.

Santa’s little helper

Santa is delivering presents to an infinite two-dimensional grid of houses.

He begins by delivering a present to the house at his starting location, and then an elf at the North Pole calls him via radio and tells him where to move next. Moves are always exactly one house to the north (^), south (v), east (>), or west (<). After each move, he delivers another present to the house at his new location.

However, the elf back at the north pole has had a little too much eggnog, and so his directions are a little off, and Santa ends up visiting some houses more than once. How many houses receive at least one present?

For example:

  • > delivers presents to 2 houses: one at the starting location, and one to the east.
  • ^>v< delivers presents to 4 houses in a square, including twice to the house at his starting/ending location.
  • ^v^v^v^v^v delivers a bunch of presents to some very lucky children at only 2 houses.

Toward a solution

We’re going to try to solve this in Java. Let’s fire up Eclipse and create a new project. Now let’s write a DeliverySimulator class with a single method that takes a input of directions and returns the set of locations visited.

import java.util.Set;

public class DeliverySimulator {


    public static Set<Location> locationsVisited(String directions) {
        return null;
    }
}

Note we can use Eclipse to “stub out” our code, and to create empty implementations wherever possible here.

clicker question

Let’s also add a method to compute the actual number of houses visited:

public static int housesVisited(Set<Location> visited) {
    return visited.size();
}

Hey, half done! Well, sorta.

What should a location look like? Let’s give it an x and y coordinate:

public class Location {
    public final int x;
    public final int y;

    public Location(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Since we know we’re going to be storing locations in a set, we should make sure we implement meaningful equals and hashCode methods. Eclipse to the rescue again (Source -> Generate hashCode() and equals()…):

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + x;
    result = prime * result + y;
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Location other = (Location) obj;
    if (x != other.x)
        return false;
    if (y != other.y)
        return false;
    return true;
}

and maybe:

public String toString() {
    return "(" + x + ", " + y + ")";
}

Does this work? We could add a main method to do some testing:

public static void main(String[] args) {
    Location x = new Location(0,0);
    System.out.println(x);

    System.out.println(x.equals(new Location(0, 0)));
    System.out.println(x.equals(new Location(0, 1)));
}

But sometimes it’s nice to have automated tests. Let’s add a couple tests, again letting Eclipse help us as we go:

import static org.junit.Assert.*;

import org.junit.Test;

public class LocationTest {
    @Test
    public void testLocationEquals() {
        Location x = new Location(0,0);
        Location y = x;
        Location z = new Location(0,0);
        assertTrue(x == y);
        assertFalse(x == z);

        assertEquals(x, y);
        assertEquals(x, z);
    }

    @Test
    public void testLocationNotEquals() {
        assertFalse(new Location(0,0).equals(new Location(0,1)));
    }
}

Let’s go back to locationsVisited. According to the problem statement, the house at (0, 0) always gets a present:

public static Set<Location> locationsVisited(String directions) {
    Set<Location> visited = new HashSet<Location>();

    visited.add(new Location(0,0));

    return visited;
}

Does this work? Let’s add a test.

@Test
public void testEmptyDirections() {
    assertEquals(new HashSet<Location>(Arrays.asList(new Location(0,0))), DeliverySimulator.locationsVisited(""));
}

Then we need to think about where the sleigh goes, and deliver a present at each stop:

public static Set<Location> locationsVisited(String directions) {
    Set<Location> visited = new HashSet<Location>();

    visited.add(new Location(0,0));
    int x = 0;
    int y = 0;

    for (int i = 0; i < directions.length(); i++) {
        switch (directions.charAt(i)) {
        case '>':
            x += 1;
            break;
        case '<':
            x -= 1;
            break;
        case '^':
            y += 1;
            break;
        case 'v':
            y -= 1;
            break;
        default:
            throw new IllegalArgumentException();
        }
        visited.add(new Location(x, y));
    }

    return visited;
}

And now we add some tests:

@Test
public void testSimple() {
    assertEquals(new HashSet<Location>(Arrays.asList(new Location(0, 0), new Location(1, 0))),
            DeliverySimulator.locationsVisited(">"));
}

@Test
public void testFour() {
    assertEquals(
            new HashSet<Location>(
                    Arrays.asList(new Location(0, 0), new Location(0, 1), new Location(1, 1), new Location(1, 0))),
            DeliverySimulator.locationsVisited("^>v<"));
}

You could also add checks for housesVisited, either to their own test cases (which would be more true to the spirit of unit tests) or within the above.

A new problem, and a new data structure

What if, each time Santa visited a house, he delivered a present, and we wanted to know the total number of presents delivered? How might you go about tracking this?

You could, for example, keep a List of visited locations (instead of a Set). Then you could count the number of times each location appeared. But how would you tabulate it? You want to build some kind of table, a way to connect a set of things, and for each thing, some information about it.

(0,0) | (0,1) | (1,1) | (1,0)
------|-------|-------|------
  2      1        1       1

You might create two Lists, one of Location and one of Integer, where the ith element in each corresponded. But that’s clunky, hard to program, and has the poor performance of a list for lookups. You might edit the Location to contain a counter variable. That could work; but it might cause problems in other contexts, when the thing you’re storing is doesn’t have a clear “has-a” relationship with whatever you’re adding it to. Objects, like functions, should generally do a small set of things well.

What if I told you there was a data structure that solved this problem? What if I told you you’ve already (sorta) seen it? What if I told you it could be yours for no money down, and no monthly installments?

Introducing… the Map.

Map ADT

A Map “maps” keys to values. In other words, it associates one kind of thing with another kind of thing.

The first “thing” is the key. Keys are lookup keys; you can think of them as kinda analogous to an index card, or a page number, or a URL; they are the thing we can do lookups on (though they can also be useful data in and of themselves).

The second “thing” is a value – each value is associated with a key.

So Maps model, in essence, a table (on board). For our Santa problem, we might use Locations as keys and Integers as values.

The keys in a map form a set – there can only be one of each. And each value is associated with exactly one key. Keys are unique (as they are a set), but the same value can be associated with more than one key (as shown in our table above).

And, the key values should be immutable, or at least, should not change with respect to equals, for exactly the same reason as in Sets – in fact, if you recall, Sets are implemented using Maps.

Like Sets, Maps come in two flavors you’ll likely use: HashMaps and TreeMaps, with the same constraints (about efficient hashCode and compareTo methods).

The type signature is slightly different though: Map<K, V>; the key and value types are both parameterized. This should make sense: there’s no particular reason a map would store keys and values of the same type, only that all keys are of the same type, and all values are of the same type.

There are many useful Map methods, but we’ll start with the basics that are unique to Map:

  • containsKey checks to see if a particular object is in the map’s set of keys, by equals (this is fast, because it’s just like set access)
  • containsValue checks to see if a particular object is in the map (this is slow/linear, since generally it requires traversing each key’s value until the value is found or all values are checked)
  • get, which given a key returns the associated value (or null if not found!!!)
  • getOrDefault, which given a key and a default returns the associated value for the key (or the default if not found)
  • put which given a key and value, insert that key and value into the map (overwriting if previously in the map)
  • keySet returns the Set<K> of keys in this map
  • and so on; read the Javadoc for details.

Let’s use these to come up with an answer to our previous question (“how many presents per house”).

How many?

Let’s say we only want to build the table once for a given set of directions. It makes sense, maybe, to think about encapsulating this in a class. The class is instantiated with a set of directions, then can answer questions about what results from that set of directions. Here we go:

import java.util.Map;

public class SleighTrack {
    private Map<Location, Integer> locationCount;
    public SleighTrack(String directions) {
        locationCount = new HashMap<Location, Integer>();
        locationCount.put(new Location(0, 0), 1);
    }
}

And now let’s move our code over and adapt it to populate this map.

public SleighTrack(String directions) {
        locationCount = new HashMap<Location, Integer>();
        locationCount.put(new Location(0, 0), 1);

        int x = 0;
        int y = 0;
        for (int i = 0; i < directions.length(); i++) {
            switch (directions.charAt(i)) {
            case '>':
                x += 1;
                break;
            case '<':
                x -= 1;
                break;
            case '^':
                y += 1;
                break;
            case 'v':
                y -= 1;
                break;
            default:
                throw new IllegalArgumentException();
            }
            Location loc = new Location(x, y);
            int t = locationCount.getOrDefault(loc, 0);
            locationCount.put(loc, t+1);
        }
    }

Let’s add a toString method and see if it works:

public String toString() {
    return locationCount.toString();
}

public static void main(String[] args) {
    SleighTrack st = new SleighTrack("^>v<");
    System.out.println(st);
}

What do we expect? Same as the table above. What do we get?

{(1, 0)=1, (0, 0)=2, (1, 1)=1, (0, 1)=1}

Bingo!

In class exercise

Add public int housesVisited() and public Set<Location> locationsVisited() methods to SleighTrack. Hint: they should be one-liners using Map instance methods.

public int housesVisited() {
    return locationCount.size();
}

public Set<Location> locationsVisited() {
    return locationCount.keySet();
}

How about a public int numberPresents(Location loc) method that answers the question we posed initially?

public int numberPresents(Location loc) {
    return locationCount.getOrDefault(loc, 0);
}

On Map generality

Maps need not just be used to count things; they can be used to associate any (relatively immutable) set of keys with any values you like. For example, you could store:

  • URLs and the results of retrieving those URLs
  • filesystem paths and the sizes of those files
  • student IDs and photos
  • latin names and genomes
  • basically, anything.

We’ll be doing several more example of Maps in class next week!