259 lines
12 KiB
Markdown
259 lines
12 KiB
Markdown
# 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.
|
|
|
|
<table>
|
|
<tr>
|
|
<td>Sample Input</td>
|
|
<td>Sample Output</td>
|
|
</tr>
|
|
<tr>
|
|
<td><pre>3<br>abc<br>bcccb<br>seeks</pre></td>
|
|
<td><pre>1<br>5<br>2</pre></td>
|
|
</tr>
|
|
</table>
|
|
|
|
## 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
|