Assignment 5
Starter pack: a5-starter.zip — download, open in IntelliJ, and fill in the TODO stubs.
Submission: Upload a single .zip file named u{Student ID}.zip (e.g., u6880517.zip) to MS Teams Classroom.
Typesetting. For written questions, typeset your answers and produce a PDF named hw5.pdf; place it at the top level of your zip. We do not accept scanned or handwritten submissions. Proper typesetting means math symbols — summations, fractions, exponents, fractions — are rendered with correct notation, bounds, and spacing, as you would see in a textbook.
Coding logistics. Your submission is graded by a script before any human reviews it. The script is unforgiving, so follow these instructions exactly:
- Name each source file exactly as specified in the task and place it at the top level of your zip — no subdirectories.
- Use the exact function, class, and interface names given.
AI Usage Policy. Before you begin, remind yourself of the AI usage policy and guideline on the course website. In brief: Don’t offload your learning and thinking to AI. Use AI to help you learn.
Missing Tile (20 pts)
Save your work in MissingTile.kt.
There are four ways an L-shaped triomino can be arranged. Labeled by the missing corner, the four arrangements are:
You have seen this theorem in Discrete Math:
Theorem. Any \(2^n\)-by-\(2^n\) grid with one painted cell can be tiled using L-shaped triominoes such that the entire grid is covered, no two triominoes overlap, and no triomino covers the painted cell.
Subtask I (6 pts) — written. Prove this theorem in your own words, by induction on \(n\).
Subtask II (14 pts) — implementation. Implement the function
fun tileGrid(board: Grid)
that tiles the given board using L-shaped triominoes. The Grid interface is provided in the starter pack:
interface Grid {
// Width/height of the square grid; coordinates are 0..size()-1.
fun size(): Int
fun getPaintedCellX(): Int
fun getPaintedCellY(): Int
// Place a triomino whose missing corner is at (x, y).
// Orientation: 0 = missing NE, 1 = missing SE, 2 = missing SW, 3 = missing NW.
// Returns true on success; false if the placement conflicts with an existing tile
// or the painted cell.
fun setTile(x: Int, y: Int, orientation: Int): Boolean
fun isFullyTiled(): Boolean
}
For example, a \(4 \times 4\) board with the painted cell at \((x=3, y=1)\) can be tiled by calling:
board.setTile(1, 1, 1) // missing SE corner at (1,1)
board.setTile(3, 1, 1)
board.setTile(2, 1, 0)
board.setTile(1, 2, 0)
board.setTile(2, 2, 3)
A test driver implementing Grid is provided in the starter pack; do not modify it.
Performance. Your function must return within 2 seconds for grids up to \(1024 \times 1024\).
(Hint: To tile a \(2^k \times 2^k\) grid, recursively tile four \(2^{k-1} \times 2^{k-1}\) subgrids, each with its own painted cell.)
Tail Sum of Squares (10 pts)
Written question — answer in your PDF.
Consider the following Kotlin code:
fun sumHelper(n: Int, a: Int): Int =
if (n == 0) a else sumHelper(n - 1, a + n * n)
fun sumSqr(n: Int): Int = sumHelper(n, 0)
Prove that for \(n \geq 1\), sumSqr(n) returns \(1^2 + 2^2 + 3^2 + \cdots + n^2\). To prove this, use induction to show that sumHelper computes the right thing. (Hint: How did we prove factHelper in class?)
Mysterious Function (10 pts)
Written question — answer in your PDF.
Consider the following Kotlin function foo, which takes an integer \(n \geq 1\) and returns a Pair of integers:
fun foo(n: Int): Pair<Int, Int> {
require(n >= 1)
if (n == 1) return Pair(1, 2)
val (p, q) = foo(n - 1)
return Pair(q + p * n * (n + 1), q * n * (n + 1))
}
Prove that for \(n \geq 1\), foo(n) returns \((p, q)\) such that
\(\frac{p}{q} = 1 - \frac{1}{n+1}.\)
(Hint: induction on \(n\).)
Midway Tower of Hanoi (15 pts)
Save your work in Midway.kt.
In Tower of Hanoi, you are given \(N\) disks labeled \(0, 1, \dots, N-1\) by size. Three pegs — Peg 0, Peg 1, and Peg 2 — hold the disks. Initially all disks sit on Peg 0, smallest on top. The goal is to move all disks to Peg 1, moving one disk at a time, never placing a larger disk on a smaller one.
The following Kotlin code prints the optimal sequence of moves:
fun solveHanoi(n: Int, fromPeg: Int, toPeg: Int, auxPeg: Int) {
if (n > 0) {
solveHanoi(n - 1, fromPeg, auxPeg, toPeg)
println("Move disk ${n - 1} from Peg $fromPeg to Peg $toPeg")
solveHanoi(n - 1, auxPeg, toPeg, fromPeg)
}
}
// entry point: solveHanoi(n, 0, 1, 2)
Following these instructions uses \(2^N - 1\) moves in total. Below is a trace of all configurations for \(N = 3\) disks (left to right, step 0 through step 7):








