Files

Lab 4: Coding Palindrome Partitioning with Backtracking

Description

In this lab you will develop a recursive algorithm for the palindrome partition counting problem.

A palindrome is a string of letters that is the same backwards as forwards. For example, "racecar", "eevee", and "12321" are all palindromes. A string with a single character is trivially a palindrome.

A palindromic partition of a string is a partition of the string into substrings whose concatenation is equal to the original string, but such that every substring is itself a palindrome.

For example, the string, "seeks" can be broken up into the palindrome partition ["s", "ee", "k", "s"] or as ["s", "e", "e", "k", "s"]. Your task is to design and implement a recursive algorithm that counts the number of palindromic partitions of a given string.

Input

Your input will begin with a single line containing a nonnegative integer n followed by exactly n lines each of which contains an input string.

Output

You should output exactly n lines, one for each input string with each ending in a newline character. The value of each line should be the number of unique palindromic partitions that can be made with the input string.

Sample Input Sample Output
3
abc
bcccb
seeks
1
5
2

Turning it in

Submit your Python code as cs412_palindrome_count_bt.py to Gradescope.

Hints

  • As always, when devising a recursive backtracking algorithm, think of your output as a series of choices. Almost always you can get to the recursive solution by having each recursive call brute-force make all possible next choice and use the recursion fairy to handle the remaining choices.
    • What are the choices here? Think about what you need to do to the string to turn it into a palindromic partition--this will tell you exactly what a "choice" is in terms of producing palindromic partitions.
  • Don't worry about speed here. You are almost certainly going to have a rather bad runtime. We'll fix this in a few weeks when we talk about dynamic programming. You probably want to write a tiny helper predicate isPalindrome(s) that returns true if s is a palindrome and false otherwise.
  • How can you easily have a string evaluate to the reverse of itself? Recall that slicing takes 3 values (start pos, end pos, step size). See this website for details: https://www.digitalocean.com/community/tutorials/how-to-index-and-slice-strings-in-python-3

Lab 6: Coding Palindrome Partitioning with Memoization

NOTE: This lab is meant to be started and mainly completed in class.

For this lab you will modify your solution (or the sample solution) from Lab 4: Coding Palindrone Partitioning with Backtracking so that it doesn't run in exponential time. To do this , you will employ Memoization! You must manually program the memoization and NOT use any Python features (like those in functools) to accomplish this task.

The backtracking algorithm ends up repeating the same computation over and over again, because the recursive algorithm is often invoked on the same input string. For any but the shortest strings, this adds up very, very fast and the algorithm becomes unusable. In this lab we will modify the algorithm so that it uses a memoization structure to memoize any answers it computes.

Before you start working on the code, just as a sanity check, try to run your code from last week on the following input and see if your code terminates within the next minute.

1
eefffefeeefffefeffefefeeefefffefefefe

Your solution should return 8931805.

Now try it with a string that is just 3 characters longer:

1
abcdefghijklmnopqrstuvwxyzabcdefghijklmn

Why is it that this solution runs so quickly? (We will discuss in class).

And finally one that is just a little longer than that. Actually, you will probably need to hit Ctrl-C to kill this one after a few minutes (as I do not believe it will finish in any reasonable amount of time).

1
eefffefeeefffefeffefefeeefefffefefefefefefffffffefeeee

Step 1: Introduce a memoization structure to improve performance

The input to our recursive algorithm is a string. The output is an integer. Thus, for any input string, we would like to memoize the resulting integer. The most natural quick-and-dirty way to do this is to use a hashmap/dictionary, that maps input strings to output integers. Python implements hashmaps as first class objects in python dictionaries.

Task 1: Modify your algorithm so that it uses a dictionary to cache any computed solutions immediately before the solution is returned. The algorithm should pre-empt the recursive algorithm by checking whether the solution is already in the dictionary. If it is, then return the result immediately rather than running the recursive algorithm.

Verify your results: Run your modified code on the input above (the one that would never finish). You should get 82654655060 possible ways of subdividing the string. That was fast too, wasn't it? Unless you killed it, your other solution is still running (and will be until the heat death of the universe...).

Name your code cs412_palindrome_partition_memoized_a.py and turn it in into Gradescope

Step 2: Stop using strings as inputs.

Ok. This is going to be a bit trickier conceptually. Your recursive algorithm, right now, is taking a string as input and then slicing and dicing it to produce inputs for the recursive calls. This is fine and works in this case on the inputs we've tested, but one of the great things about memoization and dynamic programming is that it allows us to completely eliminate recursion. This is important, because many languages (like Python) have a maximum recursion depth that is fairly limited (for example, Python's is usually 1,000 and Java's is 1024). This means that if you wanted to run your algorithm on a string of size 1025 in Java, it would get a stack overflow (Haskell, on the other hand, has an infinite stack, maybe you shouldn't be so happy programming Java and Python all the time...).

