12: Santa and `Map`s
Announcements
Plagiarism workshop! 6pm Tuesday Oct 23rd in CS 150/151. https://www.cics.umass.edu/event/hacking-plagiarism-how-use-sources-writing-code
DTA nominations are due soon. https://www.umass.edu/tefd/distinguished-teaching-award
Quiz: Generally we will excuse or permit a makeup with reasonable justification and/or documentation, especially if you ask in advance. But makeups must be done that week (as we return graded quizzes about a week later).
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.
In-class exercise
Suppose we want to add the following method:
public static int housesVisited(Set<Location> visited) {
// what goes here?
}
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)));
}
}
A reminder about assertEquals
: it uses the objects’ built-in equals()
method. If you have tests failing (say, in a project?), and the expected and actual look the same, maybe you forgot to define .equals
on your objects?
Anyway, 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++) {
if (directions.charAt(i) == '>') {
x += 1;
} else if (directions.charAt(i) == '<') {
x -= 1;
} else if (directions.charAt(i) == '^') {
y += 1;
} else if (directions.charAt(i) == 'v') {
y -= 1;
}
else {
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 List
s, one of Location
and one of Integer
, where the i
th 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 Map
s model, in essence, a table (on board). For our Santa problem, we might use Location
s as keys and Integer
s 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 Set
s – in fact, if you recall, Set
s are implemented using Map
s.
Like Set
s, Map
s come in two flavors you’ll likely use: HashMap
s and TreeMap
s, 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, byequals
(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 (ornull
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 theSet<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++) {
if (directions.charAt(i) == '>') {
x += 1;
} else if (directions.charAt(i) == '<') {
x -= 1;
} else if (directions.charAt(i) == '^') {
y += 1;
} else if (directions.charAt(i) == 'v') {
y -= 1;
}
else {
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 exercises
In our SleighTrack
, how might we compute the number of houses visited, given the code written so far?
public int housesVisited() {
// what goes here?
}
In our SleighTrack
, how might we find the set of houses visited, given the code written so far?
public Set<Location> locationsVisited() {
// what goes here?
}
In our SleighTrack
, how might we find the number of presents delivered to a house?
public int numberPresents(Location loc) {
return locationCount.getOrDefault(loc, 0);
}
On Map
generality
Map
s 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.