Problem Set 2: List Folding and Cubes

Due Thursday, September 20, 11:59 pm


Current Version: 8
Last modified: Monday, September 17, 2:15 am
Changes:


Instructions

This problem set has two parts. You should write your solution to each part in a separate file: fold.ml and cube.ml To help get you started, we have provided a stub file for each part on CMS. You should download and edit these files. Once you have finished, submit your solution using CMS, linked from the course website.

Important notes about grading:

  1. Compile errors: All code you submit must compile. Programs that do not compile will likely receive an automatic zero. If you are having trouble getting your assignment to compile, please visit consulting hours. If you run out of time, it is better to comment out the parts that do not compile, than hand in a more complete file that does not compile.
  2. Naming: We will be using an automatic grading script, so it is crucial that you name your functions and order their arguments according to the problem set instructions, and that you place the functions in the correct files. Otherwise you may not receive credit for a function properly written. However, you may rename the arguments of functions as you wish.
  3. Code style: Finally, please pay attention to style. Refer to the CS 3110 style guide and lecture notes. Ugly code that is functionally correct may still lose points. Take the extra time to think out the problems and find the most elegant solutions before coding them up. Even though only the second part of the assignment explicitly addresses the style issue, good programming style is also required for the other parts of this assignment, and for all the subsequent assignments in the rest of the semester.
  4. Late assignments: Please carefully review the course website's policy on late assignments, as all assignments handed in after the deadline will be considered late. Verify on CMS that you have submitted the correct version, before the deadline. Submitting the incorrect version before the deadline and realizing that you have done so after the deadline will be counted as a late submission.

Part 1: List Folding (40 pts)

Folding is one of the most useful tools in functional programming. You will use folds many times the semester. The following exercises are designed to get you comfortable with folds that perform useful computations on lists. Note: All of these functions can be written without folding, but the point of the exercise is to get used to folding, so every function must include a folding function from the OCaml List library. Note that List.fold_left is tail recursive and List.fold_right is not. For full credit, your solution must not contain the rec keyword.

  1. (4 points) Implement list_exists using fold, following the same behavior as List.exists.

  2. (5 points) Implement a function dot_product : int list -> int list -> int that computes the dot product of two vectors inputted as lists. Recall that the dot product of two vectors is the sum of multiplying their respective components pairwise. For instance, dot_product [1;2;3] [4;5;6] = 1*4 + 2*5 + 3*6 = 32. Raise an exception Dot_product if the input lists have unequal lengths.

  3. (5 points) Implement list_flatten tail recursively using fold, following the same behavior as List.flatten (List.flatten is not tail recursive).

  4. (7 points) Implement a function mode : int list -> int option that returns the mode of the input list if a mode exists, and None otherwise.

  5. (9 points) Implement a function median : int list -> float option that returns the median of the input list if a median exists and None otherwise. Sorting functions in the List module are not allowed.

  6. (10 points) Write a function bowl_a_rama : int list -> int that uses folding to calculate the score of a game of bowling given a list of how many pins were knocked down for each roll. Assume that the input contains 11 to 21 integers inclusive, and that each integer is between 0 and 10 inclusive. For information on how to score a game of bowling, consult the following helpful resources:

Part 2: Why did the Rubik's cube cross the compiler? (60 pts)

In 1974, Hungarian sculptor and professor of architecture Ernő Rubik invented the Rubik's cube. Since then, millions of copies of the cube and other related twisty puzzles have been sold worldwide. In the non-disjoint set of avid cubers and coders, much computational exploration has been done.

The objective of this part is to construct scramblers and solvers for a Rubik's cube. We will use a 2x2x2 Rubik's cube, since it shares many of the same properties as the 3x3x3 but in the same time much easier to solve. For full credit, your solutions must not contain the rec keyword.

Notation

To be able to talk about cube, we need some definitions. We will use the standard Rubik's cube notation for referring to and turning sides as described here. To summarize:

