- Do not hurry! Especially on the easy problem where you are tempted. In this SRM I have overlooked an overflow issue. Detecting it before starting to code would have probably took me 3-4minutes. I did not and the result was a delay of at least 15 minutes.
- Have a good idea of how to treat special cases when you start coding. I had a O(2^N*N) solution to the medium problem but it had some off by one bugs related to whether people eat or not in the initial setting. Instead of taking a break and think about it I started doing more or less random changes because I had just a few more minutes. What a mistake! Think about it: even if you get lucky, would you feel good about yourself for not exactly knowing how you earned the points? [NOTE: the "normal" solution to this problem was O(2^N*N*N)]
- Elegance should still be a foremost concern: don't be drawn to the "speed demons dark side". In the end you will see that elegance and speed go hand in hand. I'm particularily ashamed of my solution to the easy problem in this SRM.
21 November 2004
srm 219 lessons
16 November 2004
operational semantics lectures
Here is a nice set of lecture notes on programming languages operational semantics.
14 November 2004
search engines
A few search engines you should know about:
And, just for comparison, you can take a look at the clumsy MSN Beta Search wannabe.
12 November 2004
merging lists
Literate programming is a nice approach to coding with some shortcomings. One of them is the lack of tools. However three of its central ideas can be applied by every programmer:
- Write programs for humans not for computers.
- Put links in your code that help the reader navigate the code (similar to the web).
- Keep code and documentation close to each other.
i < j implies cmp xi xj. A list has a repeated element if there is an i such that cmp xi xi+1 = cmp xi+1 xi (in other words we use the equivalence relation induced by cmp to decide if an element is repeated or not).
With these definitions we can restate the problem as follows. We are given two lists that are sorted and have no repeated elements. We must produce a list that has the same two properties and whose set of elements is the union of the set of elements of the given lists. All this implicitly refers to a comparator function cmp.
Here is an example. Suppose the comparator function is:
let cmp a b = (String.length a) < (String.length b);;It imposes a strict total order on strings. If the two input lists are
["x"; "bau"] and ["y"; "yy"; "bau"] then there are two answers that are considered valid according to our specification: ["x"; "yy"; "bau"] and ["y"; "yy"; "bau"]. Because as far as cmp is concerned "x" = "y": they have the same length.
Now, the first thing to notice is the symmetry of the problem. We are given two lists: they are not named in any way. It would be better to have a solution that exploits this symmetry. Another thing we can notice immediately is that if one of the lists is empty then the other list is a good answer.
let rec merge cmp l1 l2 = match (l1, l2) with | ([], lst) | (lst, []) -> lst <both lists are not empty case>[Note: this nice symmetric pattern matching was suggested by Seth Fogarty.] If both lists have a head then we have two situations: either the heads are equivalent or one of them is smaller. If they are equivalent then the answer list starts with either of them and continues with the merging of the tails (i.e. the rest of the two lists after removing both heads). If one head is smaller then it will also be the head of the answer. And the tail of the answer is the merging of the tail of the list from which we removed the head and the whole other list. That's an awful many words. The code will clear things up.
let rec merge cmp l1 l2 = match (l1, l2) with
| ([], lst) | (lst, []) -> lst
| (h1 :: t1, h2 :: t2) ->
match (cmp h1 h2, cmp h2 h1) with
| (true, false) -> h1 :: (merge cmp t1 l2)
| (false, true) -> h2 :: (merge cmp l1 t2)
| _ -> h1 :: (merge cmp t1 t2);;
.
Notice that we should really try to preserve the symmetry of the problem in the text of the program. It makes it much clear. Notice also the order of the parameters: cmp is not the first one by chance. If we think how this function is to be used we should see that specialization for different comparators is very likely. The client can write for example:
let merge_less = merge (<);;. Since we are looking at
cmp we can also see a performance problem. We will have O(min(m,n)) recursive calls and cmp is passed as a parameter although it does not change! Better:
let merge cmp l1 l2 =
let rec rm l1 l2 = match (l1, l2) with
| ([], lst) | (lst, []) -> lst
| (h1 :: t1, h2 :: t2) ->
match (cmp h1 h2, cmp h2 h1) with
| (true, false) -> h1 :: (rm t1 l2)
| (false, true) -> h2 :: (rm l1 t2)
| _ -> h1 :: (rm t1 t2) in
rm l1 l2;;
.
Another thing to notice here is the function names. The one that is visible from outside, who knows from what other parts of the code, has a longer, more meaningful name: merge. The one that has a "very local" visibility has a much shorter name: rm; it is meant to suggest "recursive merge" but it is not really essential for it to succeed; given it's narrow visibility, the brevity is more important.
Now, that is a nice solution! The only problem is that it is a bit slow since it is not tail recursive. The problem comes from the fact that in OCaml lists are singly linked. In other words they have only a cons constructor/destructor, and do not have a snoc constructor/destructor. If you are not familiar with functional languages you might wonder what the heck am I talking about :) Well, it is pretty simple. If you have a cons-list then what you can do with it is (1) split it into the first element and the rest of the list through pattern matching on (head :: tail) and (2) construct a new list by adding an element at its beginning using the same (::) operator. All this means is that the natural way of "reading" lists in OCaml is from left to right and the natural way of constructing them is from right to left. Our solution has poor performace because we go against this second rule-of-thumb: construct lists from last element to the first element.
A common trick is to split the processing in two steps: construct the reverse of what you need, and then reverse it. This is a trick I learned from Brian Hurt. Notice that to reverse a list you need to "read" (deconstruct) the input from left to right and "write" (construct) the output from right to left: so it is a "natural" operation in OCaml.
The solution becomes:
let merge cmp l1 l2 =
let rec rm l1 l2 acc = match (l1, l2) with
| ([], lst) | (lst, []) -> List.rev_append lst acc
| (h1 :: t1, h2 :: t2) ->
match (cmp h1 h2, cmp h2 h1) with
| (true, false) -> rm t1 l2 (h1 :: acc)
| (false, true) -> rm l1 t2 (h2 :: acc)
| _ -> rm t1 t2 (h1 :: acc) in
List.rev (rm l1 l2 []);;
.
The use of the accumulator (acc) is also a common trick to help separate the process in two steps. Now, don't you like this function? For comparison look at jartur's original code (he's just learning ocaml...):
let rec merge al bl sf =
let hl il el = match il with
| [] -> []
| hd::tl -> if hd=el then tl else il
and
z sff a b = if (sff a b) then a else b
in
if bl=[] then al else match al with
| [] -> bl
| hd::tl -> let x=(z sf hd (List.hd bl)) in
x::(merge (hl al x) (hl bl x) sf)
;;
.
Brrrrr....
The whole point of this entry is that if you think about your audience, exploit symmetries in the problem statement, and avoid being overspecific (i.e. adding in the mix details that are not essential) you will produce programs that are much more readable and, usually, more efficient.
03 November 2004
srm 215 & 216 lessons
- "Know your libraries" (leadhyena_inran)
- To split a string into words in C++ you can do
while (is >> word)since there is an implicit conversion from input streams tovoid*. - The STL has nice algorithms and utilities like
set_union,copy,inserter,back_inserter. It is not enough to know about them: you have to recognize opportunities to use them.
- To split a string into words in C++ you can do
- Debug methodically
- Never panic for lack of time. If time is short then don't rush: do it by the book.
- Make it repeatable. Investigate your data structures. Try different input data and try to infer the cause.
- Use sanity checks when you have redundant information in the data structures (Knuth).
- Print the content of the DS.
- Construct additional information and print it. For example if you are counting ways to build a spanning tree then actually build the spanning trees and print them. The same goes for counting anything.