It is a little tricky to use dynamic programming on a recursive algorithm that is splitting strings to produce the next subproblem's input. (Or, similarly, if your algorithm is working on a list and you are creating sublists, a string is just a fancy list of characters after all.) It turns out to be much easier to convert a memoized recursive algorithm that operates on a string or list to a loop if we never split the string or list into smaller pieces.

But how can we do that? You are thinking it, I know it.

Think of the following. Suppose you have a string s = "abcdef" and you want to pass the substring "cdef" beginning at index 2. You could create the string "cdef" explicitly using a substring method and pass the string to the next call. Or you could decide to be clever. Instead of passing the substring, just pass the string s again (or make it a global string defined in an outer function) and an index representing the substring you want. So instead of passing "cdef", you just pass the index 2. Your algorithm would then use index 2 implicitly to mean the substring of the original string s beginning at index 2.

This way your algorithm is really using an integer as input to the recursive calls of the algorithm. The original string remains unmodified throughout the algorithm and is really just a constant. You can pass a reference to this constant to each recursive call, or you can create an outer function and define it (see the example below), or set up a class that just stores it as member data, or something like that. It doesn't matter how you do it, you just need to get it into your head that the variable part of the input to the recursive part of your algorithm is no longer the string but is instead the integer. Leave the string alone. You'll thank me later.

Task 2: Now for the hard work. Modify your solution above so that you track substrings implicitly by passing integers as input representing the start of the substring instead of passing the substrings themselves. You may pass a reference to the input string as well (i.e. avoid making it global), but every recursive call should get the same string. To get credit for this portion of the lab, you must:

  • Use an index to access the memoized structure (in other words, do NOT use a string)
  • your recursive function should accept the index to the string as the argument (and not a substring).
  • To receive full credit, you need to use a list to store the memoized data (and not a dictionary)

Python allows you to create nested functions which can be very helpful in these situations. Take the following example:

def countPalindroneParts(input_string):
   def countPP(i):
      # your recursive code here
      return something
   
   return countPP(0) # first call to inner method

This will set you up for our next week of dynamic programming, where we replace recursion with iteration like the monsters we are.

Name your modified code cs412_palindrome_partition_memoized_b.py and turn it in on Gradescope. It should behave the same as your first solution except it must use a nested function and not pass any strings in the recursive calls.

Turn It In:

Submit both cs412_palindrome_partition_memoized_a.py (5 points) and cs412_palindrome_partition_memorized_b.py (5 points) to Gradescope by the due date.

Lab 7: Dynamic Programming

Dynamic Programming Solution to Palindromic Partitions

Your first task is to modify your solution (or the sample solution) from Lab 6: Coding Palindrome Partitioning with Memoization so that your algorithm no longer uses recursion, but instead fills the memoization structure via a loop. To do this, follow the steps outlined below.

  1. The memoization structure from the previous lab is a 1D array. Your first task is to figure out which direction the recursive algorithm fills the array. This is determined by how our recursive subproblems are generated.
    • If, in order to solve a problem on input i, the recursive algorithm recurses on a value larger than i, then determining the solution for i requires that we have already computed solutions for larger values. Thus, we need to loop backwards through the memoization array, since each iteration will fill in a value at a particular index i and need access to already computed values for larger indices. On the other hand, if we recurse on smaller values of i then our loop should iterate forwards through the array. Drawing a diagram of your memoization structure with dependency arrows can help you determine this.
  2. Your next task is to initialize a memoization structure and write a loop that fills in the memoization structure in the proper direction.
  3. Next you need to initialize the entries in the memoization data structure that represent base cases.
  4. Next, modify your recursive algorithm by replacing recursive calls with direct lookups into the memoization structure, and place the code for this into the body of your loop. In other words, your code should not contain ANY recursive calls.
  5. Analyze the (analytical) runtime of your algorithm and put the runtime into the comments section at the top of your program. Your analysis should reference specific lines of code (i.e., the loops on lines xx-yy take these many steps).
  6. Turn your code in as cs412_palindrome_dynamic.py

Submit your code to Gradescope. Gradescope only checks that the correct solution is achieved. Code that is submitted that does not use dynamic programming (that is, does not fill in an array with the answers) will receive 0 credit.

Rubric:

  • (7 points) your code does not use recursion and fills in the memoized structure correctly
  • (2 points) your comments are at the top of the code and correctly identify the run time of the algorithm.
  • (1 point) instructor code review