Google HashCode 2022
Table of Contents
1. HashCode
HashCode is an annual competition that Google organizes. It is open to everyone in the world and people participate in teams of two to four members. It was my first time in this competition. Atish included me in his team and we four (Atish, I, Nirajan, and Subarna) spent the competition night solving the problem.
We had 3 hours 45 minutes to solve the problem and submit solutions. We can submit solutions as many times as we want, and teams are ranked according to the score. The actual problem statement in detail is here. I'll summarize the problem in short. There are some projects that require certain skills of some level. And there are people that have skills. We need to select people to be involved in the project, assigning people with required skill (at required level). Also, a person with skill level just one less than required can also take part in the project, if there is someone else in the project that can mentor him. Projects have scores, number of days for completion and best before deadlines. There were 6 problem data sets i.e. collection of projects and people. And we need to write code that generated solution to each problem, run the code locally and submit the solution. The score for each problem is the total score of projects completed. (There are some details that I didn't elaborate).
2. Parse Data
First Task was to parse the input dataset. So, I started with read-input
function, implementing its dependency functions turn by turn. I thought lookup would be necessary, so I stored people
, projects
in hashtable with names as keys. But, it wasn't necessary. However, skills
table was necessary for speed. It associates each skill with a list of people and the level of skill they have.
< Collapse code block
(defparameter *people* nil) (defparameter *projects* nil) (defparameter *skills* nil) (defun add-to-table (table data) (loop for row in data for key = (getf row :name) do (setf (gethash key table) row))) (defun read-input(file) (setf *people* (make-hash-table :test 'equal)) (setf *projects* (make-hash-table :test 'equal)) (setf *skills* (make-hash-table :test 'equal)) (with-open-file (f file :direction :input) (let ((people-n (read f)) (projects-n (read f))) (let ((people (loop repeat people-n collect (read-people f))) (projects (loop repeat projects-n collect (read-project f)))) (add-to-table *people* people) (add-to-table *projects* projects) (list :people people :projects projects) nil))))
I didn't use structs or classes to represent people, or projects. This decision was influenced by a sentence/idea Rich Hickey said in a talk, "Let Data be Data". In retrospect, I now know in this case that quote/ideas wasn't applicable, but being in no mood to contemplate on minor details (because of limited time), I had decided not to use structs. So, lists it was. Plain lists like
< Collapse code block
("Bob" (("HTML" 5) ("CSS" 3))) ("Logging" 5 10 5 1 (("C++" 3)))
could have worked but without thinking much I used property lists. And this was better, because with plain lists I needed to remember the position of data. (nth 1 person)
to get the list of skills, and there would be chances of mistake. But with property list, I didn't need to remember location (getf :skills person)
.
< Collapse code block
(:name "Bob" :skills (("HTML" 5) ("CSS" 3))) (:name "Logging" :days 5 :score 10 :bf 5 :skills (("C++" 3)))
< Collapse code block
(defun read-string (file) (with-output-to-string (str) (loop for char = (read-char file) do (if (equal char #\Space) (return) (write-char char str))))) (defun register-skill (skill person) (let ((s (first skill)) (l (second skill))) (push (list l person) (gethash s *skills*)))) (defun read-people (file) (let ((name (read-string file)) (skill-n (read file))) (let ((skills (loop repeat skill-n collect (list (read-string file) (read file))))) (let ((person (list :name name :skills skills))) (loop for skill in skills do (register-skill skill person)) person)))) (defun read-project (file) (let ((name (read-string file)) (days (read file)) (score (read file)) (bb (read file)) (roles-n (read file))) (let ((roles (loop repeat roles-n collect (list (read-string file) (read file))))) (list :name name :days days :score score :bb bb :skills roles))))
3. First Iteration
The main flow of my program was simple:
- From all the uncompleted projects, find projects that we can start (I call these
solvable-projects
in the code) - Select one of those projects
- Select people to work on that project
- Then mark the project as done and increase the skill level of people
< Collapse code block
(defun get-people-ge (skill level) "return list of people with `skill' level greater than or equal to `level'" (loop for (l person) in (gethash skill *skills*) when (>= l level) collect person)) (defun get-people-e (skill level) "return list of people with `skill' level equal to `level'" (loop for (l person) in (gethash skill *skills*) when (= l level) collect person)) (defun solvable? (peoples) "return one possible team assignment" ;; peoples is in reverse order (when (every #'first peoples) ;; pick min skilled person strategy (let ((assigned nil)) (loop for (can may) in peoples for assigned? = nil do (loop for person in can do (unless (find person assigned) (setf assigned? t) (push person assigned) (return))) (unless assigned? (return-from solvable? nil))) assigned))) (defun solvable-projects () "returns list of projects that can be completed" (let ((result nil)) (maphash (lambda (key value) (declare (ignore key)) (let ((peoples nil) (solvable t)) (loop for (skill level) in (getf value :skills) for can-people = (get-people-ge skill level) for maybe-people = (get-people-e skill (1- level)) do (if (and (null maybe-people) (null can-people)) (progn (setf solvable nil) (return)) (push (list can-people maybe-people) peoples))) (when solvable (let ((soln (solvable? peoples))) (when soln (push (list :project value :people (reverse peoples) :one-solution soln) result)))))) *projects*) result))
I looped through the projects and for each skill a project requires, found can-people
(who have skill level greater or equal to that required) and may-people
(those who have one level below, but may take part in the project if a mentor is present). Then, if the project is possible with the those peoples (solvable? peoples)
(peoples = list of list of person), add it to the list of possible projects. (solvable? peoples)
returns one possible team for the project (selecting the first person available for the task).
Now, I need to select a project and complete it.
< Collapse code block
(defun do-project (project people) (loop for (skill l) in (getf project :skills) for person in people for sk = (find skill (getf person :skills) :test #'string-equal :key #'first) do (when (<= (second sk) l) ;; level up!! (incf (second sk)) (let* ((levels (gethash skill *skills*)) (record (find person levels :key #'second))) (incf (first record))))) (remhash (getf project :name) *projects*)) (defun main (file out) (read-input file) (let (result) (loop for solvable = (solvable-projects) do (unless solvable (return)) (let ((assignment (getf (first solvable) :one-solution)) (project (getf (first solvable) :project))) (do-project project assignment) (push (list :project (getf (first solvable) :project) :assignment assignment) result))) (setf result (reverse result)) (write-output out result)))
I did the simplest thing possible, select the first project from the list, and do-project
. Doing a project is also simple, remove the project from the *projects*
table, and increase the level of people working in the project if applicable.
3.1. Result and Discussion
Attempt | Set | Score | Time |
---|---|---|---|
25 | F - Find great mentors | 42282 | 02:27:24 |
24 | E - Exceptional skills | 1208352 | 02:27:23 |
23 | C - Collaboration | 4180 | 02:24:19 |
22 | B - Better start small | 741913 | 02:24:19 |
21 | D - Dense schedule | 133020 | 02:24:19 |
Total | 2129747 |
So, we got a total score of 21 Lakhs (about 77% of our final score) from this first code. The core logic of the solution was simple. However 2.5 hrs had already past, and we had only 1.25 hours left. The remaining 1.25 hours were spent making slight adjustments and resumitting the solution, increasing our score few lakhs at a time.
There was one flaw in the core logic that I missed during the competition. Projects can be done parallelly, but increasing the level of people and having no concept of a person being not available meant that sometimes the projects which could have executed parallelly were being done sequentially, decreasing the score.
4. Next Iterations
Since the core logic was simple, places possible for optimization were obvious.
- Finding projects that we can start : This can be optimized in terms of code execution time, but needs no change in the logic. And my main concern wasn't code execution speed, but solution score. I could wait extra 1 min for the program to give me the results.
- Select project : First place for logical change/optimiaztion
- Select people to work on the project: Second place for logical change/optimization
- Mark the project and level up the team members : This also needs change in logic but as I mentioned earlier it didn't occur to me at that time.
So, in the second and next iterations I made small changes in the project selection and team selection strategy. Some strategies were silly, some worked. for example in the second iteration, I selected team members for each skill randomly rather than selecting the first one available. However this gave almost the same score as first iteration.
5. Our Best Iterations
Rather than listing all changes in each iteration, lets look at the changes that gave top score in each problem. In each case changes were only done in two places: choice of project, and choice of team.
5.1. Problem B, C & D
Attempt | Set | Score | Time |
---|---|---|---|
115 | B - Better start small | 743212 | 03:37:11 |
119 | C - Collaboration | 24716 | 03:39:40 |
116 | D - Dense schedule | 133020 | 03:37:11 |
< Collapse code block
(defun project-bb-<= (p1 p2) "return true if project1 has best before deadline earlier than project2" (<= (getf p1 :bb) (getf p2 :bb))) (defun choose-project (solvable) (first (sort solvable #'project-bb-<= :key (lambda (p) (getf p :project))))) (defun solvable? (peoples) ;; peoples is in reverse order (when (every #'first peoples) ;; pick min skilled person strategy (let ((assigned nil)) (loop for (can may) in peoples for assigned? = nil do (loop for person in (sort can #'< :key (lambda (p) (getf p :exp))) do (unless (find person assigned) (setf assigned? t) (push person assigned) (return))) (unless assigned? (return-from solvable? nil))) assigned)))
- Project choice: Choose the project with the earliest best before deadline.
- Team choice: Choose the members with least
:exp
(experience points)
After a project completion, each team member's :exp
was increased by one i.e. it is a count of number of projects that the person participated in. So, choosing a person with least :exp
meant more new people would be involved, meaning that the chances of project being executed parallelly was increased.
This simple change increase our score by about 21 thousand.
5.2. Problem E & F
92 | E - Exceptional skills | 1637258 | 03:20:20 |
78 | F - Find great mentors | 226272 | 03:05:01 |
The best solution for problem E & F from our team was not mine. Other team members were trying different strategies on their own. In overall we increased our score by 6.1 Lakhs.
6. Scores
Our best score:
Attempt | Set | Score | Time |
---|---|---|---|
1 | A - An example | 33 | 00:35:28 |
115 | B - Better start small | 743212 | 03:37:11 |
119 | C - Collaboration | 24716 | 03:39:40 |
116 | D - Dense schedule | 133020 | 03:37:11 |
92 | E - Exceptional skills | 1637258 | 03:20:20 |
78 | F - Find great mentors | 226272 | 03:05:01 |
Total | 2764511 |
For me, score of 27 lakh and 589th position in global scoreboard was not bad. Global winner (team Make love, not war from Russia) had a score of about 42 Lakh. I hadn't expected that from about ~10,000 teams worldwide, we could be in top ~600th. But more importantly, the challenge and those 3.75 hours were fun.