Compare commits
2 Commits
f51d122a33
...
e3f82b9c3c
| Author | SHA1 | Date | |
|---|---|---|---|
| e3f82b9c3c | |||
| 5e7253c3be |
@@ -61,3 +61,198 @@ unique palindromic partitions that can be made with the input string.
|
||||
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
|
||||
|
||||
52
Palindrome-Partitioning/cs412_palindrome_dynamic.py
Normal file
52
Palindrome-Partitioning/cs412_palindrome_dynamic.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
name: Nicholas Tamassia
|
||||
|
||||
Honor Code and Acknowledgments:
|
||||
|
||||
This work complies with the JMU Honor Code.
|
||||
|
||||
Runtime: O(n^2).
|
||||
"""
|
||||
|
||||
# Analysis:
|
||||
# The loop on line 23 iterates n times where n is the length of the input string.
|
||||
# The loop on line 28 iterates n - i times, which despite being practically fewer
|
||||
# steps on average than the line 23 loop, still scales linearly with the size of
|
||||
# the input string. Because both loops are O(n) and the second is nested in the
|
||||
# first, the overall runtime complexity is O(n^2).
|
||||
|
||||
|
||||
def palindrome_partitions(s: str) -> int:
|
||||
|
||||
n: int = len(s)
|
||||
memo: list[int] = [0] * (n + 1)
|
||||
memo[n] = 1
|
||||
|
||||
def is_palindrome(start: int, end: int) -> bool:
|
||||
return s[start:end] == s[start:end][::-1]
|
||||
|
||||
for i in range(n - 1, -1, -1):
|
||||
|
||||
sum = 0
|
||||
|
||||
for j in range(i + 1, n + 1):
|
||||
if is_palindrome(i, j):
|
||||
sum += memo[j]
|
||||
|
||||
memo[i] = sum
|
||||
|
||||
return memo[0]
|
||||
|
||||
|
||||
# All modules for CS 412 must include a main method that allows it
|
||||
# to imported and invoked from other python scripts
|
||||
def main():
|
||||
n: int = int(input())
|
||||
|
||||
for _ in range(0, n):
|
||||
in_str = input().strip()
|
||||
print(palindrome_partitions(in_str))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user