A scramble is a series of turns applied to a solved cube intended to produce a random state, and an algorithm is a series of turns applied to an arbitrary cube intended to produce another state. Both are represented as turn lists in this assignment.

A 2x2x2 has 8 pieces, as denoted in type piece. Each piece is named by the side intersection in which it lies (eg. UBL = upper back left; DFR = down front right). Note that since all positions are relative, we can always consider one piece to be solved. In this problem set, we take that piece to be DBL, and keep it fixed as other pieces are moved around. This leaves us with 9 possible turns: F, F', F2, U, U', U2, R, R', R2.

To keep track of a cube's state, we need to note two things: orientation and permutation. A corner is a tuple of piece and orientation, and a cubie is defined as a tuple of position and corner. On a solved cube, the piece of the corner is equal to the position in every cubie. For orientation, we consider a corner to be correctly oriented if you can return it to its solved position using only U, R2, and F2. This means that those 3 moves (along with U' and U2) will never affect the orientation. More abstractly speaking, a corner is correctly oriented (O) if a color from either the U or D side is on either the U or D side, oriented clockwise (CW) if you would have to twist a correctly oriented piece 120 degrees clockwise to move the U or D color to where it is, and oriented counterclockwise (CCW) analogously. Note that visual knowledge of orientation is not required to complete this assignment; the function specifications always tell you exactly how to handle each case. Finally, a cube is a list of 8 cubies. A solved cube is represented by:

let solved_cube : cubie list = 
  [(UBL, (UBL,O)); (UBR, (UBR,O)); (UFR, (UFR,O)); (UFL, (UFL,O)); 
   (DBL, (DBL,O)); (DBR, (DBR,O)); (DFR, (DFR,O)); (DFL, (DFL,O))]

Note that the order does not matter, since all cubies tell you where on the cube they are. You may however find it useful to enforce an order, which can be useful when comparing whether two cubes have the same state. For the purpose of this assignment, only worry about random ordering of cubies for the input to state_checker.

Resources

While you don't need a 2x2x2 cube to complete this part of the problem set, having a cube with you can be helpful when visualizing what is going on. Cubes will be provided at consulting hours. In addition, below are some virtual cubes that can be useful as well:

