CS 70

Your Turn! (Optional)

In this part, you have an open-ended opportunity to experiment and play! There are many options, and we'll just outline a handful of possibilities here. Although a few bonus points are available for this part, it's primarily for your own learning and enjoyment. (If you're pressed for time, you can skip this part.)

Whichever option you choose, document it in your README.md file (or document that you opted not to do anything further).

Ideas for Further Exploration

Here are just a few possible directions you could take things…

Length of the Solution Path

Currently, our maze solvers show the path they found, but they don't say how long it is. That's not a huge deal as you can get the job done with standard Unix tools like this:

./amaze -n -o mazes/maze-201x201-solved.txt mazes/maze-201x201.txt
tr -cd 'o' < mazes/maze-201x201-solved.txt | wc -c

But it might be nice if the solver just told you how long the path was. You could add a countState() member function to the Maze class that counts how many cells are in a given state. And then print that value out at the end of solveMazeDFS() and solveMazeBFS() (i.e., send it to std::cerr).

Number of Cells Explored

Another interesting statistic is how many cells the solver explored, which is a measure of how much work the algorithm did, and can be quite different from the length of the solution path. You would need to add a bit of extra bookkeeping in the solver functions, but not much.

Make Your Own Mazes #1 (The Easiest Way)

Rather than writing code to make mazes, you can just create your own maze text files. The format is simple: Walls are #, paths are spaces. You've seen plenty of examples in the mazes/ directory. Just make sure your maze has walls around the edges, and a single start and end point (two gaps in the rectangular outer wall).

  • Hedgehog speaking

    A chance to be creative without getting bogged down in code? Sounds wonderful!

  • Goat speaking

    Meh. We could team up. You be creative, and I'll handle not writing the code.

Make Your Own Mazes #2 (An Easy-ish Way)

It's fairly easy to turn any text file into a possible maze.

  • Read all the lines of the file.
  • Find the longest line, and pad all the other lines with spaces to make them the same length.
  • Change all non-space characters to #.
  • Add a line of # characters to the top and bottom.
  • Add a # character to the start and end of each line.

That's it. You could code this in whatever language you like. It's pretty straightforward in C++, but you could use Python or any other language you're comfortable with.

To keep the autograder happy, put your program and any mazes you make in the mazes/ directory.

Make Your Own Mazes #3 (Challenge Mode)

For a bigger challenge, you could implement a maze-generation algorithm. Wikipedia has a whole page on this topic, with links to many different algorithms. In particular, the randomized depth-first search algorithm has a lot in common with the DFS maze solver we implemented, so it might be a good place to start.

Implement your algorithm using the generateMaze function stub in solver.cpp.

  • Duck speaking

    That actually looks pretty straightforward. I might just try it.

  • Rabbit speaking

    You'll need random-number generation for this approach. Although the version in the C++ standard library has many powerful features, it's a bit daunting for simple use cases like this. CS 70 has its own small library to simplify the process, described in this document.

A Simpler Solver

Another way to solve perfect mazes is to use the “right-hand rule”: Keep your right hand on the wall, and follow it. This is a simple algorithm that doesn't require any extra data structures like stacks or queues. It won't work for mazes with loops, but it will work for perfect mazes like the ones we're using.

Add a new solveMazeRightHandRule function to solver.cpp that implements this algorithm. Add code to amaze.cpp to call it when the -r or --right-hand flag is given.

Math Fun with Resizing Strategies

Here's a mathy one!

  • LHS Cow speaking

    When we implemented CoordVector, we used a doubling strategy for resizing the internal array. That’s not the only option, of course—another natural idea is add a fixed amount K each time (linear growth).

  • Goat speaking

    Yeah, that's what I said.

Let’s compare how many total element moves (copies) happen after inserting n items, assuming we copy all existing elements on each resize.

To keep things simple, let's assume we start with an empty vector and only consider the copies that happen during resizing (not the initial insertions).

  • For doubling, start with capacity 1 and double whenever the array is full.
  • For add-K, start with capacity K and grow by +K whenever the array is full.

Doubling (capacity: 1, 2, 4, 8, …)

Every time we resize from capacity \(2^i\) to \(2^{i+1}\), we copy \(2^i\) elements. If we have inserted \(n\) items total, the resizes happen at capacities \(1,2,4,\dots\) up to the largest power of two less than \(n\).

\[ \text{Total copies}(n) = \sum_{i=0}^{\left\lfloor \log_2(n-1) \right\rfloor} 2^i = 2^{\left\lfloor \log_2(n-1) \right\rfloor + 1} - 1 \;\le\; 2n - 1. \]

Add-K (capacity: K, 2K, 3K, …)

Let \(m = \left\lceil \frac{n}{K} \right\rceil - 1\) be the number of resizes. At the \(j\)-th resize \(for (j=1,\dots,m)\), the current size is \(jK\), so we copy \(jK\) elements.

\[ \text{Total copies}(n) = \sum_{j=1}^{m} jK = K \cdot \frac{m(m+1)}{2} \approx \frac{n^2}{2K} \]

Your Turn

Given these formulas, what's the average number of copies per insertion for each strategy? Which strategy is better for large \(n\)? What happens if you change \(K\)?

Graphical Maze Output

Our Maze class is very ASCII-centric. But we could make bitmap graphics instead. There is a neat format called PBM/PGM/PPM that is very simple to write. You can create a PPM file by writing a text file with this format:

P3
# This is a comment
width height
max_color_value
R G B R G B R G B ...
R G B R G B R G B ...
...

Where

  • P3 is a magic piece of text that identifies the file type
  • width and height are the dimensions of the image in pixels
  • max_color_value is the maximum value for each color channel (usually 255)
  • The rest of the file is a list of RGB color values for each pixel, in row-major order (i.e., left-to-right, top-to-bottom).

You could add a writePPM() member function to the Maze class that writes the maze to a PPM file. You could use different colors for walls, paths, the solution path, and visited cells.

You can then convert the PPM file to a PNG file using the Netpbm tools,

ppmtopng maze.ppm > maze.png

and view it in your editor or browser.

Make a Heat Map

Our BFS solver actually finds the shortest path from the start to every cell in the maze. Using the same ideas as the Graphical Maze Output section above, you could create a heat map that shows how far each cell is from the start. Cells that are closer to the start could be colored blue, and cells that are farther away could be colored red, with a gradient in between.

Characterizing Mazes

You might think that one “random maze” is just like any other. But that’s not really true. Some mazes are “easier” than others. You could try to come up with some metrics to characterize how “hard” a maze is. For example, think about

  • Size: Larger mazes might be harder simply because they have more paths to explore.
  • Branching Factor: Mazes with more branches (i.e., more choices at each step) can be more complex.
  • Dead Ends: A maze with many dead ends might be harder to solve, as it requires more backtracking.
  • Path Length: The length of the shortest path from start to finish could be a measure of difficulty.
  • Wall Density: More walls might mean fewer paths, increasing difficulty.

You could write a program that computes these (or other!) metrics for a given maze and then see how they correlate with how long it takes your solver to find a solution.

  • Rabbit speaking

    This topic is quite the rabbit hole! Especially if you combine it with maze-generation algorithms, where you try to create mazes with specific characteristics.

  • Goat speaking

    Meh. Sounds like way too much work.

Advent of Code

Advent of Code is an annual set of programming puzzles that are released every December. Many of the puzzles involve maps where # are walls and . are open spaces, similar to our mazes. In addition, many of the puzzles are designed to be solved using search strategies like DFS and BFS.

As an example,

You'll likely have to make significant changes to your code to solve these problems, so if you choose this option, copy the entire maze/ directory to a new directory (e.g., aoc/) and work there.

  • Duck speaking

    You know, I might not do this now, but I'm putting a note in my calendar to check out Advent of Code at 9 p.m. on November 30th when this year's puzzles come out. That way I won't miss the first day!

  • Goat speaking

    Meh. Sounds too much like work.

  • Rabbit speaking

    It's a great way to practice problem-solving and coding skills. Plus, it's fun! Remember, Prof. Melissa said she has a go herself each year.

  • L-Diskie speaking

    Except that she does some of the problems on a 1982 ZX Spectrum with only 49152 bytes of RAM just to show she can!

  • Dog speaking

    Whoa! That's hardcore.

  • Goat speaking

    Meh. You're too easily impressed.

Or Your Own Adventure

Those were just a few ideas to get you started. Feel free to come up with your own ideas for things to try. If you do, please share them with the class on Piazza so others can benefit from your creativity!

Don't Forget to Update Your README.md

Whatever you choose to do (or not do), please update your README.md file to document it. If you did something, explain what you did and how to use it. If you didn't do anything further, just say so.

To Complete This Part of the Assignment...

You'll know you're done with this part of the assignment when you've done all of the following:

(When logged in, completion status appears here.)