Advent of Code (AoC) for this year started yesterday. I’ve been too busy with work to write here how my puzzle solving has been going. Or anything else, for that matter. Probably will be for the rest of this year.
Why busy? I’ve been moving Data structures and algorithms course exams from Moodle to Exam. Exam is a controlled environment as Moodle was not (at all). There is a tool to import Moodle exam questions to Exam but that is so limited that I decided to hand write new exam questions for this year.
This has taken time, also learning how the Exam works since I have never used it before. Also I’ve been preparing new demonstrations and visualizations for the course to aid learning. There’s still one visualization demonstration to do, namely how the Dijkstra’s shortest path algorithm works. If I do not find the time for that, I just have to search for existing demonstrations from the internet. Would like to use the familiar graph examples from the lectures and other demo materials though.
Anyhows, I have solved both Day 1 and Day 2 puzzles in AoC, using Swift as last year. If you wish to see how the actual puzzles look like, just head on to the AoC website.
Yesterday’s part 2 was a bit difficult, mainly due to some basic mistakes I made in the beginning, and not starting from scratch when I saw the mistakes I had “fixed” with unnecessarily complex code. Sauna break in the evening helped, teaching and other work in between less so 😅. And starting from scratch was a good idea.
Day 1 implementations were fast, part 1 took 0.000684708 seconds to execute, part 2 took 0.000698 seconds (on MacBook Pro M2, Swift implementation, release build, obviously). Lines of code I needed for the part 1 solution is 18, for part 2 line count is 32.
Day 2 implementation’s Part 1 (17 lines of code) took 0.150482667 seconds to execute, while part 2 (27 lines of code) took 0.437480584 seconds. Here I was using Swift’s .split, .map, stride (a for loop kind of a thing) and chunks(ofCount:) from Swift Algorithms package. Much more straightforward (for me) than yesterday’s part 2.
Now to lunch, then to prepare for a MSc thesis supervision session, then to teach (remotely, as usual) for the rest of the afternoon.
I’ve been busy with preparing the Data structures and algorithms (DSA) course. As mentioned earlier, I decided to remake all the programming tasks, which I did.
Partly they were edited from earlier year tasks. Like the Graphs task is the old 2021 Mazes task with a Pac-Man kind of a game. Each task has the actual algorithm and/or data structure to implement, and additionally a task to apply that in some specific problem solving situation. Another programming task applying a data structure is the implementation of linked list and then using it as a snake in a Snakeses game.
I also decided to remake all the recorded lecture videos. There was a couple of errors in the slides and some content needed revisions and better treatment. So I did that too.
All in all my attempt was to make the course easier to pass without cutting or compromising the learning goals. It has become kind of a bottleneck course in the curriculum when the compulsory prerequisite to a later course was cut away. Now students tend to take it later than before, so the number of students passing has been slightly lower than before. Hopefully this helps, well see.
Courses started two weeks ago and the first deadline of DSA is tomorrow at noon. Students are supposed to deliver the URL to the remote repository where their code will be.
I also remade the tool teachers and students can use to check the code does use the building blocks they should use. Like when implementing a recursive binary search, the tool checks that the function actually calls itself. Another example is a data structure’s toString() -method. There, it must use StringBuilder, not String — since it is thousands of times slower with large number of elements in the collection.
Some checks are made to make sure students do not use loops in various cases where they should cope without. So the tool has a rules file listing all these different kinds of rules to follow:
The implementation then uses the com.github.javaparser library to parse the student code and check if the rules are followed. To avoid false warnings (and to some extent trying to fool the tool), the tool first clears comments and literal string values out of the code, and then analyses if the rules are fulfilled.
—
Otherwise, I’ve been in the usual Fall flu for the past ~25 days. I had to stop running due to that. A week a go I had to visit a doctor due to feeling really odd, like fainting. Nothing was found, except that the white cell levels in blood were a bit elevated. Strange times, strange illnesses.
First run after the break was the on Tuesday this week. I did 5+ km run, and yesterday 4+ km run, totaling a bit over ten km. Hopefully no more sickness this year.
—
Now I’ll have to rush to a teachers’ meeting. Returning back to this blog later, hopefully sooner.
I usually start my summer vacation from Midsummer. This year that’ll mean that the last workday is today 🥳 I’ll be returning to work in the beginning of August.
So far, I’ve been busy in several fronts, including supervising and evaluating MSc theses and planning and organizing the teaching schedules and resourcing for the Fall. We eventually got one half time student assistant to help us in the courses for the Fall semester.
We were required to provide at least some face-to-face teaching for 1st year students, since complaints this study year about only online/remote teaching offered. It is “a bit” difficult to organize classroom teaching when the total number of students is 900+ and there are only two full time teachers and two other teachers who have only some hours allocated for these three large courses (plus one smaller) we need to organize. Especially when the remote possibility is a must, not an option, since these courses have Open University students and students from other Finnish universities participating.
Anyways, we again tried to fulfill these requirements with the little resources we’ve got. In some earlier years, we have had two halftime student assistants, last year only one and that’ll be the case for next Fall too. Not even comparing to 15 years ago when we had like ten+ student assistants helping out in various courses. Things really have changed from those days. Let’s see how we cope this time…
Another big thing I’ve been working on is implementing new programming tasks for my Data Structures and Algorithms (DSA) course. The passing ratio of the course is too low. I am again trying to find new ways to make the course more straightforward to pass.
Last year I made explicit paths to pass the course so that the students could take an easier path with less programming to get the grade 1. If someone wanted a higher grade, then they’d do additional programming tasks. And if someone wanted a high grade (4-5/5), then they were also required to participate in one-to-one evaluation discussion with me, talking through the algorithms and data structures they implemented and analyzed in their reports.
Before last year’s changes, most students got a grade 3-5. After this change, most students got the grade 1, then some the grade 2 and minority got grades 3-5. Not sure which change contributed to what extent to this change in the course output.
Unfortunately, the passing rate did not improve regardless of these changes. Most of the students that fail actually either never start doing anything, or they do at most the 1-2 first programming tasks (which actually were based on the previous prerequisite course contents) and then disappeared. Maybe they just decided that the course is too challenging considering their prerequisite knowledge and skills? Or they had more important courses they prioritized?
One reason may be that DSA is no longer a prerequisite for any other course in the curriculum. A couple of years ago it still was a prerequisite and students needed to pass DSA to get into Programming 3 (server side stuff). That was then required to enter Programming 4 (GUI programming) and via that path, getting in the BSc Project course was not possible without passing DSA first in the chain. As the prerequisite DSA ⇒ Programming 3 was removed, it may well be that DSA will be one of those last ones they try to pass before getting their BSc degrees, together with the compulsory Swedish language course… Maybe the hypothesis was that DSA is useless for the courses coming after it. Not that I agree with that…
The issues of plagiarism and using AI to avoid learning and doing things have somewhat increased during the last 2-3 years. These issues are not too widespread yet (?), but still clearly on the rise. I’ve now used the same programming tasks for two years, and it is apparent that some are getting the solutions copied from somewhere.
Luckily there are lots of students that are willing to do the work and walk the path, regardless of the challenges — always very motivating to the teachers to see. It is very rewarding for us teachers to discuss issues with students, walk them through their problems, seeing their joy of finally solving difficult issues and at the same time see them learn and gain skills that are important for their professional development as software developers.
Items from above were the motivation for me to implement new programming tasks for the DSA course. Most of the task learning objectives are the same as before. I decided to drop the first task that was just revisiting the insertion sort algorithm taught already in the prerequisite course. That algorithm is now given to the students, as they are supposed to know it already. So no two weeks of course time is spent on that anymore.
Instead, the first task focuses on correctness, first of the two important learning goals for the DSA course, the other being time complexity (to some extent also memory complexity). They’ll learn to use invariants to ensure code is behaving correctly, including pre- and postconditions, as well as loop and class invariants, implemented with asserts and thrown exceptions.
I’ve brought back the partitioning algorithm task back from 2021 course implementation. The binary search tree task will be a bit easier; the current task is too challenging because the context of applying it makes it too large a task to do. So that’ll be simplified.
Furthermore, all of the tasks where they apply the generic algorithm or data structure they first implement, will change. There will no longer be a single example app using all those algorithms and data structures they implement. Some students did value this concrete example with a GUI; seeing a real world context where all this they are supposed to learn, is actually used and needed. But for many, this context was too large, too much code to digest and understand, they got confused and felt lost. In the next DSA, each task they go through will be in a separate, small and simple(r) component, focusing only on a specific algorithm or data structure and how to apply it and analyze it.
Another of my goals is also to move the course exams from Moodle to controlled computer exam class. It is apparent that the Moodle exam is too easy to do in a way that doesn’t measure the students’ actual knowledge of the topics. Maybe the new setup does this better, if I manage to prepare the new exams in the new environment in time for Fall…
The new set of programming tasks has 11 different tasks, and I’ve currently (re)implemented five of them:
Assess and support code correctness of algorithms by using asserts, pre- and postconditions and loop and class invariants.
Recursive binary search, comparing time efficiency with linear search.
Partitioning an array using a predicate, filtering utilizing the partitioning algorithm.
Quicksort, comparing it with insertion sort and partitioning/filtering, when sorting is not actually necessary.
Stack, using stack to check XML correctness (parsing code is given).
That leaves six tasks to be finished in August, before the course begins in September:
Queue.
Linked list.
Binary search tree, applying it to count unique words in Project Gutenberg books / frequency of person names in datasets.
Hash tables and hash functions, possible application is the same as with binary search tree, to compare time/memory efficiency of these two ?
Using the Set in Java (not implementing it).
Something about Graphs (breath/depth first searches?), or leave this to theory only, depending on the estimated student workload.
As usual, all tasks will have automated unit tests and also the code is analyzed by using tools that I’ve discussed in some of my previous posts.
To get all this done in time, it’ll be a busy month in August. Especially when I want to avoid having errors with the material given to students. I should reserve time to test and assess the code and the instructions so that I do not have to post too much errata when the course starts…
In the past, I did use the summer time to do these kinds of teaching development work, but since the past 2-3 years, I’ve decided not to sacrifice myself to work. Vacation time is my time, not the employers.
Hopefully you, dear reader, have also the time to relax during your vacation time, whenever that might be. Have a nice Summer! 🌞 (or Winter if you locate at the other side of the globe, don’t actually know if it is dry or rainy season in between 😃).
In the post last week I mentioned that I’ve added new features to the tool I use in Data Structures and Algorithms course to supervise, support, test and grade the student programming course work. I’ll describe here the new features and provide some (hopefully) interesting implementation details about those.
Background about the course that helps in understand the role of this tool:
students each have their own github or gitlab repository to where they fork the initial status of their course work from the teacher repository,
repository contains the task instructions, skeleton code to implement, unit tests code and other supporting code given ready for students,
repository tests enable testing the student’s implementation of the data structures and algorithms that are required to implement in the course,
teachers have access to the student repositories to assist students with any issues, and finally to grade the work.
Before going into details, a comment about the motivation for this tool to exist in the first place: The course the tool is used in, has 300+ enrolled students and only two full time teachers (sometimes we do have 1-2 assistants with small number of hours, like 50 hrs in total for the whole Autumn), simultaneously teaching 2-3 other 200-300 student courses. That totals around 900+ active students taught simultaneously, so providing individual support and attention to all is practically impossible, at least if no automation (and other support tools) is taken into use.
So the critical thing in managing all this work is, that we do need automated tools to:
keep track of how the students are progressing in the course and help those who desperately need it, and after the last deadline,
to evaluate and grade the students’ programming submissions as fast as possible.
The grading should be done in three weeks after the last deadline, so it cannot be done in time manually. The aim is to use this tool during the course, trying to catch those students that are not doing OK, and then try to contact and help them in getting their coursework going and spotting any possible mistakes in their work so that they can correct their mistakes before the final deadline.
Students next Fall will receive the same unit tests and the same code analysis tools (in Java unit tests) they can also themselves use. However, this is not enough, especially if the students do not contact the teachers about their issues soon enough or at all. A problem that is too common, unfortunately.
The new features added to the teachers’ tool during this Winter and Spring are:
Automatic source code analysis:
results of the source code analysis are stored in a database table; repository table has a column where problems found in code are flagged,
tool checks for java imports that are not allowed, notifies about this and sets error flag in the database table,
tool also uses the Antrl parser generator to check if method implementations in the source code violate any rules and if the code does not do the things it should do.
Remote repo checks for both github and gitlab:
checks that remote is not public, the rule of the course,
checks that remote has only allowed contributors.
Testing and grading enhancements
tests are now grouped to grade level tests,
when all tests in a grade group passes, grade is stored to repository table,
grade tests can be executed in whichever order, since grade value is bit flag,
test for a certain student repository can be disabled if they cause issues like tests never ending or taking way too much time,
if git pull updates code, grade is set to zero and test results are cleared.
Status reporting
the tool now generates a textual status report that can be sent to student by email, listing how test pass, if any forbidden things are used / things to use are not used, git statistics etc.
Let’s take a closer look at the code analysis done by the tool in this post. The other features are left for future possible posts to discuss.
Analyzing if the source code uses “forbidden” imports was implemented with simple text parsing techniques. The tool goes through the Java source files in a certain directory where student code is located, reads the .java files and checks if the source uses imports that are not allowed.
For example, when implementing a Stack data structure, student must not import the java.util.Stack and just use it instead of implementing their own stack from scratch, using an array as internal data structure. Or when sorting an array, java.util.Arrays is not imported and Arrays.sort is not used, because student should have used their own implementation of the sorting algorithms.
These kinds of mistakes can be found by analyzing the imports in the student’s .java files. You cannot call Arrays.sort without having import java.util.Arrays in the .java file.
The tool first reads a rules file that contains the import rules for the course:
For example, the java.util.Comparator may be used by the students in all of their .java files (*). The same goes with java.util.function.Predicate and all the files in package oy.interact.tira (the course source packages). The class SetImplementation may also import java.util.Iterator.
Et cetera. As you can see, this is an allow list, not a deny list. The imports that are allowed and can be used in the course are small in number, so it is easier to have an allow list than deny list.
Reading the allow list and analyzing the source files was straightforward to implement by simple file handling and text parsing. First read in the rules file and parse the import rules from there, into this structure:
fileprivate
struct SourceRequirementsSpecification {
var allowedImports: [String: [String]] = [:]
The Swift dictionary object allowedImports has the class as the key (either * or the name of the class rule is about), and then the array of allowed imports the class may contain.
Then the tool starts reading the student .java files and handles all lines beginning with import to check if that class may import that specific thing or not. If the .java file imports something that was not allowed, the tool database table (for the student’s repository) having a column with integer value, a specific bit in that number (a “flag”) is set to 1 (true). This indicates that the student’s code imported something it was supposed to not do.
This is the data structure keeping track of these repository flags, if they are set or not set:
struct Flags: OptionSet {
typealias RawValue = Int16
var rawValue: Int16
static let remoteFails = Flags(rawValue: 1 << 0)
static let buildFails = Flags(rawValue: 1 << 1)
static let usesForbiddenFeatures = Flags(rawValue: 1 << 2)
static let repositoryIsPublic = Flags(rawValue: 1 << 3)
static let repositoryHasOutsideCollaborators = Flags(rawValue: 1 << 4)
static let repositoryTestingIsOnHold = Flags(rawValue: 1 << 5)
}
The Swift OptionSet is a type that presents a mathematical set interface to a bit set. So each flag is a single bit in the 16 bit integer, being either off (0) or on (1). So instead of having six boolean columns in the database table, I can have one 16 bit integer column to store 16 different flags. Most of those still available for future needs.
The flag usesForbiddenFeatures is set in this case of student code using forbidden imports (or other features, discussed below). Other flags are used to:
track if the remote repository cannot be accessed at all (remoteFails) or is empty,
compiling the code fails (buildFails),
repository is public even though it must be private (repositoryIsPublic) and finally
if the repository has other collaborators than the student owning the repo and the course teachers (repositoryHasOutsideCollaborators).
These are all issues that have been met in the course during the previous years.
Final flag repositoryTestingIsOnHold can be set manually from the tool user interface for those repositories that mess up the testing, either because the tests are stuck or take too much time and therefore should not be executed before fixed.
That flag struct is a 16 bit integer, the datatype of the database column storing the flags in each student repository database table. If the code has issues related to these rules, the flag is set. When the git repository is updated from the remote, the user needs to run the code analysis again. If a violation of a rule was fixed, the flag is then reset to zero (false), if no other violations are found.
The imports checking from source code is not enough, though.
Sometimes students do mistakes in method implementations they are not supposed to do. Like when implementing a recursive binary search, they implement an iterative binary search. Or when sorting a table in descending order, they first sort in ascending order and then reverse the table order. Since time efficiency is the major topic of the course, this is not what they are supposed to do. Instead, they should use a comparator to sort the table to descending order in the first place, and then no reverse operation is needed at all, making the operation faster.
For checking these kinds of issues the import rules are not enough since these kind of mistakes can be made without importing something. Therefore, the same rules file contains also rules like this:
[codeRules]
# Algorithms.binarySearchRecursive must contain word binarySearchRecursive
Algorithms.binarySearchRecursive,must,binarySearchRecursive
# Algorithms.binarySearchRecursive must not contain word while
Algorithms.binarySearchRecursive,mustnot,while
Algorithms.binarySearchIterative,must,while
CodeWordsCounter.topCodeWords,must,Algorithms.fastSort
CodeWordsCounter.topCodeWords,mustnot,Algorithms.reverse
Here lines beginning with # are comments and ignored by the tool. Illustrating the examples of mistakes above, the rule:
Algorithms.binarySearchRecursive,mustnot,while
specifies that the algorithm binarySearchRecursive in class Algorithmsmust not contain the keyword while. Since breaking this rule implies that the student has implemented the recursive algorithm using iterative approach. Therefore, the learning goal of implementing a recursive binary search has not been met, and code needs to be fixed.
These rules are also parsed from the rules file into the above mentioned rules structure, which now looks like this:
enum Rule {
case must
case mustNot
}
struct CodeRule {
let methodName: String
let rule: Rule
let stringToMatch: String
}
fileprivate
struct SourceRequirementsSpecification {
var allowedImports: [String: [String]] = [:]
var rules: [String: [CodeRule]] = [:] // ClassName : CodeRules
}
In addition to the allowedImports rules, the structure now contains also rules for a class (the Swift dictionary key String has the class name), where the CodeRule structure has the name of the method the rule is about, then if the method must or must not (Rule) contain the string to match.
This is a very simple current implementation, and as I develop this further, it may change to something different later. For example, it should be possible to say that the recursive binary search algorithm must not contain “while” but also not the “for” keyword.
The second example about being able to realize that descending order can be implemented by a specific comparator (instead of ascending sort and then doing reverse, which is more time-consuming operation) is checked by the rule that the class CodeWordsCounter in the algorithm topCodeWords (where the work in the coding task is done) must not call Algorithms.reverse.
If a student uses the Java Collections.reverse or Apache Commons ArrayUtils.reverse for this, that mistake is already caught using the import rules.
For this feature implementation, I used the Antrl parser generator. Antlr has existing grammars for many languages, including Java, and it supports generating parsers also for Swift. So all I needed to do was to get the Antlr tool and the Java grammar files (Java20Lexer.g4 and Java20Parser.g4) and then generate the Java parser and lexer for Swift:
$ antlr -Dlanguage=Swift -message-format gnu -o Autogen Java20Lexer.g4
$ antlr -Dlanguage=Swift -message-format gnu -o Autogen Java20Parser.g4
Those commands place the generated Swift source code files in a subdirectory named Autogen.
Using the generated Swift code from Autogen, the tool can then parse Java code. All I needed was to implement a class that extends the Antlr generated Java20ParserBaseListener, give the rules to this class and start parsing the source string read from the student’s java file, containing the Java source code:
import Antlr4
class Java20MethodBodyChecker: Java20ParserBaseListener {
private let file: String
private let rules: [CodeRule]
private var currentMethod: String = ""
private var stream: ANTLRInputStream? = nil
var violations: [String] = []
func start(source: String) throws {
stream = ANTLRInputStream(source)
if let stream {
let lexer = Java20Lexer(stream)
let parser = try Java20Parser(CommonTokenStream(lexer))
let parserTree = try parser.compilationUnit()
let walker = ParseTreeWalker()
try walker.walk(self, parserTree)
}
}
// ...
Analysing the code happens in the overridden handler for parsing Java methods, enterMethodDeclaration. There, the implementation checks, using the rules, if the Java code contains any keywords that should not be there or does not contain keywords that should be here:
override func enterMethodDeclaration(_ ctx: Java20Parser.MethodDeclarationContext) {
if let identifier = ctx.methodHeader()?.methodDeclarator()?.identifier() {
currentMethod = identifier.getText()
let ruleSet = rules.filter( { $0.methodName == currentMethod } )
if !ruleSet.isEmpty {
if let body = ctx.methodBody() {
let bodyText = body.getText()
for rule in ruleSet {
switch rule.rule {
case .must:
if !bodyText.contains(rule.stringToMatch) {
violations.append("\(file).\(rule.methodName) ei käytä \(rule.stringToMatch), vaikka pitäisi")
}
case .mustNot:
if bodyText.contains(rule.stringToMatch) {
violations.append("\(file).\(rule.methodName) käyttää \(rule.stringToMatch), vaikka ei pitäisi")
}
}
}
}
}
}
}
The violations then contains all the things found to be against the rules.
This worked, except for one thing: if the code has comments that do/do not have the specified keywords of the rules, the code analysis did check those comments also. Even though the comments do not influence the implementation and are therefore totally irrelevant.
So if the comments of recursive binary search algorithm contains the keyword while, the tool would report that as a violation of the rule! Or if the code contains a required keyword, but not in the actual code implementation but only in the comments, the analysis would be satisfied. End result: not satisfactory at all.
First I started to fix this by implementing code myself to remove the comments from the code in a String variable before the analysis, but soon I realized that the Antlr parsing tool should somehow already handle this.
What I did instead, was (after some searching on the internet) to modify the Antrl lexer file for Java so that it does not parse the comments at all, but skips them:
Then I regenerated the Swift parser and lexer source files and added them to the tool project.
Using the new lexer/parsers, the parsed Java code does not anymore include comments and I do not have to consider them at all! This works perfectly.
Without asking anything about any “AI” tools out there. As usual, I do not touch them even with a long stick. I prefer to discover and learn myself, as long as the internet is not polluted with “AI” generated crap which makes everything impossible.
Below you can see the tool reporting about imports in a student repository, imports that should not be in the code, and that the code in CodeWordsCounter.topCodeWords does not call Algorithms.fastSort, even though it should, based on the rules in the rules file.
As you can see, the first violation is because the student has misnamed the class as setImplementation. In Java, classes (and therefore also class files) should always begin with capital letters and the name should be SetImplementation instead. So this is a mistake, but actually does not violate the import rule; the Set data structure implementation may use the Iterator. Therefore, when the rules are found to be violated, it often requires human rechecking to make sure no unfair judgments are made by the automation.
Considering the 300 something students in the course, perhaps each of them making at least 3-5 mistakes the code analysis could find, this results in 900-1500 mistakes in total. For the teacher to comb through all the hundreds of lines of code in each of the student projects, repeatedly during the course, working on this “code quality control” task manually is very time consuming and therefore not a realistic task. Especially considering the available teaching resources mentioned in the beginning.
Hopefully the new features of this tool and the similar tests given to students in Java test code, improve the support the teachers can provide during the course to lead the students to the correct path of implementing the required data structures and algorithms.
The post is getting quite long, so the rest of the new features will perhaps be discussed in future posts. Unless I decide to focus on other more interesting topics. Have a nice day!
This was surprisingly easy! The part 1 of today’s task was correct on the first run, which is the first time this has happened to me! Usually I make stupid mistakes, not reading the puzzle carefully enough, and rushing to coding with too little planning ahead.
The puzzle part 1 was to find out positions on a map to place antinodes to prevent antennas to emit a signal that makes people 0.1% more likely to buy Easter Bunny brand Imitation Mediocre Chocolate as a Christmas gift.
Calculate the impact of the signal. How many unique locations within the bounds of the map contain an antinode?
Where 0 and A are antennas that are sending on a same frequency, A antennas on their own and 0 antennas theirs, respectively. To prevent their signals, the antinodes ( #)must be placed in line of the antennas, after the first and last pair of antennas, like this:
To solve the problem, I implented a Dictionary with key-value -pairs (the [Character: [Point]] below), where the key is an antenna with a specific frequency and value is an array of (x, y) positions on the map where that kind of antennas are placed.
var antinodes: Set<Point> = []
let matrix: Matrix<Character> = parse(data: data)
let antennaMap: [Character: [Point]] = parseAntennaMap(matrix)
After parsing the antennas and their positions into a Matrix, and extracting the antennas and their positions into the dictionary, I could print out the dictionary keys (antennas) with the values (a list of positions for the antennas with the same frequency ):
Antenna A 5:6 8:8 9:9 Antenna 0 1:8 2:5 3:7 4:4
So I could verify that everything is in order for the next step of the puzzle.
I already had a Point structure, having the x and y coordinates, as well as operations to calculate the difference of two points in the coordinate (the distance between them) as well as add together two coordinate points to extrapolate the line between two antennas to position the antinodes inline with the antennas.
So what to do with these? First I needed to handle all the antennas with the same frequency in pairs, to extrapolate the line between them to both directions to add the antinodes. The permutations(ofCount:) algorithm did that. For each pair of antennas, I determined which comes first (point is upper on the map compared to the other) and vice versa.
After that I counted the difference between the first and last point, because the antinode must be place an equal distance away from the antennas. Then I added a point above the first point (first - difference) and below the last point (last + difference).
Then I just added these two antinodes into a Set to get a number of antinodes placed on the map:
for (_, points) in antennaMap {
let permutations = points.permutations(ofCount: 2)
for permutation in permutations {
let first =
permutation.first! < permutation.last! ? permutation.first! : permutation.last!
let last = first == permutation.first! ? permutation.last! : permutation.first!
let difference = last - first
antinodes.insert(first - difference)
antinodes.insert(last + difference)
}
}
antinodes = antinodes.filter({ matrix.contains(y: $0.y, x: $0.x) })
return antinodes.count
Finally I made sure no antinodes were placed outside the map coordinates (using filter to include only those points in the area of the matrix coordinates) and finally returning the count of antinodes placed (the puzzle answer).
Part 2 was just an addition to part 1. Instead of placing one antinode before and after the line of two antennas, place them so that the antinodes are repeated with equal spacing along the line formed by a pair of antennas, until to the edge of the map.
What I first missed in the puzzle instructions was that the positions of the antennas also count in the final answer, not just the antinodes. I did get that the unique positions only are counted (that is why the Set<Point> is used for antinode positions). As I realized this, I just formed a union of antinode and antenna positions, and count of those was the correct answer!
In total, this didn’t take long to solve, thanks to the existing data structures and algorithms, both from Swift, Swift libraries (Swift Algorithms) and my own utilities from previous years. Performance is also OK:
$ swift run -c release AdventOfCode 8 --benchmark Building for production... Build of product 'AdventOfCode' complete! (0.28s) Executing Advent of Code challenge 8... Part 1: 259 Part 2: 927 Part 1 took 0.000727958 seconds, part 2 took 0.000798333 seconds.
The goal for the day was to find out if summing or multiplication would produce the result of an equation, evaluating from left to right (not as usual in maths, evaluating multiplication first). For example:
190: 10 19
Here the result is 190, so it is obvious that multiplication is needed, 10 x 19 is 190. Things get a bit more complicated when there are more numbers:
3267: 81 40 27
Here it is possible to solve the correct result by 81 + 40 * 27 and 81 * 40 + 27 since they both equal 3267. Basically this is a problem where you want to try out different combinations, also called as permutations, of how to use summing and multiplication to produce the correct end result. The input contains some formulas that do not produce the correct result with any permutation, so those are ignored when producing the end sum of the game.
One way to solve this is using recursion – trying different combinations based on the previous evaluation step result with the current number and looking if, after evaluating all the numbers, this produces the correct result.
Simplified, the recursive part looks like the code below. Parameters are:
result: The expected end result of the evaluation.
intermediate: The intermediate result of the previous + and * operations on the previous numbers, and
numbers: the numbers not yet evaluated.
The return value is the result of the evaluation, either the correct result or zero if evaluation didn’t result in the correct result.
func evaluate(_ result: Int, _ intermediate: Int, _ numbers: [Int]) -> Int {
if numbers.count == 1 {
if intermediate + numbers.first! == result {
return result
} else if intermediate * numbers.first! == result {
return result
}
return 0
}
let nextNumber = numbers.first!
let summed = intermediate + nextNumber
let multiplied = intermediate * nextNumber
let evaluateSummed = evaluate(result, summed, Array(numbers.suffix(numbers.count - 1)), useConcatOperator: useConcatOperator)
if evaluateSummed == result {
return result
}
let evaluateMultiplied = evaluate(result, multiplied, Array(numbers.suffix(numbers.count - 1)), useConcatOperator: useConcatOperator)
if evaluateMultiplied == result {
return result
}
return 0
}
Recursion must always stop, otherwise it would continue forever – or actually, until stack memory available to the program runs out, producing a stack overflow.
Ending the recursion is handled first; if there is only one number left (the rightmost) we check if the intermediate result of the previous calculations together with the last number produce the expected result either by summing or multiplying. If not, return zero to indicate that this permutation of operations does not work correctly .
If there was more than one number left, both the sum and multiplication is calculated from the previous intermediate result, together with the current number. Then both intermediate values are used — recursively calling evaluate twice with sum and multiplied intermediate — with the rest of the numbers, checking if this produced the result we want. If not, code continues to the end of the function and returns zero — cannot do the thing with this permutation of operations.
I could have optimized the result, finishing the recursion earlier if the produced intermediary sum is greater than the end result. In that case, it is not useful to continue handing the numbers that are still left. Maybe I’ll add that later.
Second part of the puzzle added a third operator, concatenation. In addition to summing or multiplying, two numbers could be also concatenated, e.g. numbers 12 and 34 would result in number 1234.
This was easily added to the code above, just add another else if branch in the beginning and in the end part of the algorithm above, and execute those conditionally (if the function was called from part 2 solution).
Since the non-optimized version was already fast enough, haven’t done any optimization:
$ swift run -c release AdventOfCode 7 --benchmark Building for production... Build of product 'AdventOfCode' complete! (0.17s) Executing Advent of Code challenge 7... Part 1: 12940396350192 Part 2: 106016735664498 Part 1 took 0.023434208 seconds, part 2 took 0.590302166 seconds.
Now lunch, then some socializing, maybe continuing to Day 8 puzzles today or tomorrow, let’s see…
First part of today’s puzzle was to find out the path of the guard on a map shown below, and count how many distinct positions will the guard visit before leaving the mapped area.
Guard starts walking upwards from the ^ and the # ‘s are blocks on the path, and when the guard meets one, she always turns right in front of the block.
The part 1 was quite straightforward to implement and didn’t take too long.
In part 2 the goal was to find out in how many places a new obstacle (#) can be placed, so that it causes the guard to walk in circles.
I was working on different solutions, considering using a graph again to see if the graph contains a cycle (there are algorithms for that). Since yesterday was quite a busy day otherwise, I couldn’t focus on solving the puzzle, hence I am one day behind.
Today morning I continued with the part 2 with the ideas I had been thinking about, not needing a Graph. One can detect a cycle when the same turning point is met the second time. So I use a Set<Turn> structure, containing the point and the direction where the turn was taken. Set doesn’t allow you to add a new item that is already in the set, and if this happens, it indicates a cycle in the path.
So I just started to add new obstacles in the Matrix in every possible point that is not a starting point, not already containing an obstacle, and then see if this leads to a cycle in the path, counting those places.
The solution worked with the tests, but when executed with my input data, it always produced too small a number. And I couldn’t find out why! Finally, I searched the internetz and found a comment in Reddit, someone having the same issue and explaining the reason to it:
I was changing direction and going to the next position if there was a blockade in the same iteration, instead of changing direction or moving to the next position. This worked for part 1, but for part 2, this gave me the correct answer for sample input, but for the actual input, the answer was too low, and it took me 4 hours to figure out the issue.
— ajzone007
I was having exactly the same issue! This was a one line fix, not advancing to the next position when direction changes, until the next roll of the loop! This is relatively fast:
$ swift run -c release AdventOfCode 6 --benchmark Building for production... Build of product 'AdventOfCode' complete! (0.16s) Executing Advent of Code challenge 6... Part 1: 4819 Part 2: 1796 Part 1 took 0.001846167 seconds, part 2 took 0.56900375 seconds.
Here’s the path finding algorithm, used both in part 1 and part 2, throwing an exception when a cycle is met, used in part 2 solution:
private func findPath(
from start: Point,
in matrix: Matrix<Character>
) throws -> (path: [Point], turns: Set<Turn>) {
var direction = Direction.from(matrix[start.y, start.x]!)!
var currentPoint = start
var path: [Point] = []
var turns: Set<Turn> = []
path.append(start)
repeat {
let nextPoint = currentPoint.movedTo(direction)
if !matrix.contains(y: nextPoint.y, x: nextPoint.x) {
break
}
if matrix[nextPoint.y, nextPoint.x] == "#" {
direction = direction.turnRight()
let (inserted, _) = turns.insert(Turn(point: currentPoint, direction: direction))
if !inserted {
throw Day06Error.hasCycles
}
}
else {
currentPoint = nextPoint
}
path.append(currentPoint)
} while true
return (path, turns)
}
The Set return value is not actually used in the calling code, so that could be taken out.
Yesterday was a busy day at work and also after, shopping for today: the Independence Day of Finland. Soon I’ll go to see the cannons fire at Linnansaari (Castle Island) with the kids’ families so just a short update on yesterday’s puzzle.
Part 1 was easy and fast to implement. I used last year’s Graph<T> algorithms I implemented to find out which book pages should precede other pages in a list of page numbers. Graph contains the list of pages (vertices) and for each page, those pages that should follow that page (edges).
Second puzzle then… I started with it way too complicated. When failed, I decided to go brute force. The goal was to fix broken page orders, so I thought that I’d just create all the permutations of page orders for a certain print job and try which of those would be correct.
Well, when the input data has page sequences like…:
74,56,86,81,84,44,53,92,12,36,15,66,95,26,71
…and you realize that this sequence of 15 elements alone has 15! permutations, that is 1 307 674 368 000, and the data set has 200+ such sequences, you’ll know that this will take way too much time.
So I left this puzzle alone yesterday and continued this morning, when I woke up after 5 am and couldn’t get any sleep, waiting for the trip to see the cannons fire.
Instead of permutations, the working (and fast) algorithm just goes through the sequence and checks if a page number following the previous is in the edge list of the previous page. If not, the pages are swapped. This is done in a loop that checks if the sequence is still not correct, the checking and swapping continues until the morale improves:
private func fix(_ pages: [Int], using graph: Graph<Int>) -> [Int] {
var modPages = pages
repeat {
for index in 0..<modPages.count - 1 {
let preceding = modPages[index]
let following = modPages[index + 1]
let edges = graph.edges(for: graph.vertex(for: preceding)!)?.map ( {$0.destination.item } )
if edges != nil && !edges!.isEmpty {
if !edges!.contains(following) {
modPages.swapAt(index, index + 1)
}
} else {
modPages.swapAt(index, index + 1)
}
}
} while !isUpdateValid(modPages, graph)
return modPages
}
This did the trick, and is fast, and the puzzle is solved. Let’s see how today’s puzzle is when I return from the morning trip.
$ swift run -c release AdventOfCode 5 --benchmark Building for production... [5/5] Linking AdventOfCode Build of product 'AdventOfCode' complete! (2.73s) Executing Advent of Code challenge 5... Part 1: 4689 Part 2: 6336 Part 1 took 0.006513333 seconds, part 2 took 0.020255375 seconds.
The goal in part 1 of the puzzle was to find out how many times the word “XMAS” appears in the grid, whether it is written left-to-right, right-to-left or downwards or upwards. And diagonally, from left to right and down, or from right to left and up, as well as from left to right and up, or from right to left and down.
The Matrix implementation has an algorithm to rotate the matrix in steps of 90 degrees. So instead of traversing the grid in all those directions, I rotated it 90 degrees to find out the left-right-up-down and back occurrences of XMAS. After each rotation, I just searched let to right, covering the other directions using rotation
The diagonals are handled in the same way, but in each rotation, I only handled the upper right half of the grid, diagonally where the lines were longer than three characters. I first made the mistake of handling the corner-to-corner line in all rotated diagonals, having too large count of occurrences. When I got to see this mistake, I handled that corner-to-corner diagonal only in one of the rotations.
In all of those cases I ended up with a line, string of characters. What I needed to do then was to find out how many times the line (and the line in reversed order) contains the XMAS.
Part 2 was much more simpler than part 1, so that was quickly solved. The goal was to count how many crosses like this…:
M.S .A. M.S
…the matrix contains.
This required just two loops within, starting from the first line and column from the edge, finishing on second-to-last row and column, check if the cell contains an A and then check if the opposing corners are different and contain M and S.
This was quick on my work laptop (MacBook Pro M2, 2022):
$ swift run -c release AdventOfCode 4 --benchmark Building for production... [1/1] Write swift-version--1AB21518FC5DEDBE.txt Build of product 'AdventOfCode' complete! (0.16s) Executing Advent of Code challenge 4... Part 1: 2549 Part 2: 2003 Part 1 took 0.014584666 seconds, part 2 took 0.002027958 seconds.
The matrix rotations take time, obviously, so part 1 was slower. I considered counting the results of rotated matrices in parallel, but decided not to. That may or may not produce any speed advantage, don’t know… Also could’ve tried to do not rotating in part 1, and just go all in to the matrix from all directions and back to find the words. Well’ it works so I’ll let it be.
Today’s puzzle was about parsing. Quite straightforward to implement, having done parsing earlier for different types of structured strings, like tsv, csv, json, xml, etc.
In part 1, I anticipated that part 2 would add parsing some other instructions than mul so I did implement it so. There is an Instruction structure that specifies:
the instruction name
expected number of parameters and
the list of parameter values
Then the instruction is able to evaluate the result. Parsing returns an array of Instructions to execute, and then they are executed, accumulating the result of each mul instruction evaluated:
private struct Instruction {
let instruction: String
let params: Int
var values: [Int] = []
var result: Int {
if instruction == "mul" {
return values.reduce(1, *)
}
return 0
}
}
In part 2, the execution just changes the state; after do instruction, following instructions are evaluated, and after don't, the instructions are ignored, until next do instruction:
$ swift run -c release AdventOfCode 3 --benchmark Building for production... [6/6] Linking AdventOfCode Build of product 'AdventOfCode' complete! (2.55s) Executing Advent of Code challenge 3... Part 1: 188116424 Part 2: 104245808 Part 1 took 0.00304425 seconds, part 2 took 0.004164459 seconds.