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
|
that slicing takes 3 values (start pos, end pos, step size). See this website
|
||||||
for details:
|
for details:
|
||||||
https://www.digitalocean.com/community/tutorials/how-to-index-and-slice-strings-in-python-3
|
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