# 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:
```python
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