Subtask I (5 pts) — written. Prove by mathematical induction that solveHanoi(n, ...) generates exactly \(2^n - 1\) lines of output.
Subtask II (10 pts) — implementation. The monks have been following the computer-generated instructions exactly. Given the current disk configuration, implement a function
fun stepsRemaining(diskPos: IntArray): Long
that returns the number of steps left before completing the task. diskPos[i] \(\in \{0,1,2\}\) gives the peg where disk \(i\) currently sits. You are guaranteed \(0 \leq\) diskPos.size \(\leq 63\).
For example:
stepsRemaining(intArrayOf(0))\(= 1\)stepsRemaining(intArrayOf(2, 2, 1))\(= 3\)stepsRemaining(intArrayOf(2, 2, 1, 1, 2, 2, 1))\(= 51\)
Performance. Your function must return within 1 second without generating the full instruction sequence.
(Hint: Solve intArrayOf(2, 2, 0) and intArrayOf(1, 2, 0) by hand. How many moves are made before the largest disk moves?)
Higher-Order Merge (10 pts)
Save your work in MultiwayMerge.kt.
You have \(k\) ArrayDeque<Int> instances, each sorted from small to large (duplicates allowed). Implement a function
fun mergeAll(lists: Array<ArrayDeque<Int>>): ArrayDeque<Int>
that returns a single ArrayDeque<Int> containing all elements in sorted order.
Your algorithm must run in \(O(N \log k)\) time, where \(N\) is the total number of elements and \(k =\) lists.size. Note: accessing an ArrayDeque at the front or back is \(O(1)\); accessing anywhere else costs \(O(\text{distance from end})\).
(Hint: Keep a PriorityQueue whose size never exceeds \(k\), storing the lists themselves ranked by their current front element.)
A Binary Tree Representation
The following three problems use a binary tree node class provided in BinaryTreeNode.kt from the starter pack:
class BinaryTreeNode(val key: Int, var left: BinaryTreeNode? = null, var right: BinaryTreeNode? = null)
An empty tree is represented by null. A single leaf: BinaryTreeNode(10001). A node with children: BinaryTreeNode(512, ll, rr) where ll and rr are the left and right subtrees. You may also assign T.left and T.right directly.
Let’s Grow a Tree (10 pts)
Save your work in MakeTree.kt.
Subtask I (7 pts). Implement a function
fun buildBST(keys: IntArray): BinaryTreeNode?
that takes an array of integer keys and returns a balanced binary search tree. If \(n =\) keys.size, your algorithm must run in \(O(n \log n)\) time and produce a BST no deeper than \(1 + \lfloor \log_2 n \rfloor\) levels.
Subtask II (3 pts) — written. Analyze the running time of your algorithm and argue why the resulting tree meets the depth requirement.
Uprooting (15 pts)
Save your work in Uproot.kt.
The parent-mapping representation of a tree stores, for each node, the key of its parent. Concretely it is a Map<Int, Int> where looking up a key yields the key of its parent. The root is the one key that appears as a value but never as a lookup key. Below is an example tree and its corresponding map:
val p = hashMapOf(
20 to 1,
9 to 1,
14 to 20,
2 to 9,
18 to 9
)
Subtask I (7 pts). Implement:
fun treeToParentMap(t: BinaryTreeNode): HashMap<Int, Int>
that takes a binary tree (not necessarily a BST) and returns its parent-mapping representation.
Subtask II (8 pts). Implement:
fun parentMapToTree(map: Map<Int, Int>): BinaryTreeNode?
that takes a parent-mapping map and returns the corresponding BinaryTreeNode tree. The parent-mapping has no notion of left vs. right; you may freely assign children to either side. The input is guaranteed to encode a valid binary tree (but not necessarily a BST).
Performance. On inputs with around 2,000 nodes, both functions must return within 2 seconds.
Decorative Tree (10 pts)
Save your work in Decor.kt.
Given the post-order and in-order traversal sequences of a binary tree, reconstruct the tree. The traversal sequences are produced by:
fun postList(t: BinaryTreeNode?): List<Int> =
if (t == null) emptyList()
else postList(t.left) + postList(t.right) + listOf(t.key)
fun inList(t: BinaryTreeNode?): List<Int> =
if (t == null) emptyList()
else inList(t.left) + listOf(t.key) + inList(t.right)
For example, the tree below yields:
| Traversal | Sequence |
|---|---|
| post-order | \([w,\, t,\, x,\, z,\, a]\) |
| in-order | \([w,\, x,\, t,\, a,\, z]\) |
All keys are unique. Implement:
fun mkTree(postOrder: List<Int>, inOrder: List<Int>): BinaryTreeNode?
For full credit, your algorithm must run in \(O(n)\) time, where \(n\) is the length of the traversal sequences.
(Hint: A Map can speed up locating the root within the in-order sequence. Once the root position is known in both sequences, can you determine the sizes of the left and right subtrees in \(O(1)\)?)