From 5e7253c3be2b406bbc7b3cf5ed66d8be30dbaf60 Mon Sep 17 00:00:00 2001 From: Eclypsed Date: Sun, 21 Sep 2025 16:24:55 -0400 Subject: [PATCH] Palindrome Paritioning: Updated README for Lab 6 & 7 --- Palindrome-Partitioning/README.md | 195 ++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/Palindrome-Partitioning/README.md b/Palindrome-Partitioning/README.md index f3f204e..281cb2e 100644 --- a/Palindrome-Partitioning/README.md +++ b/Palindrome-Partitioning/README.md @@ -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