Your Tasks

  1. State Checker (6 pts)

    Have you ever taken apart a Rubik's cube and randomly reassembled the pieces (surely you did not try peeling the stickers, the much cheaper alternative)? When pieces are randomly put together, it is not guaranteed that the resulting state will be solvable with the allowed turns. Write a function state_checker : cube -> bool that confirms two things: First, ensure that we have a valid cube. Nothing in the type specification forces the cubie list to be of length 8, nor that we have all 8 positions and all 8 corners present. Second, while any permutation of pieces on a 2x2x2 is possible, the same is not true for orientations (useful link; only rule 3 applies to 2x2x2 cubes). In fact, only two "types" of orientations are possible: either 3 corners can all be rotated in the same direction (all CW or all CCW), or a two corners can each be rotated in the opposite direction (one CW and one CCW, or vice versa), or some composition of them. For example, if we find that exactly 2 corners had orientation CW, we know it is not a valid cube state. Similarly, if there are two with orientation CW, and another with orientation CCW, it is not a valid state.

    Update: Recall that the order of cubies does not matter in our representation of the cube. Therefore, you should not expect the cubies to be of a certain order. That being said, only worry about the order of cubies in this function. For other functions in this assignment, the positions of cubies in the input cube will be in the same order as solved_cube shown above. Also, recall that the DBL cubie is fixed. As a result, there should always be a (DBL, (DBL,O)) cubie in a valid cube state.


  2. Move (10 pts)

    Now we have a checker for valid cube states, let's make it possible to apply turns on a cube. Write a function move : cube -> turn -> cube that given a cube and a turn will return the result of performing the turn on the input cube. To aid you, below are expected outputs to all single turns on a solved cube:

      
    Turn Result
    R [(UBL, (UBL, O)); (UBR, (UFR, CW)); (UFR, (DFR, CCW)); (UFL, (UFL, O));
    (DBL, (DBL, O)); (DBR, (UBR, CCW)); (DFR, (DBR, CW)); (DFL, (DFL, O))]
    R' [(UBL, (UBL, O)); (UBR, (DBR, CW)); (UFR, (UBR, CCW)); (UFL, (UFL, O));
    (DBL, (DBL, O)); (DBR, (DFR, CCW)); (DFR, (UFR, CW)); (DFL, (DFL, O))]
    R2 [(UBL, (UBL, O)); (UBR, (DFR, O)); (UFR, (DBR, O)); (UFL, (UFL, O));
    (DBL, (DBL, O)); (DBR, (UFR, O)); (DFR, (UBR, O)); (DFL, (DFL, O))]
    U [(UBL, (UFL, O)); (UBR, (UBL, O)); (UFR, (UBR, O)); (UFL, (UFR, O));
    (DBL, (DBL, O)); (DBR, (DBR, O)); (DFR, (DFR, O)); (DFL, (DFL, O))]
    U' [(UBL, (UBR, O)); (UBR, (UFR, O)); (UFR, (UFL, O)); (UFL, (UBL, O));
    (DBL, (DBL, O)); (DBR, (DBR, O)); (DFR, (DFR, O)); (DFL, (DFL, O))]
    U2 [(UBL, (UFR, O)); (UBR, (UFL, O)); (UFR, (UBL, O)); (UFL, (UBR, O));
    (DBL, (DBL, O)); (DBR, (DBR, O)); (DFR, (DFR, O)); (DFL, (DFL, O))]
    F [(UBL, (UBL, O)); (UBR, (UBR, O)); (UFR, (UFL, CW)); (UFL, (DFL, CCW));
    (DBL, (DBL, O)); (DBR, (DBR, O)); (DFR, (UFR, CCW)); (DFL, (DFR, CW))]
    F' [(UBL, (UBL, O)); (UBR, (UBR, O)); (UFR, (DFR, CW)); (UFL, (UFR, CCW));
    (DBL, (DBL, O)); (DBR, (DBR, O)); (DFR, (DFL, CCW)); (DFL, (UFL, CW))]
    F2 [(UBL, (UBL, O)); (UBR, (UBR, O)); (UFR, (DFL, O)); (UFL, (DFR, O));
    (DBL, (DBL, O)); (DBR, (DBR, O)); (DFR, (UFL, O)); (DFL, (UFR, O))]


  3. Scramble Generator (13 pts)

    What good does moving do if we don't have scrambles? We will implement two scrambling functions here, one returning all possible scrambles, and the other returning one randomly generated scramble.

    1. gen_scrambles (9 pts)

      A scramble is any sequence of turns. Write a function gen_scrambles : int -> bool -> scramble list such that gen_scrambles n b returns a list of scrambles of length n. If the boolean argument to gen_scrambles is true, then you should return all length n combinations of R, R', R2, etc. However, note that applying, say, the turn R followed by the turn R' is an identity — we can cancel them out. Similarly, note that an F followed by an F is the same as an F2, and a U followed by a U2 is the same as a U', and so on. Therefore, if the bool argument is false, you must take this into account for counting moves: [R; U; R; U; R; U] is still of length 6, but in [R; U; F'; U; U'; F2], we can cancel the U and U' and then combine the F' and F2, giving [R; U; F] — a length 3 scramble.

    2. gen_scramble (4 pts)

      For practicing solves, cubers like to perform a randomly generated scramble on a solved cube. Write a function gen_scramble : int -> scramble that produces a random scramble of length specified by the int. You may find the OCaml standard Random module useful. You should reduce moves in this part, as you do for gen_scrambles with a false bool argument.


  4. Cube Solver (31 pts)

    Now that we have a cube and know how to make moves on it, let's try to solve one from any valid state! While there are many methods to solve a Rubik's cube, here we use two of them (one in part 2, one in karma).

    The first method (orient_cube_alg, permute_cube_alg, solve_cube_alg) is one of the simplest and most procedural approaches, and it involves using conjugates. We solve the cube in two big steps: orienting all corners, then permuting all corners. The idea is that we solve the cube bit by bit, making a little progress every time toward the finish. Once you know what action needs to be applied to which corners, the hardest part is how to do it. For example, you know that the current corner at UBR has orientation CW and needs to be twisted counterclockwise, and current corner at DFR is CCW and needs to be twisted clockwise, but how do you twist them? This is where conjugate comes to play. Using an algorithm (get_setup_moves UBR DFR), we can bring the two pieces to UFL and UFR respectively while preserving their orientations. Then we use an algorithm (orient_corners) that twists UFL counterclockwise, and UFR clockwise. Finally, we use the inverse of the setup moves to revert corners back to their previous positions, but in their new orientations. In fact, we can keep on using this technique until every corner is oriented correctly. Similar process can be used for permuting corners. For example, the current corner at UFR belongs to UBR. Using an algorithm (get_setup_moves UFR UBR), we can move the two corners at the two positions to UFL and UFR, respectively, while preserving their orientations. This time, we use an algorithm (permute_corners) that swaps the corners at UFL and UFR, then again undo the setup moves to return the two corners to their former positions. As with orienting corners using conjugates, we can permute corners in this fashion until all corners are in the correct positions.

    Below are the functions you have to implement for part 2:

    1. gen_inverse_algorithm (3 pts)

      Each turn has an inverse. If you perform R and then R', you get an identity, just as if you apply U2 and then another U2. Write a function gen_inverse_algorithm : algorithm -> algorithm that given an algorithm, will return its inverse. That is, if scr is a scramble, then apply_scramble (scr @ (gen_inverse_algorithm scr)) gives you a solved cube (bad coding style used for ease of illustration).


    2. gen_conjugate_algorithm (2 pts)

      We have implemented a working gen_conjugate_algorithm for you. However, the current implementation uses the "@" operator, which is often not the ideal way to concatenate lists together, especially on big lists. Reimplement this function to be tail recursive.


    3. orient_cube_alg (12 pts)

      Write a function orient_cube_alg : cube -> algorithm that given a cube, returns an algorithm that when applied, makes all cubies on the input cube have the correct orientations.


    4. permute_cube_alg (12 pts)

      Write a function permute_cube_alg : cube -> algorithm that given a cube, returns an algorithm that when applied, makes all cubies on the input cube have the correct permutations.


    5. solve_cube_alg (2 pts)

      Write a function solve_cube_alg : cube -> algorithm that given a cube, returns an algorithm that when applied, makes all cubies on the input cube have the correct orientations and permutations.



Karma

semiopt_alg

Write a function semiopt_alg : cube -> algorithm that given a cube, returns an 11-move algorithm that when applied, makes all cubies on the input cube have the correct orientations and permutations.

This method (semiopt_alg) is unlikely to be achievable by hand, and instead requires assistance from computers. The orientation-permutation method, while correct, often requires over 100 moves overall to complete. However, it has been proven that a 2x2x2 cube in any state can be solved using 11 moves or fewer. In this problem set, we will implement a function that always returns a solution of 11 moves (assume that there is an 11-move solution to all 2x2x2 states).

One way to solve is exhaustively searching all possible 11 moves starting from the scrambled state, and check if any reaches the solved state. This is computationally very expensive though. A clever trick similar to a meet-in-the-middle attack (often used in breaking crypto systems) can be used to speed up the search by a huge factor:

Because hash tables are imperative by nature, we have abstracted away calls to the OCaml Hashtbl module, and you should create and modify hash tables only through the helper functions provided.